java并发编程原理2 (AQS, ReentrantLock,线程池)

news2024/11/15 13:39:27

一、AQS:

1.1 AQS是什么?

AQS就是一个抽象队列同步器,abstract queued sychronizer,本质就是一个抽象类。

AQS中有一个核心属性state,其次还有一个双向链表以及一个单项链表。

首先state是基于volatile修饰,再基于CAS修改,同时可以保证三大特性。(原子,可见,有序)

其次还提供了一个双向链表。有Node对象组成的双向链表。

最后在Condition内部类中,还提供了一个由Node对象组成的单向链表。

AQS是JUC下大量工具的基础类,很多工具都基于AQS实现的,比如lock锁,CountDownLatch,Semaphore,线程池等等都用到了AQS。


state是啥:state就是一个int类型的数值,同步状态,至于到底是什么状态,看子类实现。

condition和单向链表是啥:都知道sync内部提供了wait方法和notify方法的使用,lock锁也需要实现这种机制,lock锁就基于AQS内部的Condition实现了await和signal方法。(对标sync的wait和notify)


sync在线程持有锁时,执行wait方法,会将线程扔到WaitSet等待池中排队,等待唤醒

lcok在线程持有锁时,执行await方法,会将线程封装为Node对象,扔到Condition单向链表中,等待唤醒


Condition在做了什么:将持有锁的线程封装为Node扔到Condition单向链表,同时挂起线程。如果线程唤醒了,就将Condition中的Node扔到AQS的双向链表等待获取锁。

1.2 唤醒线程时,AQS为什么从后往前遍历?

如果线程没有获取到资源,就需要将线程封装为Node对象,安排到AQS的双向链表中排队,并且可能会挂起线程

如果在唤醒线程时,head节点的next是第一个要被唤醒的,如果head的next节点取消了,AQS的逻辑是从tail节点往前遍历,找到离head最近的有效节点?

想解释清楚这个问题,需要先了解,一个Node对象,是如何添加到双向链表中的。

基于addWaiter方法中,是先将当前Node的prev指向tail的节点,再将tail指向我自己,再让prev节点指向我

如下图,如果只执行到了2步骤,此时,Node加入到了AQS队列中,但是从prev节点往后,会找不到当前节点。

image.png

1.3 AQS为什么用双向链表,(为啥不用单向链表)?

因为AQS中,存在取消节点的操作,节点被取消后,需要从AQS的双向链表中断开连接。

还需要保证双向链表的完整性,

  • 需要将prev节点的next指针,指向next节点。
  • 需要将next节点的prev指针,指向prev节点。

如果正常的双向链表,直接操作就可以了。

但是如果是单向链表,需要遍历整个单向链表才能完成的上述的操作。比较浪费资源。

1.4 AQS为什么要有一个虚拟的head节点

有一个哨兵节,点更方便操作。

另一个是因为AQS内部,每个Node都会有一些状态,这个状态不单单针对自己,还针对后续节点

  • 1:当前节点取消了。
  • 0:默认状态,啥事没有。
  • -1:当前节点的后继节点,挂起了。
  • -2:代表当前节点在Condition队列中(await将线程挂起了)
  • -3:代表当前是共享锁,唤醒时,后续节点依然需要被唤醒。

Node节点的ws,表示很多信息,除了当前节点的状态,还会维护后继节点状态。

如果取消虚拟的head节点,一个节点无法同时保存当前阶段状态和后继节点状态。

同时,在释放锁资源时,就要基于head节点的状态是否是-1。来决定是否唤醒后继节点。

如果为-1,正常唤醒

如果不为-1,不需要唤醒吗,减少了一次可能发生的遍历操作,提升性能。

1.5 ReentrantLock的底层实现原理

ReentrantLock是基于AQS实现的。

在线程基于ReentrantLock加锁时,需要基于CAS去修改state属性,如果能从0改为1,代表获取锁资源成功

如果CAS失败了,添加到AQS的双向链表中排队(可能会挂起线程),等待获取锁。

持有锁的线程,如果执行了condition的await方法,线程会封装为Node添加到Condition的单向链表中,等待被唤醒并且重新竞争锁资源

Java中除了一会讲到的线程池中Worker的锁之外,都是可重入锁。

