Lock底层实现
Lock完全用Java写成,无关JVM实现。
总概
在java.util.concurrent.locks包中有很多Lock的实现类,如ReentrantLock、ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖java.util.concurrent.AbstractQueuedSynchronizer类,实现思路大同小异,下面主要讲ReentrantLock
ReentrantLock把所有Lock接口的操作都委派到Sync类上,该类继承了AbstractQueuedSynchronizer,多数需要覆盖的方法都是在Sync类实现的,而NonfairSync、FairSync只是在tryAcquire方法和lock方法上有所不同。
static abstract class Sync extends AbstractQueuedSynchronizer
Sync又有两个子类
static final class NonfairSync extends Sync {
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//默认是非公平锁,所以nonfairTryAcquire方法放在了Sync里面
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {...}
}
显然是为了支持公平锁与非公平锁
Reentrant.lock()的调用过程(非公平锁)
T4.gifAbstractQueuedSynchronizer中抽象了绝大多数Lock的功能,而只把tryAcquire方法延迟到子类中实现。tryAcquire方法的语义在于用具体子类判断请求线程是否可以获得锁,无论成功与否AbstractQueuedSynchronizer都将处理后面的流程。
加锁的实现
AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全部处于阻塞状态,线程的显式阻塞是通过调用LockSupport.park()完成,而LockSupport.park()则调用sun.misc.Unsafe.park()本地方法,再进一步,HotSpot在Linux中中通过调用pthread_mutex_lock函数把线程交给系统内核进行阻塞。
在代码的实现上,首先会有调用公平锁(或者非公平锁)的lock,内部调用acquire(1),再到AQS里面
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
当中的tryAcquire在下面列出,也就是尝试获取锁,如果失败,那么将其加入CLH队列中,看看addWaiter方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
这段代码中先构造了一个节点,接着获取当前同步队列中尾节点,如果尾节点不为空,尝试使用compareAndSetTail设置尾节点,如果成功则会直接返回,失败则调用enq方法不断尝试加入尾节点,失败的原因可能不只是并发,还有可能是空队列
private Node enq(final Node node) {
//死循环
for (;;) {
Node t = tail;
//如果是空队列
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
//设置尾节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
紧接着,返回了当前新建立的node节点,看回到acquire方法中,发现addWaiter外面还包了一层函数,没错,就是acquireQueued,这个方法的作用是把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞,但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞,直接返回,再进去看看
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//死循环,不过这个方法既然是阻塞那就肯定不会死循环了!!!
for (;;) {
final Node p = node.predecessor();
如果当前加入的节点前面一个节点是头结点,那么阻塞该节点前再次调用tryAcquire尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//不会死循环的原因就在这!
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
当中又出现了新的方法shouldParkAfterFailedAcquire,不过这次不仔细看了,这个方法主要检查Node节点的mode信息,mode信息是在第一次创建该节点的时候传入的,检查规则有3个:
- 规则1:如果前继的节点状态为SIGNAL,表明当前节点需要unpark,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将导致线程阻塞
- 规则2:如果前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,导致线程阻塞
- 规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与规则2同
当然了,如果parkAndCheckInterrupt()返回false,那么设置interrupted = true(意味着解锁了),之后又进入无限循环;在这里可以看到,并不是得到解锁的线程一定能获得锁,必须在这个死循环里调用tryAccquire重新竞争,因为锁是非公平的,有可能被新加入的线程获得,从而导致刚被唤醒的线程再次被阻塞
重入锁
需要解决两个问题:
- 线程再次获得锁
- 锁的最终释放(线程重复n次获得锁,随后在第n次释放该锁后,其他线程能够获得该锁)
处理方式:
在覆盖tryAcquire的时候(包含公平与非公平锁),加入再次获取同步状态处理逻辑——通过判断当前线程是否为获得锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//当前锁没被线程占有
if (c == 0) {
//hasQueuedPredecessors()是公平锁需要加入的
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;
}
那么在释放重入锁的时候,也是对同步状态值进行减少
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;
}
解锁
跟加锁思路差不多,不过更简单,先tryRelease再unparkSuccessor
公平锁与非公平锁的实现区别
公平锁表示:在锁的获取顺序就应该符合请求的绝对时间请求,也就是FIFO
跟非公平锁的方法相比,公平锁tryAcquire的实现唯一的不同就是判断条件多了个hasQueuedPredecessors(),即加入了同步队列中当前节点是否有前驱节点的判断,有则表示前面的节点应该比当前节点先获得锁
ReentrantLock默认使用非公平锁是基于性能考虑,公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销。如果直接插队获取非公平锁,跳过了对队列的处理,速度会更快。
读写锁
遵循获取写锁、获取读锁再释放写锁的次序,写锁能降级成为读锁
读写锁的自定义同步器需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。
LockSupport
LockSupport定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也称为构建同步组件的基础工具。
park阻塞当前线程、unpark唤醒一个被阻塞的线程