Lock (Synchronized)
在Lock接口出现之前,Java中的应用程序对于多线程的并发安全处理只能基于synchronized关键字来解决。但是synchronized在有些场景中会存在一些短板,也就是它并不适合于所有的并发场景。但是在
Java5以后,Lock的出现可以解决synchronized在某些场景中的短板,它比synchronized更加灵活。
ReentrantLock(重入锁)
重入锁:自己可以再次获取自己的内部的锁。比如有线程A获得了某对象的锁,此时这个时候锁还没有释放,当其再次想获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
Reentrantlock 提供了公平锁(FairSync)和非公平锁(NonFairSync)两种实现,我们最常用的就是非公平锁的实现(默认)
源码
先看非公平锁
lock.lock()
由**lock.lock()**进入源码
final void lock() {
//抢占互斥资源,有多个线程进入到这段代码?多个线程抢占到同一把锁.所以cas为解决这一问题
if (compareAndSetState(0, 1))//乐观锁( true / false) | 只有一个线程能够进入.
//能够进入到这个方法 , 表示无锁状态
setExclusiveOwnerThread(Thread.currentThread());//保存当前的线程,设置当前线程为持有的线程
else
//cas设置stat为1失败,代表获取的资源失败,执行AQS获取锁的模板流程,否则获取流程成功
acquire(1);
}
进入方法后,cas判断是否有锁,有则保存当前的线程,设置当前线程为持有的线程,失败则进入acquire(1)的方法。
acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
然后我们一个一个的去理解!tryAcquire(arg)、 acquireQueued(addWaiter(Node.EXCLUSIVE)、 selfInterrupt()意义
tryAcquire(arg)
tryAcquire(arg):首先A Q S的acquire函数是获取锁的流程模板,模板流程会先执行tryAcquire函数获取资源,tryAcquire函数要子类实现,NonfairSync作为子类,实现了tryAcquire函数,具体实现是调用了Sync的nonfairTryAcquire函数。
接下来,我们再看看Sync
专门给NonfairSync
准备的nonfairTryAcquire
函数逻辑
final boolean nonfairTryAcquire(int acquires) {
//获取当前的线程
final Thread current = Thread.currentThread();
//获取当前状态
int c = getState();
if (c == 0) {//无锁,state==0,代表资源可获取
//cas设置state为acquires,acquires传入的值为1
if (compareAndSetState(0, acquires)) {
//cas成功,设置当前为持有锁的线程
setExclusiveOwnerThread(current);
//返回成功
return true;
}
}
//如果state!=0,但是当前的线程是持有锁的线程,直接重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;//state+1
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置state状态,这里不需要cas,因为持有锁的线程只有一个
setState(nextc);
return true;
}
return false;
}
可见当前的方法就是当前线程查看资源是否可获取:
- 可获取,尝试使用C A S设置state为1,C A S成功代表获取资源成功,否则获取资源失败
- 不可获取,判断当线程是不是持有锁的线程,如果是,state重入计数,获取资源成功,否则获取资源失败
acquireQueued(addWaiter(Node.EXCLUSIVE)
addWaiter 将未获得锁的线程加入到队列
acquireQueued(); 去抢占锁或者阻塞.
再回到acquire的方法,当tryAcquire方法获取锁失败以后,则会先调用addWaiter将当前线程封装成Node.
入参mode表示当前节点的状态,传递的参数是Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了AQS的独占锁功能
-
将当前线程封装成Node
-
当前链表中的tail节点是否为空,如果不为空,则通过cas操作把当前线程的node添加到AQS队列
-
如果为空或者cas失败,调用enq将节点添加到AQS队列
private Node addWaiter(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是AQS中表示同比队列队尾的属性,默认
if (pred != null) {//tail不为空的情况下,说明队列中存在节点
node.prev = pred;//把当前线程的Node的prev指向tail
if (compareAndSetTail(pred, node)) {//通过cas把node加入到AQS队列,也就是设置为tail
pred.next = node;
return node;
}
}
//尾节点不为空加入队列
enq(node);
return node;
}
//该函数会初始化队列(如果队列未被初始化)
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize 初始化队列
if (compareAndSetHead(new Node()))//将head设置为一个空node。这个空node很重要,aqs的队头一定是空节点,用来表示正在执行的那个线程,想一下当执行线程结束后只有这个空节点才能去唤醒下一个节点,假如队头节点就是等待线程,谁能去唤醒他呢
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {//cas队尾,如果还失败看到这个是死循环会一直去放,直到放到队尾为止
t.next = node;
return t;
}
}
}
}
了解了addWaiter后,再看addWaiter(Node.EXCLUSIVE)传入的参数到了acquire里面做了那些操作
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();//获得该node的前置节点
/**
* 如果前置节点是head,表示之前的节点就是正在运行的线程,表示是第一个排队的
(一般讲队列中第一个是正在处理的,可以想象买票的过程,第一个人是正在买票(处理中),第二个才是真正排队的人);
那么再去tryAcquire尝试获取锁,如果获取成功,说明此时前置线程已经运行结束,则将head设置为当前节点返回 **/
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC,将前置节点移出队列,这样就没有指针指向它,可以被gc回收
failed = false;
return interrupted; //返回false表示不能被打断,意思是没有被挂起,也就是获得到了锁
}
/**shouldParkAfterFailedAcquire将前置node设置为需要被挂起,
注意这里的waitStatus是针对当前节点来说的,
即是前置node的ws指的是下一个节点的状态**/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //挂起线程 park()
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);//如果失败取消尝试获取锁(从上面的代码看只有进入p == head && tryAcquire(arg)这个逻辑是才会触发,这个时候前置节点正好在当前节点入队的时候执行完,当前节点正好获得锁,具体的代码以后分析)
}
}
以上是lock.lock()方法里执行的操作,当lock.unlock执行又发生什么呢,接下来看:
lock.unlock
release
讲了如何获取到资源,接下来就应该如何释放资源.这个方法会在独占的模式下释放指定的资源(减小state).这个语义也是reentrantLock.unlock();
public final boolean release(int arg) {
if (tryRelease(arg)) {//尝试释放资源
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); //唤醒队列的下一个节点
return true;
}
return false;
}
tryRelease():释放指定量的资源,这个方法是在子类中实现的.我们以reentrantLock.unlock()为例解读资源释放的过程
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 获取 AQS 的 state,并计算 c=state-释放资源数
// 判断当前线程是否是拿锁线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //独占锁模式下,state为0时表示没有线程获取锁,这时才算是当前线程完全释放锁
free = true;
// 将AQS保存的拿锁线程置为null
setExclusiveOwnerThread(null);
}
// 重置state
setState(c);
return free;
}
回到releadse看看如何唤醒后继的节点
unparkSuccessor(Node node)
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;//获得head节点的状态
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);//设置head节点状态为0
Node s = node.next;//得到head节点的下一个节点
if (s == null || s.waitStatus > 0) {//如果下一个节点为null或者status>0表示cancelled状态.
//通过从尾部节点开始扫描,找到距离head最近的一个waitStatus<=0的节点
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)//next节点不为空,直接唤醒这个线程即可
LockSupport.unpark(s.thread);
}
附个整个流程的图如下