1.6 ReentrantLock的公平锁和非公平锁的区别

  • 公平锁和非公平中的lock方法和tryAcquire方法的实现有一内内不同,其他都一样
    • 非公平锁lock:直接尝试将state从 0 ~ 1,如果成功,拿锁直接走,如果失败了,执行tryAcquire
    • 公平锁lock:直接执行tryAcquire
    • 非公平锁tryAcquire:如果当前没有线程持有锁资源,直接再次尝试将state从 0 ~ 1如果成功,拿锁直接走
    • 公平锁tryAcquire:如果当前没有线程持有锁资源,先看一下,有排队的么。
      • 如果没有排队的,直接尝试将state从 0 ~ 1
      • 如果有排队的,第一名不是我,不抢,继续等待。
      • 如果有排队的,我是第一名,直接尝试将state从 0 ~ 1
    • 如果都没拿到锁,公平锁和非公平锁的后续逻辑是一样的,排队后,就不存在所谓的插队。

生活的例子:非公平锁会有机会尝试强行获取锁资源两次,成功开开心心走人,失败,消消停停去排队。

  • 有个人前来做核酸
    • 公平锁:先看眼,有排队的么,有就去排队
    • 非公平锁:不管什么情况,先尝试做凳子上。如果坐上了,直接被扣,扣完走人,如果没做到凳子上
      • 有人正在扣嗓子眼么?
        • 没人正在被扣,上去尝试做凳子上!成功了,扣完走人。
        • 如果有人正在扣,消停去排队。

1.7 ReentrantReadWriteLock如何实现的读写锁

如果一个操作写少读多,还用互斥锁的话,性能太低,因为读读不存在并发问题。

怎么解决啊,有读写锁的出现。

ReentrantReadWriteLock也是基于AQS实现的一个读写锁,但是锁资源用state标识。

如何基于一个int来标识两个锁信息,有写锁,有读锁,怎么做的?

一个int,占了32个bit位。

在写锁获取锁时,基于CAS修改state的低16位的值。

在读锁获取锁时,基于CAS修改state的高16位的值。

写锁的重入,基于state低16直接标识,因为写锁是互斥的。

读锁的重入,无法基于state的高16位去标识,因为读锁是共享的,可以多个线程同时持有。所以读锁的重入用的是ThreadLocal来表示,同时也会对state的高16为进行追加。

二、阻塞队列高频问题:

2.1 阻塞队列

ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue

ArrayBlockingQueue:底层基于数组实现,记得new的时候设置好边界。

LinkedBlockingQueue:底层基于链表实现的,可以认为是无界队列,但是可以设置长度。

PriorityBlockingQueue:底层是基于数组实现的二叉堆,可以认为是无界队列,因为数组会扩容。

ArrayBlockingQueue,LinkedBlockingQueue是ThreadPoolExecutor线程池最常用的两个阻塞队列。

PriorityBlockingQueue:是ScheduleThreadPoolExecutor定时任务线程池用的阻塞队列跟PriorityBlockingQueue的底层实现是一样的。(其实本质用的是DelayWorkQueue)

2.2 虚假唤醒

虚假唤醒在阻塞队列的源码中就有体现。

比如消费者1在消费数据时,会先判断队列是否有元素,如果元素个数为0,消费者1会挂起。

此处判断元素为0的位置,如果用if循环会导致出现一个问题。

如果生产者添加了一个数据,会唤醒消费者1。

但是如果消费者1没拿到锁资源,消费者2拿到了锁资源并带走了数据的话。

消费者1再次拿到锁资源时,无法从队列获取到任何元素。导致出现逻辑问题。

解决方案,将判断元素个数的位置,设置为while判断。

三、线程池

3.1 线程池的7个参数(不会就回家等通知)

核心线程数,最大线程数,最大空闲时间,时间单位,阻塞队列,线程工厂,拒绝策略

3.2 线程池的状态有什么,如何记录的?

线程池不是什么时候都接活的!
线程池有5个状态。、
image.png

线程池的状态是在ctl属性中记录的。本质就是int类型
image.png

ctl的高三位记录线程池状态

低29位,记录工作线程个数。即便你指定的线程最大数量是Integer.MAX_VALUE他也到不了

3.3 线程池常见的拒绝策略(不会就回家等通知)

AbortPolicy:抛异常(默认)

image.png

CallerRunsPolicy,谁提交的任务,谁执行。异步变同步

image.png

DiscardPolicy:任务直接不要

image.png

DiscardOldestPolicy:把最早放过来的任务丢失,再次尝试将当前任务交给线程池处理

image.png

一般情况下,线程池自带的无法满足业务时,自定义一个线程池的拒绝策略。

实现下面的接口即可。

image.png

3.4 线程池执行流程(不会就回家等通知)

核心线程不是new完就构建的,是懒加载的机制,添加任务才会构建核心线程

2个核心线程 5个最大线程 阻塞队列长度为2

image.png

3.5 线程池为什么添加空任务的非核心线程

image.png

避免线程池出现工作队列有任务,但是没有工作线程处理。

