0%

Lock接口

本文介绍J.U.C包中的Lock接口及其相关实现类。
本文的源码如无特别说明,都是基于JDK 1.8。

一、引入背景

在JDK 1.5之前,synchronized锁机制性能不好,且功能单一(“申请锁时不能响应中断”,“未提供‘尝试申请锁失败直接返回而不挂起’接口”,“不支持读写锁”,“不支持公平锁”,“不支持不可重入锁”等),故Java并发大神Doug Lea设计了Lock接口及其相关实现类——Lock接口,ReentrantLock类,ReentrantReadWriteLock类,它们在JDK 1.5被引入。

二、J.U.C包中的Lock接口及其相关实现类

Lock接口的所有方法描述如下:

  • lock(),申请锁,如果失败则挂起直到申请成功,过程中不可响应中断
  • lockInterruptibly(),申请锁,如果失败则挂起直到申请成功,过程中可响应中断
  • tryLock(),尝试申请锁,立即返回锁申请结果——成功/失败
  • tryLock(long time, TimeUnit unit),尝试申请锁,至多等待设定的超时时间,在该过程中如果申请到锁则立即返回“成功”,否则等待设定的超时时间到期返回“失败”,过程中可响应中断
  • unlock(),释放已申请到的锁
  • newCondition(),创建生成一个与本Lock实例对象关联的Condition对象,在其上可调用await()/awaitUninterruptibly()/await(long time, TimeUnit unit)/awaitNano(long nanosTimeout)/awaitUntil(Date deadline)signal()/signalAll()方法,提供一个线程之间协作的机制(可类比于Object类的wait()/wait(long timeout)/wait(long timeout, int nanos)notify()/notifyAll()方法)。需要注意的是,只有当实际锁是“排他锁”时,调用该方法才有意义。关于Condition的实现子类详见AQS中的ConditionObject

接下来主要介绍Lock接口的相关实现类。

2.1、ReentrantLock锁

2.1.1、基本介绍

ReentrantLock锁被设计作为synchronized锁的加强改进版。

最多加锁次数:2^31-1,因为是排他锁,所以该次数指的是最多重入次数。

其锁分类是:

  • 悲观锁
  • 阻塞锁
  • 创建时可选是“公平锁”还是“非公平锁”,默认是“非公平锁”
  • 可重入锁
  • 排他锁

