原标题:深入 Java Lock 锁
本文原载于 SegmentFault 社区专栏
作者:陈污龟
这篇文章讲的是 Java 的 Lock 锁,主要有以下知识点:
AQS
ReentrantLock
ReentrantReadWriteLock
Lock 和 synchronized 的选择
AQS
在学习 Lock 锁之前,我们先来看看什么是 AQS?
AQS 其实就是一个可以给我们实现锁的框架,juc 包中很多可阻塞的类比如 ReentrantLock、 ReadWriteLock 都是基于 AQS 构建的。
内部实现的关键是:先进先出的队列、state 状态
在 AQS 中实现了对等待队列的默认实现,子类只要重写部分的代码即可实现(大量用到了模板代码)
AQS 同时提供了互斥模式(exclusive)和共享模式(shared)两种不同的同步逻辑。一般情况下,子类只需要根据需求实现其中一种模式,当然也有同时实现两种模式的同步类,如ReadWriteLock。
注意:ReentrantLock 不是 AQS 的子类,其内部类 Sync 才是 AQS 的子类。
State 状态
AQS 维护了一个volatile int类型的state变量,用来表示当前同步状态。
volatile 虽然不能保证操作的原子性,但是保证了当前变量 state 的可见性。
compareAndSetState
compareAndSetState 用来修改 state 状态,它是一个原子操作,底层其实是调用系统的 CAS 算法,有关 CAS 可移步:CAS
protectedfinalbooleancompareAndSetState(intexpect, intupdate){
returnunsafe.compareAndSwapInt( this, stateOffset, expect, update);
}
请求资源
acquire
acquire(int arg) 以独占方式获取资源,如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。
publicfinalvoidacquire(intarg){
if(!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt;
}
如果tryAcquire(int)方法返回 true,则 acquire 直接返回,否则当前线程需要进入队列进行排队。
addWaiter将该线程加入等待队列的尾部,并标记为独占模式。
ReentrantLock
学习 ReentrantLock 之前先来看看它实现的 Lock 接口
publicinterfaceLock{
voidlock;
voidlockInterruptiblythrowsInterruptedException;
booleantryLock;
booleantryLock(longtime, TimeUnit unit)throwsInterruptedException;
voidunlock;
Condition newCondition;
}
lock、tryLock、tryLock(long time, TimeUnit unit)和lockInterruptibly 是用来获取锁的。
unLock 方法是用来释放锁的。
newCondition 方法是创建一个条件对象,用来管理那些得到锁但是不能做有用工作的线程。
ReentrantLock,意思是"可重入锁",线程可以重复地获得已经持有的锁。ReentrantLock 是唯一实现了 Lock 接口的类。接下来我们来看看有关源码:
AQS子类
ReentrantLock 实现了三个内部类,分别是 Sync、NonfairSync 和FairSync。
abstract static classSyncextendsAbstractQueuedSynchronizer
static final classNonfairSyncextendsSync
static final classFairSyncextendsSync
这些内部类都是 AQS 的子类,这就印证了我们之前所说的:AQS 是 ReentrantLock 的基础,AQS 是构建锁的框架.
构造器
publicReentrantLock{
sync = newNonfairSync;
}
publicReentrantLock(booleanfair){
sync = fair ? newFairSync : newNonfairSync;
}
默认实现的是非公平锁,传入 true 表示使用公平锁。
加锁
ReentrantLock 中加锁使用的是lock方法
默认使用非公平锁的lock方法
加锁流程
首先会通过CAS方法,尝试将当前的 AQS 中的State字段改成从 0 改成 1,如果修改成功的话,说明原来的状态是 0,并没有线程占用锁,而且成功的获取了锁,只需要调用setExclusiveOwnerThread函将当前线程设置成持有锁的线程即可。否则,CAS操作失败之后,和普通锁一样,调用父类 AQS 的acquire(1)函数尝试获取锁。
staticfinalclassNonfairSyncextendsSync{
privatestaticfinallongserialVersionUID = 7316153563782823691L;
finalvoidlock{
if(compareAndSetState( 0, 1)) //尝试获取锁
setExclusiveOwnerThread(Thread.currentThread);
else//获取失败则调用AQS的acquire方法
acquire( 1);
}
而在 AQS 的acquire(1)函数中,会判断tryAcquire(1)以及acquireQueued(addWaiter(Node.EXCLUSIVE), arg),如果尝试获取失败并且添加队列成功的话,那么就会调用selfInterrupt函数中断线程执行,说明已经加入到了 AQS 的队列中。
注意:AQS 的tryAcquire(1)是由子类 Sync(也就是 ReentrantLockd 的静态内部类)自己实现的,也就是用到了模板方法,接下来我们去看看子类的实现。
tryAcquire是在NonfairSync类中实现的,其中调用了nonfairTryAcquire函数。
finalbooleannonfairTryAcquire(intacquires){
finalThread current = Thread.currentThread;
intc = getState;
if(c == 0) { //获取当前线程状态
if(compareAndSetState( 0, acquires)) {
setExclusiveOwnerThread(current);
returntrue;
}
}
elseif(current == getExclusiveOwnerThread) { //可重入锁
intnextc = c + acquires;
if(nextc < 0) // overflow
thrownewError( "Maximum lock count exceeded");
setState(nextc);
returntrue;
}
returnfalse;
}
在 nonfairTryAcquire 函数中,会尝试让当前线程去获取锁:
获取当前线程,以及 AQS 的状态
如果当前 AQS 的状态为 0 的话,那么说明当前的锁没有被任何线程获取,则尝试做一次CAS操作,将当前的状态设置成acquires,如果设置成功了的话,那么则将当前线程设置成锁持有的线程,并且返回 true,表示获取成功。
如果当前的状态不为0的话,说明已经有线程持有锁,则判断当前线程与持有锁的线程是否相同,如果相同的话,则将当前的状态加上 acquires 重新将状态设置,并且返回 true,这也就是重入锁的原因。
如果当前线程没有获取到锁的话,那么就会返回 false,表示获取锁失败。
源码参考:ReentrantLock 中的 NonfairSync 加锁流程
链接:
https://www.jianshu.com/p/f7d05d06ef54
ReentrantReadWriteLock
概述我们知道 synchronized 内置锁和 ReentrantLock 都是 互斥锁(一次只能有一个线程进入到临界区(被锁定的区域))
而 ReentrantReadWriteLock 是一个读写锁:
在读取数据的时候,可以多个线程同时进入到到临界区(被锁定的区域)
在写数据的时候,无论是读线程还是写线程都是互斥的
一般来说:我们大多数都是读取数据得多,修改数据得少。所以这个读写锁在这种场景下就很有用了!
ReentrantReadWriteLock实现了ReadWriteLock接口。
接口只有两个方法,一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成 2 个锁来分配给线程,从而使得多个线程可以同时进行读操作
publicinterfaceReadWriteLock{
Lock readLock;
Lock writeLock;
}
性质
读锁不支持条件对象,写锁支持条件对象
读锁不能升级为写锁,写锁可以降级为读锁
读写锁也有公平和非公平模式
读锁支持多个读线程进入临界区,写锁是互斥的
和 ReentrantLock 相比,ReentrantReadWriteLock 多了ReadLock和WriteLock两个内部类。
publicclassReentrantReadWriteLockimplementsReadWriteLock, java.io.Serializable{
privatefinalReentrantReadWriteLock.ReadLock readerLock;
privatefinalReentrantReadWriteLock.WriteLock writerLock;
读锁和写锁的状态表示
abstractstaticclassSyncextendsAbstractQueuedSynchronizer{
staticfinalintSHARED_SHIFT = 16; // 高16位为读锁,低16位为写锁
staticfinalintSHARED_UNIT = ( 1<< SHARED_SHIFT);
staticfinalintMAX_COUNT = ( 1<< SHARED_SHIFT) - 1;
staticfinalintEXCLUSIVE_MASK = ( 1<< SHARED_SHIFT) - 1;
staticintsharedCount(intc){ returnc >>> SHARED_SHIFT; }
staticintexclusiveCount(intc){ returnc & EXCLUSIVE_MASK; }
读写锁对于同步状态的实现是将变量切割成两部分,高 16 位表示读,低 16 位表示写。
看个实际例子
classczy{
privateReentrantReadWriteLock rwl = newReentrantReadWriteLock;
........
publicvoidread(Thread thread){
rwl.readLock. lock;
try{
longstart = System.currentTimeMillis;
while(System.currentTimeMillis - start <= 1) {
System. out.println(thread.getName+ "正在进行读操作");
}
System. out.println(thread.getName+ "读操作完毕");
} finally{
rwl.readLock.unlock;
}
}
}
Lock 和 synchronized 的选择
总结来说,Lock 和 synchronized 有以下几点不同:
1)Lock 是一个接口,而synchronized是 Java 中的关键字,synchronized 是内置的语言实现;
2)synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;
而 Lock 在发生异常时,如果没有主动通过 unLock 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
3)Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断;
4)通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
5)Lock 可以提高多个线程进行读操作的效率。
总结
有关 Lock 锁的知识点就到这里,如果想了解更多请参考下面链接。
参考:
Java3y 多线程
Java 技术之 AQS 详解:
https://www.jianshu.com/p/da9d051dcc3d返回搜狐,查看更多
责任编辑: