你真的了解ReentrantLock吗?公平锁和非公平锁获取和释放相同吗?
在介绍
ReentrantLock
的时候,首先我们要先了解锁的释放和获取,在java内存中究竟怎么处理的。
锁的释放和获取的内存语义
当线程释放锁时,java内存模型(以下简称JMM)会把线程对应的本地内存中的共享变量刷新到主内存中。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得监视器保护的临界区代码必须从主内存中读取共享变量。
对比锁释放-获取的内存语义与volatile
写-读的内存语义可以看出:锁释放与volatile写有相同的内存语义;锁获取与volatile
读有相同的内存语义。
总结:
- 线程A释放一个锁,实际是线程A向接下来将要获取这个锁(线程B)的某个线程发出了(线程A对共享变量修改的)消息;
- 线程B获取一个锁,实际是线程B接收了之前某个线程(线程A)发出的(在释放这个锁之前对共享变量所做修改的)消息;
- 线程A释放锁,线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
锁内存语义的实现
借助ReentrantLock源代码,来分析锁内存语义具体的实现机制。
public void test(){
int a = 0;
Lock lock = new ReentrantLock();
lock.lock();
try{
a++;
}finally {
lock.unlock();
}
}
在ReentrantLock
中,调用lock()
方法获取锁;调用unlock()
方法释放锁。
ReentrantLock
的实现依赖于Java同步器框架AbstractQueuedSynchronizer
(简称之为AQS,AQS使用一个整型的volatile
变量(命名为state
)来维护同步状态,volatile修饰的state保证了变量可见性,volatile
变量是ReentrantLock
内存语义实现的关键。
ReentrantLock
分为公平锁和非公平锁,FairSync
和NonfairSync
,同时继承自Sync
,我们先来说公平锁。
lock() 获取锁(公平)
//公平锁的同步对象
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
}
- 如果锁未被另一个线程持有,则获取该锁并立即返回,将锁持有计数设置为1。
- 如果当前线程已经持有锁,那么持有计数将增加1,并且方法立即返回。
- 如果锁被另一个线程持有,则当前线程将因线程调度而禁用,并处于休眠状态,直到获得锁为止,此时锁持有计数设置为1。
Sync继承AQS,而公平锁FairSync继承自Sync。
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* 公平锁的tryAcquire方法,真正的加锁
* 除非递归调用或没有服务生或是第一个,否则不要授予访问权限。
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取锁的开始,首先读通过volatile修饰的state变量
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;
}
同步状态
private volatile int state;
使用公平锁时,加锁方法lock()调用流程:
ReentrantLock:lock()
FairSync:lock()
AbstractQueuedSynchronizer:acquire(int arg)
ReentrantLock:tryAcquire(int acquires)
unlock()解锁
//释放锁
public class ReentrantLock implements Lock, java.io.Serializable{
public void unlock() {
sync.release(1);
}
}
- 如果当前线程是这个锁的持有者,那么计数递减。
- 如果当前计数为零,则释放锁。
- 如果当前线程不是此线程的持有者,则抛异常
IllegalMonitorStateException
//以独占的方式释放
//tryRelease()方法如果返回true,则通过解锁一个或多个线程来实现。
abstract class AbstractQueuedSynchronizer{
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
}
//释放锁
protected final boolean tryRelease(int releases) {
//volatile修饰state 减去需要释放的锁
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//等于0,说明没有需要同步的,锁释放成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//释放锁最后,写volatile变量state
setState(c);
return free;
}
在使用公平锁时,解锁方法unlock()
调用过程:
ReentrantLock:unlock()
AbstractQueuedSynchronizer:release(int arg)
-
Sync:tryRelease(int releases)
公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。
非公平锁的释放和公平锁是一样的,我们来看下非公平锁的获取。
lock() 获取锁(非公平)
static final class NonfairSync extends Sync {
final void lock() {
//通过CAS,期望值为0 ,更新值 1
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
如果当前状态值等于期望值,则以原子方式将同步状态设置为给定的更新值。
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
使用非公平锁时,加锁方法lock()调用流程
- ReentrantLock:lock()
- NonfairSync:lock()
- AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)
编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序。组合这两个条件,意味着为了同时实现volatile读和volatile写的内存语义,编译器不能对CAS与CAS前面和后面的任意内存操作重排序。
公平锁和非公平锁的区别
现在对公平锁和非公平锁做个总结。
- 公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
- 公平锁和非公平锁释放时,最后都要写一个volatile变量state。
- 公平锁获取时,首先会去读volatile变量。
- 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同。
- 公平性锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁会出现一个线程连续获取锁的情况。
- 公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。
公平锁tryAcquire
,非公平锁nonfairTryAcquire
,唯一不同的位置为判断条件多了hasQueuedPredecessors
()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
对ReentrantLock的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种
方式。
- 利用volatile变量的写-读所具有的内存语义。
- 利用CAS所附带的volatile读和volatile写的内存语义。
tryLock()
获取锁(如果有)并立即返回true。 如果锁不可用,则此方法将立即返回值false
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。
成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值,释放锁时tryRelease()方法,上边已经贴过源码
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。
参考书籍:[java并发编程的艺术]