并发编程面试题总结
并发编程三大特性
可见性: 当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
原子性: 线程执行一个业务过程是不可中断的,要么都执行或者都不执行。
有序性: 为了提高程序运行效率可能会对代码进行重排优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
什么是内存模型
内存模型可以理解为在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的过程抽象描述(即描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及将变量从内存中取出和写入的底层细节),不同架构下的物理机拥有不一样的内存模型,Java虚拟机是一个实现了跨平台的虚拟系统,因此它也有自己的内存模型,即Java内存模型(Java Memory Model, JMM)。
因此它不是对物理内存的规范,而是在虚拟机基础上进行的规范从而实现平台一致性,屏蔽了不同硬件平台和操作系统的内存访问差异,以达到Java程序能够“一次编写,到处运行”(即让Java程序在各种平台下都能达到一致的内存访问效果)。
JMM
JMM(Java内存模型Java Memory Model,简称JMM)它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成
volatile
被volatile修饰的变量,编译后字节码中会多出一个 lock 前缀指令,即内存屏障,主要有两个作用:1)保证可见性:在对volatile修饰的变量进⾏修改后,会强制将新值刷新到主存,并通过底层的总线嗅探机制告知当前引用该变量的地址缓存失效,其他线程会重新从内存中读取变量,更新缓存的值。
2)禁止指令重排:保证内存屏障前后特定操作的执行顺序
volatile不能保证原子性
修改 volatile变量 分为四步:
1)读取volatile变量到local
2)修改变量值
3)local值写回
4)插入内存屏障 ,即lock指令,让其他线程可见
前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改,原子性需要锁来保证。
Synchronized和Lock区别
A.synchronized属于JVM层面,是java的关键字
- 会在同步代码块前插入monitorenter指令,执行该指令时会尝试获取当前对象锁绑定的对象监视器,如果抢占成功,则能执行代码块,没有则进入等待对立等待
- Lock是一个具体的类(java.util.concurrent.locks.Lock),是api层面的锁
B.是否释放锁
- synchronized:不需要用户去手动释放锁,当synchronized代码执行后或发生异常时,系统会自动让线程释放对锁的占用,不会发生死锁
- ReentrantLock:则需要用户去手动释放锁,若没有主动释放锁,就有可能出现死锁的现象
C.等待是否中断
- 同步方法或同步代码块的执行是不可中断,除非抛出异常或者正常运行完成
- ReentrantLock:可中断,可以设置超时方法,或用interrupt()
D. synchronized是非公平锁,ReentrantLock可通过构造函数指定,默认是非公平锁。
E.Lock可以通过方法返回值判定是否成功获取锁,而synchronized无法办到。
F. synchronized属于重量级锁,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,直到获取锁的线程释放锁。 如果获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程一直等待。相比synchronized,lock则更灵活的控制锁的申请和释放。如果在并发读的环境下,lock性能优于synchnorized。
Synchronized加锁过程
在执行monitorenter指令或者获取到同步方法的标识ACC_SYNCHRONIZED时,首先从对象头中找到关联的监视器monitor对象,通过CAS尝试把monitor的_owner字段设置为当前线程,如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回;
如果设置之前的_owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行recursions ++,记录重入的次数;
如果获取锁失败,则等待其他线程释放锁。
等待时当前线程被封装成ObjectWaiter对象,通过CAS把该节点push到_cxq等待队列中后,再次通过cas自旋尝试获取锁,如果还是没有获取到锁,则将当前线程挂起,等待被唤醒。
当该线程被唤醒时,会从挂起的点继续执行尝试获取锁。
执行到monitorexit指令时,或同步方法执行完,_recursions减1,当_recursions的值减为0时,说明线程释放锁。
然后又从cxq或EntryList中获取头节点,唤醒下一个线程执行。
Synchronized锁升级流程
因为将线程唤醒和挂起操作系统都需要从用户态切换到内核态工作影响性能,为了提高获取锁和释放锁的效率,锁会随着线程的竞争情况逐渐升级,偏向锁 => 轻量级锁 => 重量级锁 。
在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。
当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:
1.虚拟机将会把对象头中的标志位设为"01",即偏向模式。
2.同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word的Thread中,如果CAS操作成功,则获取锁成功。失败,则存在锁竞争。
持有偏向锁的线程以后每次进入这个锁相关的同步块时,只需要检查是否为偏向锁、锁标志位以及ThreadlD,不用别的操作,效率高。
一旦有多个线程来竞争,就要撤销偏向锁,升级为轻量级锁
撤销时,会先等待线程到达全局安全点,判断锁对象是否处于被锁定状态,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态。
轻量级锁适用于多线程交替执行同步块的情况下。
轻量级锁执行流程:
1.判断当前对象是否处于无锁状态.,如果是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),将对象的Mark Word复制到栈帧中的Lock Record中,将LockReocrd中的owner指向当前对象。
2.JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。
3.如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,当前线程便尝试使用自旋来获取锁。
在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。
轻量级锁的释被也是通过CAS操作来进行的。
主要步骤如下:
1.取出在获取轻量级锁保存在Displaced Mark Word中的数据。
2.用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,
3.如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级锁。
有些线程可能只需等一会就可以执行了,获取轻量级锁失败后,还会通过自旋尝试获取锁。如果自旋一定次数之后依然没有获取到锁,也就只能升级为重量级锁了。
重量级锁锁标识位为10
锁消除
锁消除是指虚拟机即时编译器在运行时,对于一些代码上要求同步但是被检测不可能存在共享数据竞争的锁进行消除。例如String类型的连接操作。
锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。所以如果虚拟机检测到有一系列的连续操作都是对同一个对象反复加锁和解锁,就会将其合并成一个更大范围的加锁和解锁操作。
读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
读写锁可保证并发读的执行效率。
如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口,可以通过readLock()获取读锁,通过writeLock()获取写锁。
公平锁 | 非公平锁
公平锁: 在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
非公平锁: 即无法保证锁的获取是按照请求锁的顺序进行的。
-
非公平锁的优点在于吞吐量比公平锁大,但可能导致某个或者一些线程永远获取不到锁。
-
synchronized,是一种非公平锁,ReentrantLock和ReentrantReadWriteLock,可通过构造函数指定该锁是否是公平锁默认是非公平锁。
可重入锁(也叫做递归锁)
如果锁具备可重入性,则称作为可重入锁。当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。synchronized和ReentrantLock都是可重入锁,可重入锁最大的作用是避免死锁。
乐观锁 | 悲观锁
**悲观锁:**总是假设最坏的情况,认为要操作的数据会有人修改,所以在获取数据前先上锁,其他线程阻塞,知道持有锁的线程释放锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
**乐观锁:**总是假设最好的情况,认为要操作的数据不会有人修改,所以不会直接上锁,而是在更新的时候判断数据在此期间有没有更新过,一般使用CAS+版本号来实现,如果没则可以操作,有则自旋重新获取值比较。
乐观锁适用于*多读*的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制**,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
阻塞队列BlockingQueue
- ArrayBlockingQueue:用数组实现的有界阻塞队列,按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁。
- LinkedBlockingQueue:由链表结构组成的有界队列,但大小默认值为Integer.MAX_VALUE)。FIFO进行排序
- PriorityBlockingQueue: 支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。
- DelayQueue: 一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。(DelayQueue可以运用在以下应用场景:1.缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。2.定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。)
- SynchronousQueue: 一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。
- LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列,相当于其它队列,LinkedTransferQueue队列多了transfer和tryTransfer方法。
- LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。
BlockingQueue的核心方法:
| 方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
|---|---|---|---|---|
| 插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
| 移除 | remove(e) | poll() | take() | poll(time,unit) |
| 检查 | element() | peek() | 不可用 | 不可用 |
| 性质 | 说明 |
|---|---|
| 抛出异常 | 当阻塞队列满时:在往队列中add插入元素会抛出llegalStateException:Queue full 当阻塞队列空时:再往队列中remove移除元素,会抛出NoSuchException |
| 特殊性 | 插入方法,成功true,失败false 移除方法:成功返回出队列元素,队列没有就返回空 |
| 一直阻塞 | 当阻塞队列满时,生产者继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出。 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。 |
| 超时退出 | 当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出 |
| 队列 | 有界性 | 锁 | 数据结构 |
|---|---|---|---|
| ArrayBlockingQueue | bounded(有界) | 加锁 | arrayList |
| LinkedBlockingQueue | optionally-bounded | 加锁 | linkedList |
| PriorityBlockingQueue | unbounded | 加锁 | heap |
| DelayQueue | unbounded | 加锁 | heap |
| SynchronousQueue | bounded | 加锁 | 无 |
| LinkedTransferQueue | unbounded | 加锁 | heap |
| LinkedBlockingDeque | unbounded | 无锁 | heap |
创建线程的方式
- 继承Thread类
- 实现Runnable接口
- 使用Callable和Future
- 线程池
线程池理解
线程池灵活控制线程的创建和销毁,对线程进程统一管理。当线程池接收到任务就会去创建线程执行,如果任务数量超过设定的核心线程,就会加入等待队列,线程执行完任务就会从队列中取出任务接着执行。当队列满了且处理任务线程达到了线程池所能容纳的最大线程数量,则会启动饱和拒绝处理任务。
优点:
降低资源消耗:通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
把任务的提交和执行解耦:要执行任务的人只需把Task描述清楚,然后提交即可,不用再管内部怎么执行
线程池七大核心参数介绍
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,可通过该类自定义线程池。其构造函数有7个参数。
corePoolSize: 线程池中的常驻核心线程数
maximumPoolSize: 线程池能够容纳同时执行的最大线程数,此值必须大于等于1
keepAliveTime: 非核心线程的存活时间。
unit: keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue: 任务队列,存放被提交但尚未被执行的任务。 workQueue的类型为BlockingQueue,通常取前三种类型:
ArrayBlockingQueue:由数组结构组成的先进先出的有界阻塞队列。此队列创建时必须指定大小(重点)
LinkedBlockingQueue:由链表结构组成的先进先出的有界(如果没有指定大小,默认值为Integer.MAX_VALUE)阻塞队列。SynchronousQueue:它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。也即单个元素队列。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
LinkedTransferQueue:由链表结构绒成的无界阻塞队列。
LinkedBlockingDeque:由链表结构组成的双向阻塞队列。
threadFactory: 表示生成线程池中工作线程的线程工厂,一般使用默认即可。
**handler:**拒绝策略,表示当任务队列满了并且工作线程大于等于线程池的最大线程数时拒绝处理任务时的策略( maximumPoolSize)。
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 (默认)
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
以上内置拒绝策略均实现了RejectedExecutionHandler接口
线程池中的线程初始化
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:
- prestartCoreThread():初始化一个核心线程;
- prestartAllCoreThreads():初始化所有核心线程
线程池工作原理

线程池创建后,调用execute()方法添加一个请求任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入任务队列;
- 如果队列满了且正在运行的线程数量还小于maximumPoolSize,继续创建非核心线程运行任务;
- 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来处理任务。
-
当一个线程完成任务时,它会从队列中取下一个任务来执行
-
核心线程会常驻线程池,非核心线程空闲指定时间就会被回收
线程池有哪些
Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口
1)Executors.newSingleThreadExecutor()
- 将corePoolSize和maximumPoolSize都设置为1,只会创建一个工作线程执行任务,保证所有任务按照指定顺序执行,它使用的LinkedBlockingQueue。
- 适用于一个任务一个任务执行的场景
2)Executors.newFixedThreadPool(int)
-
创建一个定长线程池,corePoolSize和maximumPoolSize值是相等的,可控制线程最大并发数,超出的线程会在队列中等待。它使用的LinkedBlockingQueue。
-
适合执行长期任务,性能好很多。
3)Executors.newCachedThreadPool()
-
将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程,可灵活回收空闲线程。
-
适用执行很多短期异步的小程序或者负载较轻的服务器
4)Executors.newScheduledThreadPool() :线程池中设定一个时间参数,比如池子中的请求每2s执行一次
5)Executors.newWorkStealingPool(int) - Java8新增,使用目前机器上可用的处理器作为它的并行级别
实际开发中,用Executors创建线程池的方式比较少,为什么?
1) FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
阿里巴巴《Java 开发手册》 禁止使用 Executors 去创建:
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
CountDownLatch | CyclicBarrier | Semaphore
CountDownLatch,CyclicBarrier和Semaphore都是java并发包concurrent提供的并发编程的工具类,是比synchrorized(关键字)更高效的同步结构。
CountDownLatch: 某个线程阻塞等待,直到其他线程执行完,他才被唤醒执行
CyclicBarrier: 一组线程相互等待到某个状态,然后这一组线程再同时执行
Semaphore: 实现多个共享资源的互斥使用,同时控制线程的并发数
ThreadLocal
ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
ThreadLocal内部设计:每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值Object。
set方法执行流程
A. 首先获取当前线程,并根据当前线程获取一个Map
B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
C. 如果Map为空,则给该线程创建 Map,并设置初始值
get方法执行流程
A. 首先获取当前线程, 根据当前线程获取一个Map
B. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到D
C. 如果e不为null,则返回e.value,否则转到D
D. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
总结: 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。
如何预防ThreadLocal内存泄漏?
- 在使用完ThreadLocal及时的调用remove
- 在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。
- 在CurrentThread依然运行的前提下,就算忘记调用remove方法,ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
ThreadLocal如何解决哈希冲突
ThreadLocalMap使用线性探测法来解决哈希冲突的。
该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
ReentrantLock加锁解锁原理
ReentrantLock lock = new ReentrantLock();
lock.lock();//加锁
// 业务逻辑代码...
lock.unlock();//释放锁
AQS全称:AbstractQueuedSynchronizer,抽象队列同步器,它是一个并发包的基础组件,用来实现各种锁,各种同步组件。java并发包下很多API都是基于AQS来实现加锁和释放锁等功能,如ReentrantLock、ReentrantReadWriteLock、Semaphore,CountDownLatch等。
ReentrantLock加锁和释放锁的底层原理
ReentrantLock内部包含了一个AQS对象,在AQS内部有三个重要变量,一个是state,表示当前持有锁的线程的加锁次数,初始值为0,一个是记录当前加锁的是哪个线程,初始状态为null,另一个是等待队列,加锁失败的线程会被包装成node加入等待队列中。
当有个线程执行ReentrantLock的lock()方法尝试加锁时,会通过CAS将state值从0变1,操作成功则成功获取锁,设置加锁线程是自己;如果加锁操作还是之前持有锁的线程,则是持有锁的线程再次重入,将state累加;其他申请失败的线程会封装成node对象加入等待队列。当持有锁的线程释放锁的时候,state–,知道state=0,则释放锁,将加锁线程变成null,唤醒队头节点尝试获取锁。
可以发现,ReentrantLock这东西只是一个外层的API,内核中的锁机制实现都是依赖AQS组件的。
这个ReentrantLock之所以用Reentrant打头,意思是他是一个可重入锁。
AQS详解:https://blog.csdn.net/a718515028/article/details/108025661
持续更新中。。。。