ReentrantLock AQS ReentrantReadWriteLock
0,Sychronized与ReentrantLock区别(两种都是常见可重入排他锁)
①ReentrantLock是JDK实现的,Sychronized是JVM实现,通过底层指令控制。
②Reentrant支持非公平锁和公平锁,sychronized只支持非公平锁。
③ReentrantLock可绑定多个条件(Condition)
对象锁靠Object类中,wait(),notify(),notifyAll(),通过对象调用(只有一个)
Contition中对象可以依赖lock定义多个,通过condition对象调用await(),signal(),signalAll().
④Reentrantlock支持可中断,sychronizd阻塞等待队列中的线程被中断唤醒会抛出异常,reentrantlock唤醒
后根据情况执行其他操作,具体搞什么我也没搞清楚,看这篇博客,很全面!!!一行一行代码分析AQS一,二,三
好像意思就是,中断也是Thread类中一个boolean标识,有点像state标识的意思,并不是操作系统里的哪个中断!
1,ReentrantLock 可重入锁(和synchronized一样是,排他锁/独占锁)
是一个类,实现Lock接口,内部类Sync(同步器)继承自AbstractQueuedSynchronizer(AQS),队列同步器。
1.1用法:生声明一个锁对象,调用lock(),unlock()即可对代码块加锁。
public class NoFairLockTest { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); try { //加锁 lock.lock(); //模拟业务处理用时 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } finally { //释放锁 lock.unlock(); } } }
2,公平锁与非公平锁
ReentrantLock支持公平锁和非公平锁
-
- 公平锁:等待队列(FIFO),等待最久的先获得锁,获取锁是顺序,每次同步队列首节点线程先获取锁
- 非公平锁:当线程调用lock()后,先CAS抢一次锁成功直接获取锁(忽略等待队列),比公平锁,有更多抢锁机会。
- 公平锁往往没有非公平锁效率高吗,有一部分线程结束释放锁后立马获取锁,刚释放的线程获取锁的纪律几率更大,
这种情况非公平锁可以减少,线程上下文切换,吞吐量更大。
Reentrantlock默认为非公平锁,看构造函数:,无参构造函数默认创建非公平同步器,有参构造函数根据boolean参数创建
public ReentrantLock() { sync = new NonfairSync(); } /** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
3,Sync,同步器,
同步器可以理解为抢锁机制(排队机制),ReentrantLock的同步器(内部类),继承与AQS。
4,AQS,
全称AbstractQueuedSynchronizer,队列同步器
4.1,源码分析:一行一行源码分析清楚AbstractQueuedSynchronizer
AQS结构:①state(同步状态,同步标识)
②同步队列(FIFO),(Node节点关系表示的,虚拟双向队列)头节点和尾节点
③排队机制(同步机制)
④等待/通知机制 Condition
- state同步状态
- 内部类:Node节点类(节点关系组成同步队列):
-
-
-
-
- Waitstate等待状态 -1为标准等待状态(-2,-3暂时不管),1为取消等待(在等待队列中挂起阻塞自旋获取同步状态的线程被
-
-
-
中断唤醒或者其他原因,取消等待)。 即>0就表示他不等了----他不应该在阻塞队列中
未指定状态的Node构造函数没有指定Waitstate值,默认未int类型默认值0,
-
-
- 同步队列示意图:头节点为持有当前锁的线程,按功能上说,不算在同步对列内。
-
- 排队(同步)机制:---独占锁(由ReentrantLock引出AQS,先看独占锁)
独占式同步状态获取:
①acquire()获取同步状态方法。首先CAS尝试try一下state是不是空闲,CASState(0,1),如果是直接获取同步状态。如果不是则addWaiter()
将节点添加至同步队列。
②addWaiter()函数构造节点,若队尾不空,将节点添加至队尾,若空则,调用enq()方法
enq()方法通过死循环CAS(自旋)来保证多线程下节点正确添加,并返回节点
③添加完节点进入acquire()方法中的acquireQueued(②返回的节点--新增节点)方法
<1>该方法让节点进入同步队列之后()一直处于自旋尝试获取同步状态,在每一次自旋尝试中,if (p == head && tryAcquire(arg))
只有前驱节点是头节点(当前获得同步状态的节点)
即自旋节点为下一个要唤醒的节点。才去获取同步状态,若获取成功,设置为head(head不属于同步队列),false=true(即获取到同步状态)
finally中执行取消等待cancelAcquire(node)。
<2>若失败,进行shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()检查。
shouldpark,是否挂起,加入等待队列的节点是需要前一个节点唤醒的,而需要确保前一个节点是能唤醒当前节点,即
前一个节点的等待状态<0,>0则说明已经取消等待了,要往前找一个靠谱的。后面else里是新建节点默认0在这里将tail设置未-1;
parkAndCheckInterrupt()检查中断情况。,则挂起线程,返回中断情况,
shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()都是在自旋循环执行的。acquire()获取执行完了,线程要么获取同步状态,
要么进入同步队列自旋抢同步状态。
独占式同步状态释放:
unparkSucessor()用于唤醒下一个节点。
共享式获取同步状态:(独占式用于实现排他锁--ReentrantLock,共享式用于实现读写锁ReentrantReadWriteLock)
写操作对资源独占式访问,渎操作对资源共享式访问。
①acquireShared() ->doAcquireShared()
与独占式一样,先将节点加入同步队列,在自旋抢同步状态,除获取条件不同是,前驱节点为head且tryAcquireShared()
返回>=0; 细节将在后面ReentrantReadWriteLock中讨论,tryAcquireShared()实现在那里
- 内部类 ConditionObject实现Condition接口,Condition主要方法:await(),sifnal(),signalAll(),
有两个Node对象成员,firstWaiter,lastWaitr,前面说过Node节点中有一个nextWaiter成员,即每一个ConditionObject维护了一个单链表--条件队列
继承接口要实现主要方法,
①await()
②signal() doSignal() transferForSignal()
调用signal()的必须是获取同步状态的当前线程
找到一个没有取消排队的WaiterNode,转移
将节点从条件队列移至等待队列,enq后面的方法为中断处理,详情看这里!
综上AQS结构为:
AQS封装了等待队列和条件队列的维护过程,自定义同步器,只需要实现根据state的争抢机制即可
5,了解完AQS,回到Reentrant Lock
①参数:
自定义同步器sync,
②构造方法:
默认是非公平锁(非公平同步器),可以选择公平锁(公平同步器)
③内部类Sync继承AQS
两个子类NonfairSync非公平同步器,FairSync公平同步器
- NonfairSync类
acquire(),AQS中的独占是获取同步状态,
nonfairTryAcquire() 在Sync类中:
独占式重入锁:state为0,表示锁空,不为0(>0),先比较持有锁线程,相同--重入,不相同--进入AQS独占式获取同步状态流程。
非公平:线程调用lock方法,先执行CAS抢一次,失败再进队列
- fairSync类:
非公平锁相对于公平锁,对新晋抢锁线程更友好,新来的比等的最久的优先级高,有一部分线程结束释放锁后立马获取锁,刚释放的线程获取锁的纪律几率更大,
这种情况非公平锁可以减少,线程上下文切换,吞吐量更大。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
④特殊方法:
Condition接口被AQS中ConditionObject内部类实现,引用在通过实现自定义同步器的锁(如ReentrantLocl)中,
sync.newCondition()在ReentrantLocl内部类自定义同步器Sync中
可以理解为,Condition对象(虽然是接口,但是中间封装了ConditionObject这一步)是通过AQS实现自定义同步器的某些锁类(如ReentrantLock)
对象调用newCondition()创建的。看起来是封装在Lock类中。实则背后为AQS服务,
6,面试题:Sychronized与ReentrantLock区别(两种都是常见可重入排他锁)
①ReentrantLock是JDK实现的,Sychronized是JVM实现,通过底层指令控制。
②Reentrant支持非公平锁和公平锁,sychronized只支持非公平锁。
③ReentrantLock可绑定多个条件(Condition)
对象锁靠Object类中,wait(),notify(),notifyAll(),通过对象调用(只有一个)
Contition中对象可以依赖lock定义多个,通过condition对象调用await(),signal(),signalAll().
④Reentrantlock支持可中断,sychronizd阻塞等待队列中的线程被中断唤醒会抛出异常,reentrantlock唤醒
后根据情况执行其他操作,具体搞什么我也没搞清楚,看这篇博客,很全面!!!一行一行代码分析AQS一,二,三
好像意思就是,中断也是Thread类中一个boolean标识,有点像state标识的意思,并不是操作系统里的哪个中断!
7,都到这里了,顺势看一下ReentrantReadWriteLock,看这篇就行
也支持公平非公平锁 ,细节在ReentrantLocK中体现了,这里不赘述了
①参数:
- 自定义同步器sync(内部类对象继承AQS)
- 读锁(内部类对象)
- 写锁(内部类对象)
②内部类:
- Sync自定义同步器:
AQS中只有一个同步状态(int),读锁和写锁都要用,就拆开用,int32位,高16位读锁用,低16位写锁用,
独占式抢锁:--给写锁用的
尝试获取写锁,跟上面很像
独占式释放:
共享式获取:---用于读锁
- protected final int tryAcquireShared(),这个函数返回的是int,独占式返回的是成不成功,在AQS的共享式获取那里
说过,执行AQS中共享式获取,要tryAcquireShared()返回>=0;说明共享式抢锁有>2种状态,不能用boolean区分。
- tryAcquireShared()
readerShouldBlock()通过,Sync子类NonfairSync和fairSync调用,AQS中apparentlyFirstQueuedIsExclusive()
该方法成立的条件是,head和等待head的节点都非空,且等待节点是写操作,即当一个写操作下一个就抢时,读操作应该
让一下,去fullTryAcquireShared();以免再读很多,写很少的情况下,写一直抢不到,因为写进入时,要等读操作都退出。
- fullTryAcquireShared()
后半部分看这里吧,说的很清楚
- tryReadLock()
共享式释放
- tryReleaseShared
- 写锁类,写操作是独占的,就等于一个ReentrantLock,自定义同步器调用ReentrantReadWriteLock的自定义同步器sync的独占式同步状态的获取与释放。
- 读锁类, 也等于一个独立的共享锁,自定义同步器调用ReentrantReadWriteLock的自定义同步器sync的共享式同步状态的获取与释放
④防止写锁饥饿:
上面介绍过了,等待队列下一个节点是写,新晋读应该让。
⑤锁降级:
已获取写锁的线程可以获取读锁,把自己的读锁放进来,防止自己写锁修改了数据还没来得及用就被别的线程修改了,造成数据不可见。
不支持锁升级,即已获取读锁线程不能再获取写锁,只要有读锁,写锁就不能抢。
总结:这篇也很好copy一下流程图:
在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
写获取流程:
写锁释放:
读锁获取:
读锁释放;
参考: