ReentrantLock简介
ReentrantLock
是Java
在JDK1.5
引入的显式锁,在实现原理和功能上都和内置锁(synchronized)上都有区别,在文章最后我们再比较这两个锁。
首先我们要知道ReentrantLock
是基于AQS
实现的,所以我们得对AQS
有所了解才能更好的去学习掌握ReentrantLock
,关于AQS
的介绍可以参考我之前写的一篇文章《一文带你快速掌握AQS》,这里简单回顾下AQS
。
AQS回顾
AQS
即AbstractQueuedSynchronizer
的缩写,这个是个内部实现了两个队列的抽象类,分别是同步队列和条件队列。其中同步队列是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁,而条件队列是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾,AQS
所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作。
在同步队列中,还存在2
中模式,分别是独占模式和共享模式,这两种模式的区别就在于AQS
在唤醒线程节点的时候是不是传递唤醒,这两种模式分别对应独占锁和共享锁。AQS
是一个抽象类,所以不能直接实例化,当我们需要实现一个自定义锁的时候可以去继承AQS
然后重写获取锁的方式和释放锁的方式还有管理state,而ReentrantLock
就是通过重写了AQS
的tryAcquire
和tryRelease
方法实现的lock
和unlock
。
ReentrantLock原理
通过前面的回顾,是不是对ReentrantLock
有了一定的了解了,ReentrantLock
通过重写锁获取方式和锁释放方式这两个方法实现了公平锁和非公平锁,那么ReentrantLock
是怎么重写的呢,这也就是本节需要探讨的问题。
ReentrantLock结构
首先ReentrantLock
继承自父类Lock
,然后有3
个内部类,其中Sync
内部类继承自AQS
,另外的两个内部类继承自Sync
,这两个类分别是用来公平锁和非公平锁的。
通过Sync
重写的方法tryAcquire
、tryRelease
可以知道,ReentrantLock
实现的是AQS
的独占模式,也就是独占锁,这个锁是悲观锁。
ReentrantLock
有个重要的成员变量:1
private final Sync sync;
这个变量是用来指向Sync
的子类的,也就是FairSync
或者NonfairSync
,这个也就是多态的父类引用指向子类,具体Sycn
指向哪个子类,看构造方法:1
2
3
4
5
6
7public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock
有两个构造方法,无参构造方法默认是创建非公平锁,而传入true
为参数的构造方法创建的是公平锁。
非公平锁的实现原理
当我们使用无参构造方法构造的时候即ReentrantLock lock = new ReentrantLock()
,创建的就是非公平锁。1
2
3
4
5
6
7
8public ReentrantLock() {
sync = new NonfairSync();
}
//或者传入false参数 创建的也是非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
lock方法获取锁
lock
方法调用CAS
方法设置state
的值,如果state
等于期望值0
(代表锁没有被占用),那么就将state
更新为1
(代表该线程获取锁成功),然后执行setExclusiveOwnerThread
方法直接将该线程设置成锁的所有者。如果CAS
设置state
的值失败,即state
不等于0
,代表锁正在被占领着,则执行acquire(1)
,即下面的步骤。nonfairTryAcquire
方法首先调用getState
方法获取state
的值,如果state
的值为0
(之前占领锁的线程刚好释放了锁),那么用CAS
这是state
的值,设置成功则将该线程设置成锁的所有者,并且返回true
。如果state
的值不为0
,那就调用getExclusiveOwnerThread
方法查看占用锁的线程是不是自己,如果是的话那就直接将state + 1
,然后返回true
。如果state
不为0
且锁的所有者又不是自己,那就返回false
,然后线程会进入到同步队列中。
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
49final void lock() {
//CAS操作设置state的值
if (compareAndSetState(0, 1))
//设置成功 直接将锁的所有者设置为当前线程 流程结束
setExclusiveOwnerThread(Thread.currentThread());
else
//设置失败 则进行后续的加入同步队列准备
acquire(1);
}
public final void acquire(int arg) {
//调用子类重写的tryAcquire方法 如果tryAcquire方法返回false 那么线程就会进入同步队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//子类重写的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
//调用nonfairTryAcquire方法
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果状态state=0,即在这段时间内 锁的所有者把锁释放了 那么这里state就为0
if (c == 0) {
//使用CAS操作设置state的值
if (compareAndSetState(0, acquires)) {
//操作成功 则将锁的所有者设置成当前线程 且返回true,也就是当前线程不会进入同步
//队列。
setExclusiveOwnerThread(current);
return true;
}
}
//如果状态state不等于0,也就是有线程正在占用锁,那么先检查一下这个线程是不是自己
else if (current == getExclusiveOwnerThread()) {
//如果线程就是自己了,那么直接将state+1,返回true,不需要再获取锁 因为锁就在自己
//身上了。
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果state不等于0,且锁的所有者又不是自己,那么线程就会进入到同步队列。
return false;
}
tryRelease锁的释放
- 判断当前线程是不是锁的所有者,如果是则进行步骤
2
,如果不是则抛出异常。 - 判断此次释放锁后
state
的值是否为0,如果是则代表锁有没有重入,然后将锁的所有者设置成null
且返回true
,然后执行步骤3
,如果不是则代表锁发生了重入执行步骤4
。 - 现在锁已经释放完,即
state=0
,唤醒同步队列中的后继节点进行锁的获取。 - 锁还没有释放完,即
state!=0
,不唤醒同步队列。
1 | public void unlock() { |
公平锁的实现原理
lock方法获取锁
- 获取状态的
state
的值,如果state=0
即代表锁没有被其它线程占用(但是并不代表同步队列没有线程在等待),执行步骤2
。如果state!=0
则代表锁正在被其它线程占用,执行步骤3
。 - 判断同步队列是否存在线程(节点),如果不存在则直接将锁的所有者设置成当前线程,且更新状态state,然后返回true。
- 判断锁的所有者是不是当前线程,如果是则更新状态state的值,然后返回true,如果不是,那么返回false,即线程会被加入到同步队列中
通过步骤2
实现了锁获取的公平性,即锁的获取按照先来先得的顺序,后来的不能抢先获取锁,非公平锁和公平锁也正是通过这个区别来实现了锁的公平性。
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
37final void lock() {
acquire(1);
}
public final void acquire(int arg) {
//同步队列中有线程 且 锁的所有者不是当前线程那么将线程加入到同步队列的尾部,
//保证了公平性,也就是先来的线程先获得锁,后来的不能抢先获取。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//判断状态state是否等于0,等于0代表锁没有被占用,不等于0则代表锁被占用着。
if (c == 0) {
//调用hasQueuedPredecessors方法判断同步队列中是否有线程在等待,如果同步队列中没有
//线程在等待 则当前线程成为锁的所有者,如果同步队列中有线程在等待,则继续往下执行
//这个机制就是公平锁的机制,也就是先让先来的线程获取锁,后来的不能抢先获取。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//判断当前线程是否为锁的所有者,如果是,那么直接更新状态state,然后返回true。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果同步队列中有线程存在 且 锁的所有者不是当前线程,则返回false。
return false;
}
tryRelease锁的释放
公平锁的释放和非公平锁的释放一样,这里就不重复。
公平锁和非公平锁的公平性是在获取锁的时候体现出来的,释放的时候都是一样释放的。
lockInterruptibly可中断方式获取锁
ReentrantLock
相对于Synchronized
拥有一些更方便的特性,比如可以中断的方式去获取锁。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
//如果当前线程已经中断了,那么抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//如果当前线程仍然未成功获取锁,则调用doAcquireInterruptibly方法,这个方法和
//acquireQueued方法没什么区别,就是线程在等待状态的过程中,如果线程被中断,线程会
//抛出异常。
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
tryLock超时等待方式获取锁
ReentrantLock
除了能以能中断的方式去获取锁,还可以以超时等待的方式去获取锁,所谓超时等待就是线程如果在超时时间内没有获取到锁,那么就会返回false
,而不是一直”死循环”获取。
- 判断当前节点是否已经中断,已经被中断过则抛出异常,如果没有被中断过则尝试获取锁,获取失败则调用
doAcquireNanos
方法使用超时等待的方式获取锁。 - 将当前节点封装成独占模式的节点加入到同步队列的队尾中。
- 进入到”死循环”中,但是这个死循环是有个限制的,也就是当线程达到超时时间了仍未获得锁,那么就会返回
false
,结束循环。这里调用的是LockSupport.parkNanos
方法,在超时时间内没有被中断,那么线程会从超时等待状态转成了就绪状态,然后被CPU
调度继续执行循环,而这时候线程已经达到超时等到的时间,返回false。LockSuport
的方法能响应Thread.interrupt
,但是不会抛出异常
1 | public boolean tryLock(long timeout, TimeUnit unit) |
ReentrantLock的等待/通知机制
我们知道关键字Synchronized
+ Object
的wait
和notify
、notifyAll
方法能实现等待/通知机制,那么ReentrantLock
是否也能实现这样的等待/通知机制,答案是:可以。ReentrantLock
通过Condition
对象,也就是条件队列实现了和wait
、notify
、notifyAll
相同的语义。
线程执行condition.await()
方法,将节点1从同步队列转移到条件队列中。
线程执行condition.signal()
方法,将节点1从条件队列中转移到同步队列。
因为只有在同步队列中的线程才能去获取锁,所以通过Condition
对象的wait
和signal
方法能实现等待/通知机制。
代码示例: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
52ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void await() {
lock.lock();
try {
System.out.println("线程获取锁----" + Thread.currentThread().getName());
condition.await(); //调用await()方法 会释放锁,和Object.wait()效果一样。
System.out.println("线程被唤醒----" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("线程释放锁----" + Thread.currentThread().getName());
}
}
public void signal() {
try {
Thread.sleep(1000); //休眠1秒钟 等等一个线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try {
System.out.println("另外一个线程获取到锁----" + Thread.currentThread().getName());
condition.signal();
System.out.println("唤醒线程----" + Thread.currentThread().getName());
} finally {
lock.unlock();
System.out.println("另外一个线程释放锁----" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
Test t = new Test();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
t.await();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
t.signal();
}
});
t1.start();
t2.start();
}
运行输出:1
2
3
4
5
6线程获取锁----Thread-0
另外一个线程获取到锁----Thread-1
唤醒线程----Thread-1
另外一个线程释放锁----Thread-1
线程被唤醒----Thread-0
线程释放锁----Thread-0
执行的流程大概是这样,线程t1
先获取到锁,输出了”线程获取锁—-Thread-0”,然后线程t1
调用await
方法,调用这个方法的结果就是线程t1
释放了锁进入等待状态,等待唤醒,接下来线程t2
获取到锁,然输出了”另外一个线程获取到锁—-Thread-1”,同时线程t2
调用signal
方法,调用这个方法的结果就是唤醒一个在条件队列(Condition)的线程,然后线程t1
被唤醒,而这个时候线程t2
并没有释放锁,线程t1
也就没法获得锁,等线程t2
继续执行输出”唤醒线程—-Thread-1”之后线程t2
释放锁且输出”另外一个线程释放锁—-Thread-1”,这时候线程t1
获得锁,继续往下执行输出了线程被唤醒----Thread-0
,然后释放锁输出”线程释放锁—-Thread-0”。
如果想单独唤醒部分线程应该怎么做呢?这时就有必要使用多个Condition
对象了,因为ReentrantLock
支持创建多个Condition
对象,例如:1
2
3
4
5
6
7
8
9
10
11
12
13//为了减少篇幅 仅给出伪代码
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Condition condition1 = lock.newCondition();
//线程1 调用condition.await() 线程进入到条件队列
condition.await();
//线程2 调用condition1.await() 线程进入到条件队列
condition1.await();
//线程32 调用condition.signal() 仅唤醒调用condition中的线程,不会影响到调用condition1。
condition1.await();
这样就实现了部分唤醒的功能。
ReentrantLock和Synchronized对比
关于Synchronized
的介绍可以看《synchronized的使用(一)》、《深入分析synchronized原理和锁膨胀过程(二)》
ReentrantLock | Synchronized | |
---|---|---|
底层实现 | 通过AQS 实现 |
通过JVM 实现,其中synchronized 又有多个类型的锁,除了重量级锁是通过monitor 对象(操作系统mutex互斥原语)实现外,其它类型的通过对象头实现。 |
是否可重入 | 是 | 是 |
公平锁 | 是 | 否 |
非公平锁 | 是 | 是 |
锁的类型 | 悲观锁、显式锁 | 悲观锁、隐式锁(内置锁) |
是否支持中断 | 是 | 否 |
是否支持超时等待 | 是 | 否 |
是否自动获取/释放锁 | 否 | 是 |
参考
《Java并发编程的艺术》
深入理解AbstractQueuedSynchronizer(AQS)
Java 重入锁 ReentrantLock 原理分析)