ReentrantLock原理分析

1、Lock对象也可以实现同步和线程间的通信,Lock对象是一个接口,其实现类有ReentrantLock。ReentrantLock也可以实现线程间的同步互斥,并扩展了其他的功能。wait()/notify()通知等待的线程时是随机的,用Condition相对灵活很多,可以实现选择性通知。Synchronized关键字相当于整个Lock对象只有一个单一的Condition对象,所有的线程都注册到这个对象上。

Condition的await()相当于Object的wait(),

Condition的signal()相当于Objectnotify(),

Condition的signalAll()相当于ObjectnotifyAll().

Lock的同步和线程通信过程:

 deposit和draw类似。

ReentrantLock的lock和tryLock方法区别:假如线程A和线程B使用同一个锁LOCK,此时线程A首先获取到锁LOCK.lock(),并且始终持有不释放。如果此时B要去获取锁,有四种方式

lock(): 此方式会始终处于等待中,即使调用B.interrupt()也不能中断,除非线程A调用LOCK.unlock()释放锁。

lockInterruptibly(): 此方式会等待,但当调用B.interrupt()会被中断等待,并抛出InterruptedException异常,否则会与lock()一样始终处于等待中,直到线程A释放锁。

tryLock(): 该处不会等待,获取不到锁并直接返回false,去执行下面的逻辑。

tryLock(10, TimeUnit.SECONDS):该处会在10秒时间内处于等待中,但当调用B.interrupt()会被中断等待,并抛出InterruptedException。10秒时间内如果线程A释放锁,会获取到锁并返回true,否则10秒过后会获取不到锁并返回false,去执行下面的逻辑。

2、公平锁和非公平锁:前者表示线程获取锁的顺序是按照线程加锁的顺序来分配,即先进先出。公平锁:一个线程获取不到锁会进入block状态,被操作系统挂起,进入同步队列。获取锁的线程释放了锁后,会唤醒同步队列中的首节点,首节点从挂起态转为恢复态。即从block态转为运行态,有频繁的状态更换。非公平锁是一种抢占机制,是随机获得锁,并不一定是先来的就能先得到锁。ReentrantLock提供一个构造方法,可以简单实现公平锁或非公平锁:传入一个boolean值,传入true表示公平锁,反之是非公平锁。非公平锁的好处:一个线程如果因为获取不到锁而阻塞,他会被操作系统挂起,如果要重新恢复这个线程的话需要时间,造成了性能的损耗,而如果直接把锁分配给新来的线程,在新来的线程执行过程中再叫醒等待队列中的线程,这样可以大大提高效率。