线程池可以设置核心线程数是0个。这样,任务扔到阻塞队列,但是没有工作线程,这不凉凉了么~~

线程池中的核心线程不是一定不会被回收,线程池中有一个属性,如果设置为true,核心线程也会被干掉

image.png

3.6 在没任务时,线程池中的工作线程在干嘛?

线程会挂起,默认核心线程是WAITING状态,非核心是TIMED_WAITING

如果是核心线程,默认情况下,会在阻塞队列的位置执行take方法,直到拿到任务为止。

如果是非核心线程,默认情况下,会在阻塞队列的位置执行poll方法,等待最大空闲时间,如果没任务,直接拉走咔嚓掉,如果有活,那就正常干。

3.7 工作线程出现异常会导致什么问题?

是否抛出异常、影响其他线程吗、工作线程会嘎嘛?

如果任务是execute方法执行的,工作线程会将异常抛出。

如果任务是submit方法执行的futureTask,工作线程会将异常捕获并保存到FutureTask里,可以基于futureTask的get得到异常信息

出现异常的工作线程不会影响到其他的工作线程。

runWorker中的异常会被抛到run方法中,run方法会异常结束,run方法结束,线程就嘎了!

如果是submit,异常没抛出来,那就不嘎~

3.8 工作线程继承AQS的目的是什么?

工作线程的本质,就是Worker对象

继承AQS跟shutdown和shutdownNow有关系。

如果是shutdown,会中断空闲的工作线程,基于Worker实现的AQS中的state的值来判断能否中断工作线程。

如果工作线程的state是0,代表空闲,可以中断,如果是1,代表正在干活。

如果是shutdownNow,直接强制中断所有工作线程

3.9 核心参数怎么设置?

线程池的目的是为了充分发挥CPU的资源。提升整个系统的性能。

系统内部不同业务的线程池参考的方式也不一样。

如果是CPU密集的任务,一般也就是CPU内核数 + 1的核心线程数。这样足以充分发挥CPU性能。

如果是IO密集的任务,因为IO的程度不一样的啊,有的是1s,有的是1ms,有的是1分钟,所以IO密集的任务在用线程池处理时,一定要通过压测的方式,观察CPU资源的占用情况,来决定核心线程数。一般发挥CPU性能到70~80足矣。所以线程池的参数设置需要通过压测以及多次调整才能得出具体的。

比如一个业务要查询三个服务

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/356685.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分享113个HTML艺术时尚模板,总有一款适合您

分享113个HTML艺术时尚模板,总有一款适合您 113个HTML艺术时尚模板下载链接:https://pan.baidu.com/s/1ReoPNIRjkYov-SjsPo0vhg?pwdjk4a 提取码:jk4a Python采集代码下载链接:采集代码.zip - 蓝奏云 女性化妆用品网页模板 粉…

【Linux】用户分类+权限管理+umask+粘滞位说明

目录 1.用户分类 su指令 2.认识Linux权限 2.1 文件访问者的分类 2.2 文件类型和访问权限 a. 文件类型 file指令 b. 访问权限 2.3 文件权值的表示方法 a. 字母表示法 b. 八进制表示法 3.如何修改文件访问者的权限及相关指令 1. chmod指令 2. chown指令 3. chgrp指…

Python语言零基础入门教程(二十七)

Python OS 文件/目录方法 Python语言零基础入门教程(二十六) 61、Python os.utime() 方法 概述 os.utime() 方法用于设置指定路径文件最后的修改和访问时间。 在Unix,Windows中有效。 语法 utime()方法语法格式如下: os.uti…

SortableJS/Sortable拖拽组件,使用详细(Sortablejs安装使用)

简述 作为一名前端开发人员,在工作中难免会遇到拖拽功能,分享一个github上一个不错的拖拽js库,能满足我们在项目开发中的需要,支持Vue和React,下面是SortableJS的使用详细; 这个是sortableJS中文官方文档&…

kafka-6-python单线程操作kafka

使用Python操作Kafka:KafkaProducer、KafkaConsumer Python kafka-python API的帮助文档 1 kafka tools连接 (1)/usr/local/kafka_2.13-3.4.0/config/server.properties listeners PLAINTEXT://myubuntu:9092 advertised.listenersPLAINTEXT://192.168.1.8:2909…

MotoSimEG-VRC教程:动态输送带创建以及示教编程与仿真运行

目录 任务描述 简易输送带外部设备创建 输送带模型添加与配置 工件安装到输送带 输送带输送工件程序编写与仿真运行 任务描述 在MotoSimEG-VRC中创建1条输送带,并且能够实现将工件从输送带起始点位置处输送到结束点位置处。 简易输送带外部设备创建 在MotoS…

