java队列加锁_深入 Java Lock 锁

原标题:深入 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 的子类。

1ee0bc639daa57c7275b7530412fa97f.png

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返回搜狐,查看更多

责任编辑:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值