public ReentrantLock(boolean fair) {
this.sync = (ReentrantLock.Sync)(fair ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
}

ReentrantLock几个常见方法:

getHoldCount()返回int,查询当前线程保存此lock的个数,即在此线程代码内,调用lock.lock()的次数。

getQueueLength()返回int,返回有多少线程正等待获取此lock。

isFair():判断是不是公平锁。

ReentrantLock的升级版ReentrantReadWriteLock可以实现效率的提升,读锁之间不互斥,读写互斥,写写互斥。同一时刻只允许一个线程进行写操作

  同步队列:存放的是竞争同步资源的线程的引用(不是存放线程),而等待队列,存放的是待唤醒的线程的引用。注意公平锁针对的是同步队列中的对象,公平锁的获取,严格按照等待顺序进行锁获取,线程在获取锁的时候会判断同步队列中是否有前驱节点在等待获取锁,如果有则放弃获取锁,添加到对尾,排队获取锁。如果是非公平锁,则线程在获取锁的时候不会判断队列中是否有前驱节点,直接就去尝试获取锁了。公平锁的性能往往没有非公平锁性能高,因为需要排队,进行线程的切换,要比非公平锁的切换次数多。AQS默认的是非公平锁

3、队列同步器:

a.ReentrantLock使用到了AbstractQueueSynchronizer(队列同步器AQS)AQS是用来构建锁和其他同步组件的基础框架,它使用一个int成员变量表示同步状态,通过内置的同步队列来完成资源获取线程的排队工作。AQS的同步机制依赖于变量state,其阻塞机制依赖于FIFO等待队列,AQS支持排他模式和共享模式。自定义的同步器需要实现以下方法:

boolean tryAcquire(int arg) 独占模式,arg为获取锁的次数,尝试获取资源,成功则返回True,失败则返回False。

boolean tryRelease(int arg) 独占方式。arg为释放锁的次数,尝试释放资源,成功则返回True,失败则返回False。

int tryAcquireShared(int arg) 共享方式。arg为获取锁的次数,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

boolean tryReleaseShared(int arg) 共享方式。arg为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False。

自定义同步器要么是独占方式,要么是共享方式,它们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。ReentrantLock是独占锁,所以实现了tryAcquire-tryRelease。

加锁过程:

  • 通过ReentrantLock的lock方法进行加锁,会执行AQS的Acquire方法
  • AQS的Acquire方法会执行tryAcquire

解锁过程:

  • 通过ReentrantLock的解锁方法Unlock进行解锁
  • 调用AQS的tryRelease方法

 

 

同步器是实现锁的关键,三个变量都是volatile,保证了多线程之间的可见

 state字段:可以通过修改该字段的值来实现多线程的独占模式或者共享模式,在独占模式下,会把state的初始值设置成0,每当某个线程要进行某项独占操作前,都要判断state的值是不是0,如果不是0就意味着别的线程已经进入该操作,则本线程需要阻塞等待,如果是0的话就把state设置成1,表示获取到了独占锁。这个先判断再设置的过程可以通过cas操作保证原子性,这个过程称为尝试获取同步状态。在共享模式下,会初始化state的值为10,表示某个操作允许10个线程同时进行,超过这个数量的线程就被阻塞。线程在尝试获取同步状态时判断state的值,如果等于0则意味着当前有10个线程在操作,本线程需要等待,如果state大于0,那么可以把state的值减1后进入该操作,每当一个线程完成操作的时候需要释放同步状态,就把state的值加1。

b、同步队列的实现:

这是同步队列中节点类的定义:

独占模式下,同一个时刻只能有一个线程获取到同步状态,其他同时获取同步状态的线程会被包装成一个Node节点放到同步队列中。

黄色默认是head节点,是空节点可以理解为代表当前持有锁的线程,每个节点中,除了存储当前线程,前后节点的引用外,还有一个waitStatus变量,用于描述节点当前状态,多线程并发时,队列中会有多个节点存在,这个waitStatus其实代表对应线程的状态,有的线程可能因为某些原因放弃竞争,有的线程在等待满足条件,满足之后才能执行。一共有四种状态

1)Cancelled=1:取消状态

2) Signal=-1:等待触发状态

3) Condition=-2:等待条件状态

4) Propagate=-3:状态需要向后传播

5) init=0: 一个node被初始化时的默认值

只有前一个节点的状态是signal时,当前节点的线程才能被挂起

c、同步队列实现原理:

1)线程A和B进行竞争时,A执行CAS成功state值被修改并返回true,线程A继续执行。

2)A执行CAS失败,说明B也在执行CAS并成功,此时A会执行步骤3

3)生成Node节点node,并通过CAS指令插入到同步队列的末尾(同一时刻可能会有多个Node节点插入到同步队列中),如果tail节点为空,则将head节点指向一个空节点(代表B)

 4)Node插入到对尾后,该线程不会立刻挂起,会进行自旋操作,因为B(即之前没有阻塞的线程)可能已经执行完成,所以要判断该node的前一个节点pred是否为head节点(代表线程B),如果pred==head,表明当前节点是队列中第一个有效的节点,因此再次尝试tryAcquire获取锁,如果成功获取到锁,表明线程B已经执行完成,A不需要挂起。如果获取失败,表示线程B还没完成,至少没修改state值,进行步5

 

 5)前面说过只有前一个节点pred的线程状态时signal时,当前节点会被挂起。如果pred的waitStatus==0,则通过CAS指令修改waitStatus为Node.SIGNAL。如果pred的waitStatus>0,表明pred的线程状态CANCELLED,需从队列中删除。如果predwaitStatus是Node.SIGNAL,则把线程A挂起,并等待被唤醒,被唤醒后进入步骤6.

