一、初始ReentrantLock
ReentrantLock 位于 java.util.concurrent.locks
包下,它实现了 Lock
接口和 Serializable
接口。ReentrantLock 是一把可重入锁
和互斥锁
,它具有与 synchronized 关键字相同的含有隐式监视器锁(monitor)的基本行为和语义,但是它比 synchronized 具有更多的方法和功能。
二、 ReentrantLock的基本方法
1. 构造函数
ReentrantLock 类中带有两个构造函数,一个是默认的构造函数,不带任何参数;一个是带有 fair 参数的构造函数。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
第二个构造函数也是判断 ReentrantLock 是否是公平锁的条件,如果 fair 为 true,则会创建一个公平锁
的实现,也就是 new FairSync()
,如果 fair 为 false,则会创建一个 非公平锁
的实现,也就是 new NonfairSync()
,默认的情况下创建的是非公平锁。FairSync 和 NonfairSync 都是 ReentrantLock 的内部类,即继承于 Sync
类。在多线程尝试加锁时,如果是公平锁,那么锁获取的机会是相同的。否则,如果是非公平锁,那么 ReentrantLock 则不会保证每个锁的访问顺序。
2. 公平锁的加锁(lock)流程详解
通常情况下,使用多线程访问公平锁的效率会非常低
(通常情况下会慢很多),但是 ReentrantLock 会保证每个线程都会公平的持有锁,线程饥饿的次数比较小
。锁的公平性并不能保证线程调度的公平性。下图为公平锁和非公平锁的加锁流程。
下图为acquire方法的三条主要流程 :
首先是第一条路线,tryAcquire 方法,顾名思义尝试获取,也就是说可以成功获取锁,也可以获取锁失败。它会调用AQS的的方法,由于ReentrantLock 实现了 AQS 接口,所以调用的是 ReentrantLock 的 tryAcquire 方法。
首先会取得当前线程,然后去读取当前锁的同步状态。如果判断同步状态是 0 的话,就证明是无锁的。如果是无锁(也就是没有加锁),说明是第一次上锁,首先会先判断一下队列中是否有比当前线程等待时间更长的线程hasQueuedPredecessors);然后通过 CAS
方法原子性的更新锁的状态,CAS 方法更新的要求涉及三个变量,currentValue(当前线程的值),expectedValue(期望更新的值),updateValue(更新的值).
if(currentValue == expectedValue){
currentValue = updateValue
}
CAS 通过 C 底层机制保证原子性。如果既没有排队的线程而且使用 CAS 方法成功的把 0 -> 1 (偏向锁),那么当前线程就会获得偏向锁,记录获取锁的线程为当前线程。然后我们看 else if
逻辑,如果读取的同步状态是1,说明已经线程获取到了锁,那么就先判断当前线程是不是获取锁的线程,如果是的话,记录一下获取锁的次数 + 1,也就是说,只有同步状态为 0 的时候是无锁状态。如果当前线程不是获取锁的线程,直接返回 false。
acquire 方法会先查看同步状态是否获取成功,如果成功则方法结束返回,也就是 !tryAcquire == false
,若失败则先调用 addWaiter方法再调用 acquireQueued 方法
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
第二条路线 addWaiter:
这里首先把当前线程和 Node 的节点类型进行了封装,Node节点的类型有两种,EXCLUSIVE
和 SHARED
,前者为独占模式,后者为共享模式。首先会进行 tail 节点的判断,有没有尾节点,其实没有头节点也就相当于没有尾节点,如果有尾节点,就会原子性的将当前节点插入同步队列中,再执行enq入队操作,入队操作相当于原子性的把节点插入队列中。如果当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
第三条路线 acquireQueued
主要会有两个分支判断,首先会进行无限循环中,循环中每次都会判断给定当前节点的先驱节点,如果没有先驱节点会直接抛出空指针异常,直到返回 true。然后判断给定节点的先驱节点是不是头节点,并且当前节点能否获取独占式锁,如果是头节点并且成功获取独占锁后,队列头指针用指向当前节点,然后释放前驱节点。如果没有获取到独占锁,就会进入 shouldParkAfterFailedAcquire
和 parkAndCheckInterrupt
方法中,下图为两个方法的源码:
shouldParkAfterFailedAcquire
方法主要逻辑是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)
使用CAS将节点状态由 INITIAL 设置成 SIGNAL,表示当前线程阻塞。当 compareAndSetWaitStatus 设置失败则说明 shouldParkAfterFailedAcquire 方法返回 false,然后会在 acquireQueued 方法中死循环中会继续重试,直至compareAndSetWaitStatus 设置节点状态位为 SIGNAL 时 shouldParkAfterFailedAcquire 返回 true 时才会执行方法 parkAndCheckInterrupt 方法。
parkAndCheckInterrupt
该方法的关键是会调用 LookSupport.park 方法,该方法是用来阻塞当前线程。
所以 acquireQueued 主要做了两件事情:如果当前节点的前驱节点是头节点,并且能够获取独占锁,那么当前线程能够获得锁该方法执行结束退出。如果获取锁失败的话,先将节点状态设置成 SIGNAL,然后调用 LookSupport.park
方法使得当前线程阻塞。如果 !tryAcquire
和 acquireQueued
都为 true 的话,则打断当前线程。公平锁的加锁主要流程如下:
3. 非公平锁的加锁流程详解
非公平锁的加锁步骤和公平锁的步骤只有两处不同,一处是非公平锁在加锁前会直接使用 CAS 操作设置同步状态,如果设置成功,就会把当前线程设置为偏向锁的线程;一处是 CAS 操作失败执行 tryAcquire
方法,读取线程同步状态,如果未加锁会使用 CAS 再次进行加锁,不会等待 hasQueuedPredecessors
方法的执行,来达到只要线程释放锁就会加锁的目的。
4. lockInterruptibly 以可中断的方式获取锁
lockInterruptibly 的中文意思为如果没有被打断,则获取锁。如果没有其他线程持有该锁,则获取该锁并立即返回,将锁保持计数设置为1。如果当前线程已经持有锁,那么此方法会立刻返回并且持有锁的数量会+1。如果锁是由另一个线程持有的,则出于线程调度目的,当前线程将被禁用,并处于休眠状态,直到发生以下两种情况之一
-
锁被当前线程持有
-
一些其他线程打断了当前线程
如果当前线程获取了锁,则锁保持计数将设置为1。
如果当前线程发生了如下情况:
-
在进入此方法时设置了其中断状态
-
当获取锁的时候发生了中断(Thread.interrupt)
那么当前线程就会抛出InterruptedException
并且当前线程的中断状态会清除。
5. tryLock 尝试加锁
仅仅当其他线程没有获取这把锁的时候获取这把锁。
6. unlock 解锁流程
unlock
和 lock
是一对情侣,它们分不开彼此,在调用 lock 后必须通过 unlock 进行解锁。如果当前线程持有锁,在调用 unlock 后,count 计数将减少。如果保持计数为0就会进行解锁。如果当前线程没有持有锁,在调用 unlock 会抛出 IllegalMonitorStateException
异常。
三、Synchronzied 和 Lock 的主要区别
-
存在层面:Syncronized 是Java 中的一个关键字,存在于 JVM 层面,Lock 是 Java 中的一个接口
-
锁的释放条件:1. 使用Syncronized获取锁的线程执行完同步代码后,会自动释放;2. 线程发生异常时,JVM会让线程释放锁;Lock 必须在 finally 关键字中释放锁,不然容易造成线程死锁。
-
锁的获取: 在 Syncronized 中,假设线程 A 获得锁,B 线程等待。如果 A 发生阻塞,那么 B 会一直等待。在 Lock 中,会分情况而定,Lock 中有尝试获取锁的方法,如果尝试获取到锁,则不用一直等待。
-
锁的状态:Synchronized 无法判断锁的状态,Lock 则可以判断。
-
锁的类型:Synchronized 是可重入,不可中断,非公平锁;Lock 锁则是可重入,可判断,可公平锁。
-
锁的性能:Synchronized 适用于少量同步的情况下,性能开销比较大。Lock 锁适用于大量同步阶段:
Lock 锁可以提高多个线程进行读的效率(使用 readWriteLock)
-
在竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态(jdk1.6中对Synchronized进行可大量的优化,二者的性能已经相近);
-
ReetrantLock 提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。
================================================================================================
简化版的加锁流程:
-
如果 lock 加锁设置成功,设置当前线程为独占锁的线程;
-
如果 lock 加锁设置失败,还会再尝试获取一次锁数量,如果锁数量为0,再基于 CAS 尝试将 state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;如果锁数量不为0或者上面的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒 。
来源:Java建设者
来源:CSDN
作者:qq_43322057
链接:https://blog.csdn.net/qq_43322057/article/details/103884879