前面两篇博客分别介绍了通过synchronized关键字(内置锁机制)和volatile关键字实现同步机制。由于volatile实现的同步不能保证操作的原子性,因此一般常用内置锁实现同步机制,但java5.0版本的内置锁在功能上有很多缺陷:如无法中断一个正在等待获取锁的线程、无法在请求获取一个锁时无限地等待下去等,基于这些原因,ReentrantLock孕育而生。
1.ReentrantLock如何实现同步机制?
ReentrantLock类实现了Lock接口,接口把加锁操作抽象为一组方法。
public interface Lock{
void lock(); // 获得锁,如果锁被占用,则被阻塞
// 获得锁,如果锁被占用,则可以中断被阻塞的锁
void lockInterruptibly() throws InterruptedException;
boolean tryLock(); // 尝试获得锁,如果成功则为true,否则为false
// 在time时间内尝试获得锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock(); // 释放锁
Condition newCondition();
}
从接口的方法可知,不单单提供简单的获得锁和释放锁的操作,还提供其他一些功能,这也是内置锁所不具备的特点。ReentrantLock类完全实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。
要理解ReentrantLock类实现同步的原理,就需要看一下源码的实现。
1.1ReentrantLock类的构造函数有两种方式:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock不带参数的构造方法生成一个(非公平的)sync对象;
ReentrantLock带参数构造方法根据传入的true或者false分别构造公平的、不公平的sync对象。
NonfairSync类和FairSync类都继承自抽象类Sync,它们都实现父类Sync的抽象方法:lock()和tryAcquire()。这里的lock()实现则是ReentrantLock对象实际调用的方法。
1.2 ReentrantLock类的lock(),即获得锁的过程:
- NonfairSync类的lock()方法
final void lock() {
// 如果锁未被占有则设置锁的状态为1,并把当前线程置为锁的拥有者
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
compareAndSetState()是抽象类AbstractQueuedSynchronizer类的方法,通过unsafe对象的本地方法compareAndSwapInt()来实现(比较并交换操作即CAS操作);AQS类内部有一个state的全局变量,该变量在ReentrantLock类中的含义是:锁获取操作的次数
。因此compareAndSetState(0,1)的意思是:如果state值与方法的第一个参数相同,则设置state值为方法的第二个参数,这里表示state值是否为0,若为0则设置为1并返回true,表示该线程获得锁,否则返回false
。返回true则执行方法setExclusiveOwnerThread(thread)把获得锁的线程设置为锁的拥有者。返回false则执行acquire(1)。即获取锁失败后的操作:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法可以分三步来理解:
第一步:通过tryAcquire(arg)尝试获取锁,该方法是NonfairSync类从父类Sync类继承过来并自己实现的方法。【它和lock()方法组成了NonfairSync类】tryAcquire(arg)方法实则调用Sync类的nonfairTryAcquire(arg)方法:
final boolean nonfairTryAcquire(int acquires) {
// 获取当前请求锁的线程
final Thread current = Thread.currentThread();
// 获取当前锁的状态 state变量是AQS的全局变量
int c = getState();
// 若当前锁状态为0 表明锁处于空闲状态 则
if (c == 0) {
// 则通过CAS操作设置锁状态为acquires参数的值(默认传进来值为1) 表明锁已经被占有
if (compareAndSetState(0, acquires)) {
// 锁状态设置成功后 并把当前线程置为锁的拥有者
setExclusiveOwnerThread(current);
return true;
}
}
// 若锁状态不为0 表明锁已经被占有,则判断当前线程是否是该锁的拥有者,若不是则返回false,否则返回true 表明同一个线程请求锁 即重入性
else if (current == getExclusiveOwnerThread()) {
// state状态值加1 表明锁被重复请求,释放时需要多次释放
int nextc = c + acquires;
// 若锁状态小于0,则抛异常
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
如果当前锁未被占有,或者请求的线程是锁的拥有者线程(重入),则返回true;否则,返回false;
如果是重入则tryAcquire()返回true,acqure()方法直接结束;
如果不是重入,则tryAcquire()返回false,继续执行acquireQueued(addWaiter(Node.EXCLUSIVE),arg)。
第二步:既然无法获得锁,又不是重入,表明该线程要被阻塞,放入获取锁的等待队列中,通过调用addWaiter(Node mode)方法放入队列中:
private Node addWaiter(Node mode) {
// 把当前线程构造成一个node节点 参数mode表示Node节点类型
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 若tail节点不为空 tail节点为尾节点 enq方法的head节点为头结点
if (pred != null) {
// 则把tail节点设置为node节点的前节点
node.prev = pred;
// 设置node节点为tail节点 node节点的下节点则为tail节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 若tail节点为空,则执行enq
enq(node);
return node;
}
enq():
private Node enq(final Node node) {
// 内旋循环
for (;;) {
Node t = tail;
// 若tail节点为空,则把node节点初始化为head节点
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
// head节点同时设置为tail节点
tail = head;
} else {
// 若tail节点不为空,则把tail节点设置为node节点的前节点
node.prev = t;
// 设置node节点为tail节点 并把tail节点的下一节点指针指向node
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter(Node. EXCLUSIVE)方法返回含有当前线程的node节点;
第三步:通过acquireQueued()方法阻塞请求锁失败后的线程:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取node节点的前驱节点p
final Node p = node.predecessor();
// 如果前驱节点p为head节点,则node节点的线程尝试获取锁
if (p == head && tryAcquire(arg)) {
// 获取成功则把node节点设置为head节点,前驱节点p的后节点断开,便于gc
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 若前驱节点状态为SIGNAL,即为-1,则返回true
if (ws == Node.SIGNAL)
return true;
// 如果前驱节点状态大于0,即前驱节点被取消了,且去掉被取消的前驱结点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果前驱节点不大于0,则通过CAS设置前驱节点状态为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果前驱节点状态为SIGNAL,则返回true,并执行parkAndCheckInterrupt();其他状态返回false,继续循环直到返回true;
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
最后返回Thread类的interrupted()方法,即线程被阻塞。
至此线程获取锁成功和失败后的流程分析完毕。总结一句话就是:某线程请求锁,如果锁没有被占有或者线程是锁的所有者则可以获得锁,即请求成功;否则请求锁的线程通过步骤二放入等待队列,通过步骤三实现线程阻塞,即请求失败。
- FairSync类的lock()方法:
final void lock() {
acquire(1);
}
直接调用acquire(1)方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
获取锁同样有三个步骤:
第一步:通过tryAcquire(1)重试获取锁;
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
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;
}
在非公平锁第一步中,当锁的状态为0时,请求的线程直接获取锁;在公平锁中请求线程是否获得锁需要通过hasQueuedPredeceddors()方法进行判断:
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
结果1:若等待队列中head节点等于tail节点,即空队列,则返回fasle,从而请求线程获得锁
;
结果2:若head节点的后驱节点不为空,且后驱节点的线程等于当前请求锁的线程,即重入性,则返回false,从而请求线程获得锁
;
结果3:其他情况返回true,线程无法获得锁
。’
第二步和第三步与非公平锁调用的方法相同,都是AQS类的方法。
公平锁的lock()和非公平锁的lock()的区别在于:请求的线程是否是直接获取锁。
1.3 ReentrantLock的unlock()过程,即释放锁
public void unlock() {
sync.release(1);
}
直接调用sync类的release(1)方法:
public final boolean release(int arg) {
// tryRelease方法返回true则释放锁,否则不释放锁
if (tryRelease(arg)) {
Node h = head;
// 若head节点不为空 且head节点的状态值不为0,则
if (h != null && h.waitStatus != 0)
// 唤醒下一个node
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease()的返回值决定是否释放锁:
protected final boolean tryRelease(int releases) {
// 获取当前锁的状态值,并减去1
int c = getState() - releases;
// 若当前线程不是该锁的所有者则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 若锁的状态值为0,则表明锁可以被释放,并清空锁的所有者
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
主要根据锁的状态值来判断:
>若为0则可以释放并清空锁的所有者,并唤醒下一个等待的线程;
>若不为0则不释放锁,且修改当前锁的状态值。
注:更多关于AQS的内容可以参考博客(https://blog.csdn.net/pfnie/article/details/53191892)
ReentrantLock类是否可以替代Synchronized?
答案当然是不可以。ReentrantLock加锁和释放锁都是显示的,所以如果在加锁后的代码中发生异常,若没有正确的释放锁则导致该锁永远得不到释放,并且无法查找最初发生错误的位置。这一个巨大的隐患使得ReentrantLock永远无法替代Synchronized。
ReentrantLock性能 VS Synchronized性能?
java5中的ReentrantLock比内置锁提供更好的竞争性能;java6改进了内置锁的算法,使得两者性能差不多。使用只要考虑适合的场景即可,即内置锁无法满足需求时,才会考虑使用ReentrantLock。
总结
本篇博文首先分析了为什么会有ReentrantLock类,然后从源码的角度解释ReentrantLock加锁和释放锁的原理,从公平锁和非公平锁的角度分别阐述;最后简单的删除ReentrantLock和synchronized的性能区别。
注:源码分析过程中,有些术语可能不太好理解,具体可参考下面两篇博客:
https://blog.csdn.net/pfnie/article/details/53191892
https://blog.csdn.net/linxdcn/article/details/72844011
个人感觉这两篇博客都很清晰,从实际例子的角度阐述了代码的执行过程。
参考书籍:
《java并发编程实战》
《实战java高并发程序设计》