其常见使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockUseExample {
ReentrantLock lock = new ReentrantLock();

Condition condition = lock.newCondition();

boolean flag = false;

public void lockExample() {
lock.lock();

try {
// doSomething();
} finally {
lock.unlock();
}
}

/**
* 跟运行signalExample()方法的线程不同
*/
public void awaitExample() {
lock.lock();

try {
while (!flag) {
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

/**
* 跟运行awaitExample()方法的线程不同
*/
public void signalExample() {
lock.lock();

try {
flag = true;
condition.signal();
} finally {
lock.unlock();
}
}
}

有点需要注意:

  1. tryLock()方法的实现总是“非公平”的,不管创建ReentrantLock实例时选择的是“公平”还是“非公平”策略;lock()lockInterruptibly()tryLock(long time, TimeUnit unit)的实现遵从创建ReentrantLock实例时选择的“公平/非公平”策略

2.1.2、源码实现

ReentrantLock内部有一个AQS继承体系如下:

1
2
3
4
5
6
7
        AQS
*
*
Sync
* *
* *
FairSync NonfairSync

ReentrantLock类内有一个ReentrantLock.Sync sync实例成员变量,ReentrantLock类的所有具体实现都通过转发给sync实现,在创建类实例对象时,可选择sync具体是一个“ReentrantLock.FairSync”实例对象,还是一个“ReentrantLock.NonfairSync”实例对象,默认是一个“ReentrantLock.NonfairSync”实例对象,即“选择公平/非公平策略,默认是非公平策略”。

根据源码推导ReentrantLock锁的锁分类:

  • 根据《AQS》,我们知道,基于AQS实现的锁,天然就是“悲观锁”和“阻塞锁”
  • 如果sync具体是一个“ReentrantLock.FairSync”实例对象,查看“ReentrantLock.FairSync”源码,可知是“公平锁”,“可重入锁”和“排他锁”;如果sync具体是一个“ReentrantLock.NonfairSync”实例对象,查看“ReentrantLock.NonfairSync”源码,可知是“非公平锁”,“可重入锁”和“排他锁”

我们知道synchronized锁存在一个happens-before规则——对一个锁的解锁,happens-before于随后对这个锁的加锁,那ReentrantLock锁满足该happens-before规则吗?答案是:是的。
接下来进行证明:基于一个ReentrantLock锁,其sync实例成员变量指向一个“ReentrantLock.FairSync”实例对象。假定有线程A和B(A和B不同线程,假如A和B是同一个线程,那么根据“程序顺序规则”该条happens-before规则,直接就能推导出目标结论),A执行“代码1”中的lockA方法,B执行“代码1”中的lockB方法,A已经获得锁正在执行解锁操作,B正在执行加锁操作。解锁操作细化见“代码2”,加锁操作细化见“代码3”。

代码1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.concurrent.locks.ReentrantLock;

public class Main {

ReentrantLock lock = new ReentrantLock();

public void lockA() {
lock.lock(); // 1

try {
// 2

// 3

// 4
} finally {
lock.unlock(); // 5
}
}

public void lockB() {
lock.lock(); // 6

try {
// 7

// 8

// 9
} finally {
lock.unlock(); // 10
}
}

}

代码2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final boolean release(int arg) {
if (tryRelease(arg)) { // 51
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true; // 52
}

return false; // 53
}

protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 511
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false; // 512
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c); // 513
return free;
}

代码3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 61
selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread(); // 611
int c = getState(); // 612
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

B想要获取锁,必须是“// 612处读取到// 513处的最新state写入值(值为0)”,而state是一个volatile变量,根据volatile变量happens-before规则,有“// 513 -> // 612”,再根据“程序顺序规则”和“传递性规则”,有“1 -> 2 -> 3 -> 4 -> 5 -> 511 -> 512 -> 513 -> 612 -> 7 -> 8 -> 9 -> 10”,其实在// 513后面还有其他语句,比如“// 52”,“// 53”,在// 612前面也有其他语句,比如“// 611”,“acquireQueued方法中的其他语句,因为获取到锁的tryAcquire方法调用可能是在acquireQueued方法中触发的”,但是// 512后面的语句和// 612前面的语句并不会影响业务代码,因此我们在叙述ReentrantLock锁的happens-before规则时(关注Lock接口的lock和unlock方法级粒度),可“宏观近似”地认为有“1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10”,到此目标结论得证。

2.1.3、与synchronized锁的比较

比较维度有4个:锁分类、使用、源码实现和性能。

2.1.3.1、锁分类
悲观锁 vs 乐观锁 阻塞锁 vs 非阻塞锁 公平锁 vs 非公平锁 可重入锁 vs 非可重入锁 共享锁 vs 排他锁
synchronized锁 悲观锁 阻塞锁 非公平锁 可重入锁 排他锁
ReentrantLock锁 悲观锁 阻塞锁 可选是“公平锁”和“非公平锁”,默认是“非公平锁” 可重入锁 排他锁
2.1.3.2、使用

ReentrantLock锁的常见使用示例如上,synchronized锁的常见使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class SynchronizedUseExample {
Object obj = new Object();

boolean flag = false;

public void lockExample() {

synchronized (obj) {
// doSomething();
}
}

/**
* 跟运行notifyExample()方法的线程不同
*/
public void waitExample() {
synchronized (obj) {
try {
while (!flag) {
obj.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

/**
* 跟运行waitExample()方法的线程不同
*/
public void signalExample() {
synchronized (obj) {
flag = true;

obj.notify();
}
}
}

“使用”维度的比较描述:

  • synchronized锁的使用形式相较更简单,自动进行锁释放;对于ReentrantLock锁,需要手动进行锁释放
  • 从使用功能丰富的角度来说,ReentrantLock锁更胜一筹,除了以上常见接口,它还提供lockInterruptibly()tryLock()tryLock(long time, TimeUnit unit)等接口
2.1.3.3、源码实现

ReentrantLock锁的源码实现可参见“2.1.2、源码实现”小节,synchronized锁的源码实现可参见《synchronized锁内部实现》

竞争申请锁的核心逻辑(假定现有T1和T2两个线程尝试竞争锁,两个线程足以代表两个以上的线程竞争情形)

  • ReentrantLock非公平锁:非公平竞争锁,失败排队。当锁申请等待队列不为空时,最多可能的竞争选手有:T1T2被唤醒队列头节点所指向线程;当锁申请等待队列为空时,最多可能的竞争选手有:T1T2
  • ReentrantLock公平锁:公平竞争锁,失败排队。当锁申请等待队列不为空时,最多可能的竞争选手有:被唤醒队列头节点所指向线程;当锁申请等待队列为空时,最多可能的竞争选手有:T1T2
  • synchronized锁:
    • 在轻度竞争环境中,竞争偏向锁或者轻量级锁
    • 在重度竞争环境中,竞争重量级锁,非公平竞争重量级锁,失败排队。当锁申请等待队列不为空时,最多可能的竞争选手有:T1T2被唤醒队列头节点所指向线程_Responsible线程[1];当锁申请等待队列为空时,最多可能的竞争选手有:T1T2
2.1.3.4、性能

在JDK 1.5之前,synchronized锁的实现性能较差,这也是促进设计引入ReentrantLock锁的其中一个原因,但是随着JDK版本的发布提升,synchronized锁的实现性能不断得以提升改进,它与ReentrantLock锁的性能差距不断缩小,一个足以说明的例子是——“在JDK 1.8的ConcurrentHashMap实现中已经用synchronized替换ReentrantLock来完成分段加锁”。

性能实现的改进包括但不限于:

  • synchronized锁的内部实现从“无锁、重量级锁”两种锁状态演进到“无锁、偏向锁、轻量级锁、重量级锁”4种锁状态
  • 优化重量级锁的竞争策略

synchronized锁与ReentrantLock锁的性能对比(分成“ReentrantLock非公平锁”,“ReentrantLock公平锁”和“synchronized锁”3类。另外需要注意的是:不明确JDK版本和锁竞争程度的性能对比毫无意义):

  • 在JDK 1.5之前(包括JDK 1.5):
    • 轻度竞争,一般是ReentrantLock非公平锁 > ReentrantLock公平锁 > synchronized锁
    • 重度竞争,一般是ReentrantLock非公平锁 > ReentrantLock公平锁 > synchronized锁
  • 在JDK 1.8中
    • 轻度竞争,一般是synchronized锁 > ReentrantLock非公平锁 > ReentrantLock公平锁
    • 重度竞争,一般是ReentrantLock非公平锁 > synchronized锁 > ReentrantLock公平锁

针对以上情况,进行一些说明(自我理解):

  • 在轻度竞争情形中,从“JDK 1.5之前(包括JDK 1.5)”到“JDK 1.8”,synchronized锁的性能提升,可能是由于在JDK 1.6中引入了偏向锁和轻量级锁
  • 在重度竞争情形中,从“JDK 1.5之前(包括JDK 1.5)”到“JDK 1.8”,synchronized锁的性能提升,应该不是由于偏向锁和轻量级锁的引入,因为此时占主导作用的是synchronized重量级锁,故大概率是由于优化了重量级锁的竞争策略——根据“2.1.3.3、源码实现”小节的确可发现,在JDK 1.8中,synchronized重量级锁的竞争策略已经优化到跟ReentrantLock非公平锁/公平锁的竞争策略大同小异

2.2、ReentrantReadWriteLock锁

2.2.1、基本介绍

读写锁的设计目标应用场景是:读多写少,允许读读操作同时进行。

ReentrantReadWriteLock类其实本身并不继承实现Lock接口,其内部有两个Lock接口的继承实现类“ReentrantReadWriteLock.WriteLock”和“ReentrantReadWriteLock.ReadLock”,分别指代“写锁”和“读锁”。

“写锁”最多加锁次数:2^16-1,因为是排他锁,所以该次数指的是最多重入次数。
“读锁”最多加锁次数:2^16-1,因为是共享锁,所以该次数指的是最多“重入+非重入”次数。

上述“写锁”和“读锁”的锁分类如下表。

悲观锁 vs 乐观锁 阻塞锁 vs 非阻塞锁 公平锁 vs 非公平锁 可重入锁 vs 非可重入锁 共享锁 vs 排他锁
写锁 悲观锁 阻塞锁 创建时可选是“公平锁”和“非公平锁”,默认是“非公平锁” 可重入锁 排他锁
读锁 悲观锁 阻塞锁 创建时可选是“公平锁”和“非公平锁”,默认是“非公平锁” 可重入锁 共享锁

其常见使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockUseExample {

ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

/**
* 执行read()方法的线程跟执行write()方法的线程不同
*/
public void read() {
rwLock.readLock().lock();
try {
// doSomething();
} finally {
rwLock.readLock().unlock();
}
}

/**
* 执行write()方法的线程跟执行read()方法的线程不同
*/
public void write() {
rwLock.writeLock().lock();

try {
// doSomething();
} finally {
rwLock.writeLock().unlock();
}
}

}

有几点补充说明:

  1. 读锁调用newCondition()方法直接抛出UnsupportedOperationException异常,因为该方法只支持排他锁;写锁调用newCondition()方法成功创建一个Condition实例
  2. 读锁和写锁的tryLock()方法的实现总是“非公平”的,不管创建ReentrantReadWriteLock实例时选择的是“公平”还是“非公平”策略;lock()lockInterruptibly()tryLock(long time, TimeUnit unit)的实现遵从创建ReentrantReadWriteLock实例时选择的“公平/非公平”策略
  3. 线程T1获取写锁后,在不释放写锁的情形下能够再获取读锁;反之,线程T1获取读锁后,在不释放读锁的情形下不能够再获取写锁。可查阅“ReentrantReadWriteLock.ReadLock”和“ReentrantReadWriteLock.WriteLock”的lock()方法实现源码,另外有如下“代码4”实验代码
  4. 基于第3点,介绍“锁降级”和“锁升级”。锁降级:T1获取写锁,然后获取读锁,再释放掉写锁,完成“写锁 -> 读锁”的锁降级;不支持锁升级,因为获取读锁后不能再获取写锁
  5. 存在“写锁申请长时间饥饿”的情形,具体是在非公平锁语境中,当前读锁被持有,“不断新产生的读锁申请”总是抢占“排队队列头节点的写锁申请”,导致该“写锁申请”长时间得不到满足,解决方案是:针对上述情形,新产生的读锁申请不参与竞争,直接排队到末尾,详细可见“ReentrantReadWriteLock.NonfairSync”类的readerShouldBlock()方法源码

代码4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockUseExample {

ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

public static void main(String[] args) {
ReadWriteLockUseExample example = new ReadWriteLockUseExample();

example.lockWriteThenLockRead();

example.lockReadThenLockWrite();

}

public void lockReadThenLockWrite() {
rwLock.readLock().lock();
try {

System.out.println("C");

rwLock.writeLock().lock();

System.out.println("D");

rwLock.writeLock().unlock();
} finally {
rwLock.readLock().unlock();
}
}

public void lockWriteThenLockRead() {
rwLock.writeLock().lock();

try {
System.out.println("A");

rwLock.readLock().lock();

System.out.println("B");

rwLock.readLock().unlock();
} finally {
rwLock.writeLock().unlock();
}
}

}

结果如下(main线程在打印“C”字符后申请读锁失败挂起):

1
2
3
A
B
C

2.2.2、源码实现

ReentrantReadWriteLock内部有一个AQS继承体系如下:

1
2
3
4
5
6
7
        AQS
*
*
Sync
* *
* *
FairSync NonfairSync

ReentrantReadWriteLock内部有一个Lock继承体系如下:

1
2
3
4
        Lock
* *
* *
ReadLock WriteLock

ReentrantReadWriteLock类内有3个主要的实例成员变量:

  • Sync sync,在创建类实例对象时,可选择sync具体是一个“ReentrantReadWriteLock.FairSync”实例对象,还是一个“ReentrantReadWriteLock.NonfairSync”实例对象,默认是一个“ReentrantReadWriteLock.NonfairSync”实例对象,即“选择公平/非公平策略,默认是非公平策略”
  • ReentrantReadWriteLock.ReadLock readerLock,与writerLock共用sync,然后所有具体实现都通过转发给sync实现,sync中的state字段高16位分配给readerLock
  • ReentrantReadWriteLock.WriteLock writerLock,与readerLock共用sync,然后所有具体实现都通过转发给sync实现,sync中的state字段低16位分配给writerLock

根据源码推导ReentrantReadWriteLock.WriteLock锁的锁分类:

  • 根据《AQS》,我们知道,基于AQS实现的锁,天然就是“悲观锁”和“阻塞锁”
  • 如果sync具体是一个“ReentrantReadWriteLock.FairSync”实例对象,查看“ReentrantReadWriteLock.FairSync”源码,可知是“公平锁”,“可重入锁”和“排他锁”;如果sync具体是一个“ReentrantReadWriteLock.NonfairSync”实例对象,查看“ReentrantReadWriteLock.NonfairSync”源码,可知是“非公平锁”,“可重入锁”和“排他锁”

根据源码推导ReentrantReadWriteLock.ReadLock锁的锁分类:

  • 根据《AQS》,我们知道,基于AQS实现的锁,天然就是“悲观锁”和“阻塞锁”
  • 如果sync具体是一个“ReentrantReadWriteLock.FairSync”实例对象,查看“ReentrantReadWriteLock.FairSync”源码,可知是“公平锁”,“可重入锁”和“共享锁”;如果sync具体是一个“ReentrantReadWriteLock.NonfairSync”实例对象,查看“ReentrantReadWriteLock.NonfairSync”源码,可知是“非公平锁”,“可重入锁”和“共享锁”

我们知道synchronized锁存在一个happens-before规则——对一个锁的解锁,happens-before于随后对这个锁的加锁,那ReentrantReadWriteLock锁满足该happens-before规则吗?具体分为几种情况:

  • 对一个ReentrantReadWriteLock.WriteLock锁的解锁,happens-before于随后对“同一个写锁”或者“关联ReentrantReadWriteLock.ReadLock锁”的加锁
  • 对一个ReentrantReadWriteLock.ReadLock锁的解锁,happens-before于随后对“关联ReentrantReadWriteLock.WriteLock锁”的加锁
  • “对一个ReentrantReadWriteLock.ReadLock锁的解锁”与“随后对同一个读锁的加锁”,如果是在同一线程,则根据“程序顺序规则”该条happens-before规则,有“对一个ReentrantReadWriteLock.ReadLock锁的解锁,happens-before于随后对同一个读锁的加锁”;否则,两者没有必然的happens-before关系

具体证明可参见“2.1.2、源码实现”小节中对于ReentrantLock锁的happens-before规则证明。


参考文献

[1]《synchronized锁内部实现》中的“3.6、重量级锁”小节

您的支持将鼓励我继续分享!