1.lock实现
public void lock() { sync.lock();}
我们都知道reentrantlock分为非公平锁和公平锁,通过new ReentrantLock(true);可以变成公平锁
我们这里分析一下jdk1.8时候的lock的实现
公平锁机制下lock调用的源码如下:
final void lock() { acquire(1);}
流程图: www.processon.com/view/link/5e3102efe4b05b335ff6b35d
public final void acquire(int arg) {
// 尝试获得锁
if (!tryAcquire(arg) &&
// 获得锁失败则将当前线程变成Node节点add进等待队列
// Node.EXCLUSIVE互斥模式、Node.SHARED共享模式
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里先看一下tryAcquire的实现
protected final boolean tryAcquire(int acquires) {
// 获得当前线程
final Thread current = Thread.currentThread();
// 取出锁状态
int c = getState();
// 为0代表还无线程占用
if (c == 0) {
// hasQueuedPredecessors判断等待队列是否初始化,简单来说就是判断当前线程是否是队列中的第一个线程
if (!hasQueuedPredecessors() &&
// CAS,将状态+1
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源码:
case 1:当前等待队列未初始化,如果为未初始化则head和tail都为null,那么h != t肯定不成立
case 2:当前等待队列中等于1,则也直接h != t不成立
case 3:当前等待队列中大于1,则&&后的判断,先判断是否还有下一个节点,然后判断当前线程是否和队列中第一个排队的节点的thread相等,不相等则代表有比当前线程更早的获取锁的线程在等待,因为公平锁需要先来后到的执行。返回true,不会进入if。
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
我们在看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 中addWaiter的实现
addWaiter将自己入队,看源码实现:
private Node addWaiter(Node mode) {
// 将当前线程以指定的模式创建节点node
Node node = new Node(Thread.currentThread(), mode);
// 获取当前等待队列的尾节点
Node pred = tail;
// 队列不为空,将新的node加入等待队列中
if (pred != null) {
node.prev = pred;
// CAS方式将当前节点尾插入队列中
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 当队列为empty或者CAS失败时会调用enq方法处理
enq(node);
return node;
}
其中,队列为empty,使用enq(node)处理,将当前节点插入等待队列,如果队列为空,则初始化当前队列。所有操作都是CAS自旋的方式进行,直到成功加入队尾为止。
private Node enq(final Node node) {
// 不断自旋
for (;;) {
Node t = tail;
// 当前队列为empty
if (t == null) {
// 完成队列初始化操作,头结点中不放数据,只是作为起始标记,lazy-load,在第一次用的时候new
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// 不断将当前节点使用CAS尾插入队列中直到成功为止
// compareAndSetTail(t, node) 判断尾部节点是不是t,是的话就将尾部指向node
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued
用于已在队列中的线程以独占且不间断模式获取state状态,直到获取锁后返回。主要流程:
- 结点node进入队列尾部后,检查状态;
- 调用park()进入waiting状态,等待unpark()或interrupt()唤醒;
- 被唤醒后,是否获取到锁。如果获取到,head指向当前结点,并返回从入队到获取锁的整个过程中是否被中断过;如果没获取到,继续流程1
Lock类对于锁的实现不会令线程进入阻塞状态,Lock底层调用LockSupport.park()方法,使线程进入的是等待状态。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))的实现:
final boolean acquireQueued(final Node node, int arg) {
// 是否已获取锁的标志,默认为true 即为尚未
boolean failed = true;
try {
// 等待中是否被中断过的标记
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翻译过来“在获取锁失败之后应该等待”
// shouldParkAfterFailedAcquire根据对当前节点的前一个节点的状态进行判断,对当前节点做出不同的操作
// parkAndCheckInterrupt让线程进入等待状态,并检查当前线程是否被可以被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 将当前节点设置为取消状态(Node.CANCELLED),为1
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// waitStatus默认为0
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 使用CAS将前一个节点状态由 INITIAL 设置成 SIGNAL(这里修改之后会自旋再次尝试获得锁)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
shouldParkAfterFailedAcquire
方法
-
主要逻辑是使用
compareAndSetWaitStatus(pred, ws, Node.SIGNAL)
使用CAS将前一个节点状态由 INITIAL 设置成 SIGNAL,表示当前线程。为什么要设置前一个线程的状态为SIGNAL而不是自己呢?
打个比方,你自己在房间睡着的时候知道自己睡着了吗?当然是不知道,那么你睡着了能自己关门嘛?不能,那你关了门万一没睡呢?所以就需要别人来帮你把门关上。
你也可以理解为修改状态的操作和使线程睡眠的操作不具有原子性,可能出现修改完状态之后却没睡着的情况。
- shouldParkAfterFailedAcquire方法会在死循环中反复重试,直至compareAndSetWaitStatus 设置节点状态位为 SIGNAL 时 shouldParkAfterFailedAcquire 返回 true 时才会执行方法 parkAndCheckInterrupt 方法。
parkAndCheckInterrupt
该方法的关键是会调用 LookSupport.park 方法,该方法是用来当前线程。
waitStatus
的各个值都是什么意思:
静态变量 | 值 | 描述 |
---|---|---|
Node.CANCELLED |
1 |
节点对应的线程已经被取消了(我们后边详细会说线程如何被取消) |
Node.SIGNAL |
-1 |
表示后边的节点对应的线程处于等待状态 |
Node.CONDITION |
-2 |
表示节点在等待队列中(稍后会详细说什么是等待队列) |
Node.PROPAGATE |
-3 |
表示下一次共享式同步状态获取将被无条件的传播下去(稍后再说共享式同步状态的获取与释放时详细唠叨) |
无 | 0 |
初始状态 |
2.interrupt(),interrupted() 和 isInterrupted() 的区别
interrupt():将调用该方法的对象所表示的线程标记一个停止标记,并不是真的停止该线程。interrupted():获取当前线程的中断状态,并且会清除线程的状态标记。是一个是静态方法。
isInterrupted():获取调用该方法的对象所表示的线程的中断状态,不会清除线程的状态标记。是一个实例方法。
简单的中断案例:t1先执行,但是sleep不释放锁资源,在这期间t2等候两秒钟还没拿到锁就中断
public class Test {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
testsync();
},"t1");
Thread t2 = new Thread(() -> {
testsync();
},"t2");
t1.start();
TimeUnit.SECONDS.sleep(2);
t2.start();
TimeUnit.SECONDS.sleep(2);
System.out.println("main");
/**
* 如果t2两秒钟还拿不到就中断
*/
t2.interrupt();
}
public static void testsync(){
try {
/**
* lockInterruptibly 和 lock的区别,前者会直接抛出异常可以响应中断,后者则不可以
*/
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
lock和lockInterruptibly的区别就在如下:
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
重点在doAcquireInterruptibly方法
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 区别在这
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
同acquireQueued() 基本相同,唯一的区别是对中断信号的处理。
acquireQueued() 被中断后,将中断标志传给外界,外界再调用Thread的interrupt() 复现中断;而doAcquireInterruptibly() 则直接抛出InterruptedException。
二者本质上没什么不同。但doAcquireInterruptibly()显示抛出了InterruptedException,调用者必须处理或继续上抛该异常。
3.unlock实现
这里我们看release的实现,sync是reentrantlock的一个内部抽象类,继承了AbstractQueuedSynchronizer
reentrantlock的公平锁FairSync和非公平锁NonfairSync都实现了sync
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 尝试释放锁
if (tryRelease(arg)) {
Node h = head;
// 当waitStatus不为0时,它会唤醒等待队列里的其他线程来获取资源。
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
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);
}
// 设置可重入次数为原始值0
setState(c);
return free;
}
我们继续看unparkSuccessor():
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果waitStatus为-1,即表示后边的节点对应的线程处于等待状态
if (ws < 0)
// 将waitStatus改为0
compareAndSetWaitStatus(node, ws, 0);
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);
}
这里当unpark唤醒下一个线程的时候,我们之前阻塞的线程就会在acquireQueued这个地方被唤醒,且继续执行
// parkAndCheckInterrupt 这里被唤醒后会获取当前线程的中断状态,并且会清除线程的状态标记。
// 如果没有被中断就if不成立,重新进行for循环
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
for (;;) {
// 获取当前节点的前节点
final Node p = node.predecessor();
// 如果前节点是头结点,则意味着自己是第一个排队的节点,尝试获取锁
if (p == head && tryAcquire(arg)) {
// 成功则将当前节点设置为头结点
setHead(node);
p.next = null; // help GC
failed = false;
// 最后返回false
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}