前言
作为一个程序员,相信大家或多或少了解或听说过锁的概念。锁,在现实中,我们的锁是用来保护我们想要保护的东西的,比如说隐私、财产...。在程序中,锁是用来实现线程安全的(多线程操作共享数据,保证数据的准确)。作为Java程序员,相信大家见过最多的一个锁应该是synchronized(Java的一个关键字,这里笔者不对synchronized做讲解)。Java在1.5的版本,推出了一系列并发的东西,其中包含一个Lock接口,以及一个抽象类AbstractQueuedSynchronizer(AQS)。Lock下有一个实现类,名为ReentrantLock,笔者本次主要讨论ReentrantLock中的公平锁。
问题
在此,笔者抛出一个疑问,为什么Java已经有synchronized来保证线程安全的锁,为什么后面还推出ReentrantLock?
1.什么是ReentrantLock
ReentrantLock是一个可重入互斥锁,具有与使用synchronized
方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。
2.ReentrantLock的结构
3.FairSync分jie析
1.FairSync的加锁过程
以下是FairSync加锁一个大概的流程图解,建议有兴趣的去仔细阅读源码,有很多细节未在图上体现。
接下来上代码
1.首先调用acquire方法(加锁的入口方法)
//加锁的入口方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
2. 调用 tryAcquire(int acquires):尝试获取锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//hasQueuedPredecessors() 判断自己是否需要排队
//compareAndSetState 加锁,只有当队列中没有元素才会尝试加锁
// setExclusiveOwnerThread设置当前持有锁的线程
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;
}
------------------------------------以上hasQueuedPredecessors()方法的内部----------
public final boolean hasQueuedPredecessors() {
//t:获取AQS的队列尾
//h:获取AQS的队列头
//s:临时变量
Node t = tail;
Node h = head;
Node s;
//h != t:判断队列是否有等待的线程
//(s = h.next) == null :判断队列第二个元素是否存在
//s.thread != Thread.currentThread(): 第二个线程是否为当前线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
3.如果获取锁失败调用addWaiter:初始化(维护)队列
//mode=null
private Node addWaiter(Node mode) {
//创建一个Node: 设置当前线程,以及下一个服务员为null
/** Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;}
**/
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
---------------------------以上代码enq()方法的内部------------------------------------------
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//如果尾为null就初始化头、尾
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//如果队列已初始化,此线程的Node的上一个指向队尾,队尾的下一个指向此线程的Node
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
入队并可能进行睡眠(此方法里的细节能推测出本章开头的问题)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
//Theard.interrup才会有作用
boolean interrupted = false;
for (;;) {
//获取改线程节点的上一个节点
final Node p = node.predecessor();
//如果上一个节点是对头,则尝试获取一次锁(只有第一个排队的才有资格去获取锁)
if (p == head && tryAcquire(arg)) {
//拿到锁后,重置头节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//shouldParkAfterFailedAcquire:睡眠前将前一个的waitStatus标识改为-1,方便解锁
//parkAndCheckInterrupt睡眠
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
2.FairSync的解锁过程
代码演示
1.调用release(解锁的入口方法)
//解锁的入口方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
2.调用tryRelease尝试释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//当前线程不是持有锁的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//free锁自由标识
boolean free = false;
if (c == 0) {
free = true;
//清除当前持有锁的线程
setExclusiveOwnerThread(null);
}
//可重入锁的释放
setState(c);
return free;
}
3.假设队列中有等待的线程,调用unparkSuccessor唤醒后面等待的线程,并重置头节点的waitStatus状态为0
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒下一个睡眠的线程
LockSupport.unpark(s.thread);
}
4.问题解答
为什么Java已经有synchronized来保证线程安全的锁,为什么后面还推出ReentrantLock?
在多线程并发时,所有的线程在某些特定情况下并不是竞争执行,而是顺序执行。
举个例子,现在有个 T1,T2两个线程,假设T1比T2先执行,大部分情况下T2是在T1执行完的情况下再去执行的,所以这里没有对锁的争夺。而synchronized(1.6以前的版本)只要线程一进来就直接加锁,而这个关键字则会调用的底层的操作系统来进行加锁,这里会有资源的开销(用户态->内核态),而ReentrantLock在Java层面就将这类情况解决了。
ReentrantLock在让线程睡眠时,会让线程再次(一共2次)尝试获取锁,笔者从这可以推测,Doug Lea尽可能不想让线程去睡眠。
当然,在一些场景下,比如线程竞争执行的情况特别多,ReentrantLock相较于synchronized(1.6以前的版本)的性能又低了很多。
至此公平锁讲解完毕,由于笔者文字功底不是很扎实,所以有些细节没有体现,本文的初衷是希望读者能大概了解ReentrantLock中的一些过程,并强烈要求自己去亲自阅读源码,感受一下Doug Lea的魅力。