说明

  1. 本文中采用的 jdk 版本为 openjdk-1.8

锁和线程状修改

  1. 加锁的本质?主要是为了在访问临界资源的时候,能够实现一个等待唤醒得有序操作。
  2. Java 中的锁的分类:​​sychronized​​​ 和​​Lock​​​ 。在​​sychronized​​​ 中主要是通过​​monitor​​​ 来进行实现的。通过​​Object.wart/notify​​​ 实现线程得阻塞和唤醒; 第二种就是基于线程的​​LockSupport.park/unpack​​ 阻塞和唤醒。
  3. 线程的中断问题,如何优雅的中断一个线程?
  • Java中断机制是一种协作机制,也就是说中断并不能直接终止某一个线程,而需要被中断的线程自己处理中断
  • API 的使用:
    interrupt(): 将线程的中断标示位设置为 true;
    isInterrupted(): 判断当前线程的中断标志位是否是 true;
    Thread.interrupted(): 判断当前线程中断位置是否位 true, 并且清除中断标志位,重置为 fasle。
  1. LockSupport 会造成线程中断吗? LockSupport 不会造成线程中断的。
  2. CAS 是什么?比较交换, 主要是一个乐观锁的概念, 底层采用的是 unsafe api 来实现比较交换。
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS 的原理和实现

AQS 是基于队列在实现排队 ​​AbstractQueuedSynchronizer​​ 类为模板方法的实现。

  1. 底层数据结构是一个双向链表。每个 Node 节点包含 prev,和 next 指针,以及数据数据字段,这里的数据字段用就是一个线程 Thread 对象。
  2. Node 的四种状态: 取消,等待,条件等待,共享状态
  3. 通常的两种实现:公平锁,非公平锁。 怎么提现公平指的是同一个时刻新加入的数据,和队列头的数据竞争是否能够进行公平的资源竞争。
  4. 状态的修改通过 CAS 来实现,底层是调用 sun.misc.Unsafe 的 ​​compareAndSwapInt​​ 进行状态的修改。
  5. 在线程进入队列之前会进行尝试加锁,如果拿不到锁会阻塞当前线程并且线程通过 ​​LockSupport.park()​​ 进入阻塞。
  6. 释放锁的时候,就会去队列中拿队列头的节点,进行唤醒,​​同步队列的头节点 head 就是当前获取锁的线程​​。
  7. 下面是获取锁中 AQS 的核心代码
    a. 尝试加锁,如果加锁不成功,就进入队列进行重试。
    ​ // 独占的方式获取锁, 可以忽略中断, 最少调用一次,如果失败会进行排队,直到成功 public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } ​​b. 队列中,先去判断是否是队列头,如果是去尝试加锁,如果不是就对 Node 的状态进行修改,修改为等待唤醒。状态修改成功后把线程状态修改为阻塞。
    ​ final boolean acquireQueued(final Node node, int arg) { 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; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } ​​下面是 shouldParkAfterFailedAcquire 方法对 Node 的状态进行维护。
    ​private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; // 前继节点的状态, 第一次进入默认值 0 if (ws == Node.SIGNAL) return true; if (ws > 0) { do { // 出队 node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { // 第一次进来, pred.waitStatus = 0 执行这个分支 // 将前继节点的状态修改为 SIGNAL, 表示 pred.next 节点需要被唤醒(此时准备进入阻塞, 但是还未被阻塞, 再次获取锁失败之后才会被阻塞) compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } ​

AQS 流程图

AQS 原理和 ReentrantLock 源码_数据

ASQ 特征

  1. 阻塞等待队列
  2. 共享独占
  3. 公平/非公平
  4. 可重入
  5. 允许中断

ReentrantLock

一个简单的 Demo

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

static ReentrantLock lock = new ReentrantLock();

static class T extends Thread {
@Override
public void run() {
try {
System.out.println(Thread.currentThread() + "开始尝试获取锁");
if (lock.tryLock(10, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread() + "成功获取锁");
TimeUnit.SECONDS.sleep(5);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread() + "开始释放锁");
lock.unlock();
}
}
}

public static void main(String[] args) {
T t1 = new T();
T t2 = new T();
T t3 = new T();
t1.start();
t2.start();
t3.start();
}
}

加锁和解锁过程图解

AQS 原理和 ReentrantLock 源码_数据_02

加锁和解锁过程描述

  1. 在上面的程序中有三个线程同时去获取锁,同一个时刻只能又一个线程获取到锁,下面是进行入队去尝试获取锁的逻辑:
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE); // 入队
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { // 如果是头节点并且获取锁成功
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 获取锁超过最大等待时间
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
// 获取锁失败进入阻塞 并且 并且超过自旋等待时间
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
// 进入阻塞 nanosTimeout 为阻塞时间
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
  1. 非公平锁尝试加锁的逻辑, 如果没有线程持有锁,那么就去通过 CAS 尝试加锁,如果是当前线程持有锁那么就 state + 1 累计,这里也可以看出​​ReentrantLock​​ 支持重入。
// 非公平锁的逻辑
// 如何理解插队, 这里的插队是当前队列中被唤醒的线程, 和当前加入的线程都可以被执行
// 如果当前加入线程比队列中唤醒的线程先获取到锁, 就是插队现象
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 无锁状态, 尝试竞争
if (c == 0) {
if (compareAndSetState(0, acquires)) { //是否获取到锁
setExclusiveOwnerThread(current);
return true;
}
}
// 当前线程持有锁, state 计数 +1
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;
}
  1. 如果获取到锁, 会将当前节点设置为头节点。并且返回 true 如果获取锁失败,并且超过获取锁的自旋时间那么当前线程将进入阻塞,阻塞是通过调用​​LockSupport.parkNanos(this, nanosTimeout);​​​实现的。在这个过程中可能调用多次​​shouldParkAfterFailedAcquire​​​ 方法。​​shouldParkAfterFailedAcquire​​ 可以用来修改当前节点的状态,和对链表上无效的节点出队
/** 当获取锁失败后, 检查更新新节点状态如果是需要阻塞返回, true
* <p>
* 一个前继节点 waitStatus = 0, 第一次将继续设置为 SIGNAL, 告诉当前线程准备进入阻塞, 此时依旧获取不到, 当前线程进入阻塞
*
* @param pred 前继节点
* @param node 当前节点
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 前继节点的状态, 第一次进入的话, 一定是 0
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
// 出队, 剔除无效的节点
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 第一次进来, pred.waitStatus = 0 执行这个分支
// 将前继节点的状态修改为 SIGNAL, 表示 pred.next 节点需要被唤醒(此时准备进入阻塞, 但是还未被阻塞, 再次获取锁失败之后才会被阻塞)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
  1. 解锁逻辑,下面是解锁的逻辑, 首先会进行解锁,如果 state 的状态修改为 0, 然后再去唤醒队列中排队的线程。
// 解锁
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
// 判断是否有需要唤醒的线程
if (h != null && h.waitStatus != 0) //waitStatus 的值为 0, 只有当后继存在节点才会被设置为该值不为 0, 此时需要唤醒后继线程
unparkSuccessor(h);
return true;
}
return false;
}

// tryRelease
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 判断是否是当前线程持有锁
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 如果 state == 0 表示当前线程不在占有该锁
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

// 唤醒队列中的线程
private void unparkSuccessor(Node node) {
// 将当前节点状态修改为 0
int ws = node.waitStatus;
if (ws < 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);
}
  1. 当前节点被唤醒逻辑,首先会在​​shouldParkAfterFailedAcquire​​ 方法中出队,然后尝试加锁如果加锁成功就返回 true.