6)被唤醒的线程调用tryAccqire重新竞争,因为锁是非公平的,有可能被新加入的线程获得,从而导致刚被唤醒的线程再次被阻塞。

d、线程释放锁的过程:调用tryRelease(),释放同步状态

AQS同步队列为什么是双向链表?

1)有可能存储在同步队列中的线程抛出异常,不再需要竞争锁,需要把线程从链表中移除,双向链表可以方便的找到其前驱节点和后面节点,改变下引用即可,复杂度是O(1)。如果是单向链表,则需要从头节点开始遍历,复杂度变成O(n)
2)新加入同步队列的线程,在进入阻塞状态之前需要判断前驱节点的状态,只有前驱节点状态是signal状态,新线程才会阻塞,所以涉及前驱节点的查找,使用双向链表效率更高

4、Condition接口

Condition可以理解为内置的条件队列。我们通过newCondition方法创建一个或多个ConditionObject对象,每一个Condition对象都包含一个等待队列,该队列是Condition实现等待通知机制的关键。使用Condition对象await和signal方法时,要先获取到锁。等待队列是一个FIFO队列,队列中每一个节点都包含一个线程的引用,该线程是在Condition对象上等待被唤醒的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁,构造成节点加入等待队列进入等待状态,这里节点Node使用的是AQS定义Node。也就是说AQS的同步队列和Condition的等待队列使用的都是AQS定义Node内部类。Condition拥有首节点和尾节点的引用,当前线程调用Condition.await(),将会以当前线程构造节点,并将该节点从尾部加入到等待队列,等待队列基本结构如下:

当前线程调用Condition.await()相当于从同步队列的首节点移动到Condition的等待队列中,并释放锁,同时线程进入等待状态,当前线程进入等待队列的过程如下:注意同步队列的首节点,封装成等待队列的节点才插入到等待队列的。注意同步队列是双向链表,等待队列是单向链表

调用当前线程的Condition.signal(),将会唤醒在等待队列中等待时间最长的节点也就是首节点,在唤醒节点前,会将该节点移到同步队列中,过程如下

调用同步器的方法将等待队列中的头结点安全的移动到同步队列的尾节点,成功的获取同步状态后,此时该线程已经成功的获取了锁。Condition.signalAll()方法,相当于对等待队列中的每一个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点。可以重新竞争下一步操作了。

ReentrantLock和Synchronized对比:

1)synchronized只能是非公平锁。而ReentrantLock可以实现公平锁和非公平锁两种

2)synchronized不能中断一个等待锁的线程,而Lock可以中断一个试图获取锁的线程

3)synchronized会自动释放锁,而ReentrantLock不会自动释放锁,必须手动释放,否则可能会导致死锁。

4) ReentrantLock对象可以绑定多个Condition对象,实现分路通知。ReentrantLock有一个同步队列,多个等待队列。Synchronized有一个同步队列,一个等待队列

Lock lock = new ReentrantLock();

Condition notFull = lock.newCondition();

Condition notEmpty = lock.newCondition();

notFull.signalAll()只会唤醒notFull.await()下等待线程,

notEmpty.signalAll()只会唤醒notEmpty.await()下等待线程

5) ReentrantLock底层调用了Unsafepark方法加锁Synchronized操作的是对象头中的Mark Word

 

posted @ 2023-02-09 16:22  MarkLeeBYR  阅读(309)  评论(0)    收藏  举报