Linux系统之终端管理命令的基本使用

Linux系统之终端管理命令的基本使用一、检查本地系统环境1.检查系统版本2.检查系统内核版本二、终端介绍1.终端简介2.Linux终端简介3.终端的发展三、终端的相关术语1.终端模拟器2.tty终端3.pts终端4.pty终端5.控制台终端四、终端管理命令ps1.直接使用ps命令2.列出登录详细信息五…

RPA落地指南:什么是RPA

什么是RPA RPA在企业中起什么作用并扮演什么角色呢?想要充分了解RPA,我们需要知道RPA的相关概念、特点、功能以及能解决的问题。接下来对这些内容进行详细介绍。 1.1 RPA的3个核心概念 RPA的中文译名是“机器人流程自动化”,顾名思义&…

初始C语言 - 数组(一维数组、二维数组、数组越界、数组传参)

目录 一、一维数组的创建和初始化 1、数组的创建 2、 数组的初始化 3.一维数组的使用 数组通过下标来访问 总结: 1. 数组是使用下标来访问的,下标是从0开始。 2. 数组的大小可以通过计算得到。 4、一维数组在内存中的存储 二、 二维数组的创建和初始化 1.二…

算法导论【分治思想】—大数乘法、矩阵相乘、残缺棋盘

这里写自定义目录标题分治法概述特点大数相乘问题分治算法矩阵相乘分治算法残缺棋盘分治算法分治法概述 在分而治之的方法中,一个问题被划分为较小的问题,然后较小的问题被独立地解决,最后较小问题的解决方案被组合成一个大问题的解决。 通常…

【软件测试】自动化测试工程师必会的单元测试编写(总结),你真的了解吗......

目录:导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜)前言 单元测试编写的目的…

supervisor-男程序员的福音

supervisor是什么 supervisor是用python语言编写的,只能用于类Unix系统上的进程管理工具。 supervisor有什么用 举一个常见的场景,比如你的项目已经到了测试联调阶段,QA需要你把程序启动起来,然后进行测试,那么启动…

用Java实现多线程打印奇偶数

用Java实现多线程打印奇偶数1. wait()和 notify() 方法的作用:2. Java实现(1)Thread1.class 奇数线程(2)Thread2.class 偶数线程(3)共享资源类(4)测试1. wait()和 notify…

一篇文章带你玩转 Kubernetes:组件、核心概念和Nginx实战演示

目录一、简介1.1 容器部署时代1.2 Kubernetes有哪些优点二、Kubernetes 组件介绍三、Kubernetes 核心概念3.1 Namespace3.2 Pod3.3 Deployment3.4 Service3.5 Ingress四、Kubernetes 核心概念实战4.1 部署yaml文件4.2 通过Pod IP访问Nginx4.3 通过Service IP访问Nginx4.4 修改i…

[数据结构]:顺序表(C语言实现)

目录 前言 顺序表实现 01-开发环境 02-文件布局 03-代码 01-主函数 02-头文件 03-SeqListCommon.cpp 04-SeqListPositionOperation.cpp 05-SeqListValueOperation.cpp 结语 前言 此专栏包含408考研数据结构全部内容,除其中使用到C引用外,全为…

node+vue微信小程序的社区后勤报修系统

社区后勤报修系统小程序进行总体设计和详细设计。总体设计主要包括小程序功能设计、小程序总体结构设计、小程序数据结构设计和小程序安全设计等:详细设计主要包括社区后勤报修系统小程序数据库访问的实现,主要功能模块的具体实现,模块实现关键代码等。最后对社区后…

目标检测论文阅读:DETR算法笔记

标题:End-to-End Object Detection with Transformers 会议:ECCV2020 论文地址:https://link.springer.com/10.1007/978-3-030-58452-8_13 官方代码:https://github.com/facebookresearch/detr 作者单位:巴黎第九大学、…

【Linux】进程替换

文章目录进程程序替换替换原理替换函数函数返回值函数命名理解在makefile文件中一次生成两个可执行文件总结:程序替换时运行其它语言程序进程程序替换 程序要运行要先加载到内存当中 , 如何做到? 加载器加载进来,然后程序替换 为什么? ->冯诺依曼 因为CPU读取数据的时候只…

【原创】java+swing+sqlserver药品管理系统设计与实现

之前数据库都是用的mysql,今天我们使用sqlserver在配合swing来开发一个药品管理系统。方便医院工作人员进行药品的管理,基础功能基本都是一些增删改查操作。 功能分析: 药品管理系统主要提供给管理员和员工使用,功能如下&#x…