前言
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费CPU时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。
一、基本概念
1.1、自旋
多核机器,能够让两个或以上的线程同时并行执行,让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。而为了让当前线程“稍等一下”,我们需让当前线程进行自旋(循环直到条件满足),如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。
- 使用场景:锁竞争不激烈、锁持有时间短的场景, 替代阻塞;
- 表现:不阻塞,一直循环,直到满足条件;
- 优点:无需上下文切换,也就避免了操作系统的进程调度;
- 缺点:
- 锁饥饿:竞争激烈的情况下,没有公平性,可能存在一个线程一直获取不到。
- 性能问题:竞争激烈的情况下,自旋锁锁状态中心化,锁状态变更会导致多个 CPU 的高速缓存的频繁同步,从而拖慢 CPU 效率。
/**
* +-----------+
* | 临界区 |
* | (线程1) |
* (线程2自旋) <——+ +-----------+ <——+ (线程3自旋)
* | | | |
* +——> +——>
*/
class Spin {
/**
* 中心化锁状态
*/
AtomicReference<Thread> spin = new AtomicReference<>();
/**
* 循环直到条件满足
*/
public void lock() {
while (!spin.compareAndSet(null, Thread.currentThread()));
}
public void unlock() {
spin.compareAndSet(Thread.currentThread(), null);
}
}
1.2、CLH
C、L、H 三个人一起针对自旋改进的锁结构,故取三人名字首字母命名。
- 改进点:
- 锁饥饿:增加队列(FIFO)保证公平性;
- 性能: 锁状态去中心化,让每个线程在不同的状态变量中自旋,这样当一个线程释放它的锁时,只能使其后续线程的高速缓存失效;
- 缩小了影响范围,从而减少了 CPU 的开销。
- 实现:队列采用伪链表实现:没有实际指针,只是针对前一个节点的值进行判断,不需要遍历整个队列。
- 缺点:
- 1、自旋的缺点,即锁持有时间很长时,CPU一直在空转;
- 2、功能单一,不支持复杂的功能,比如前驱节点线程因超时而取消获取锁时,节点里的锁状态将不会修改,导致后续节点永远自旋下去。
/**
* +-------+ +-----------+ +-----------+ +-----------+ +-----------+
* | 临界区 | |Thread1 | |Thread2 | |Thread3 | |Thread4 |
* |Thread1| | True|-----<---+---| True|-----<---+---| True|-----<---+--| True|
* +------+ +-----------+ | 自旋 | +-----------+ | 自旋 | +-----------+ | 自旋 | +-----------+
* Node +-->--+ Node +-->--+ Node +-->--+ tail
*/
class CLH {
/**
* 去中心化锁状态
* 当前线程修改当前锁状态,供后面一个线程读取。
* 2个线程写读同一个变量,用volatile修饰保证内存可见性。
*/
private static class Node{
volatile boolean locked;
}
/**
* 存储当前线程的锁节点状态,用于后继节点读取。
*/
ThreadLocal<Node> node = ThreadLocal.withInitial(Node::new);
/**
* 尾节点指向哨兵
*/
AtomicReference<Node> tail = new AtomicReference<>(new Node());
public void lock() {
final Node currentThreadNode = this.node.get();
currentThreadNode.locked = true;
final Node preNode = tail.getAndSet(currentThreadNode);
while (!preNode.locked); // 对前一个线程的锁状态自旋
}
/**
* 死循环:1、Thread3 修改 locked = false 后,
* 2、Thread4还未读取到该状态时(譬如操作系统分时调度暂停该线程运行),Thread3再次执行 lock() 后,导致死循环。
* Tail
* +-----------+ +-----------+
* |Thread3 | |Thread4 |
* +--| True|<--| True|<---+
* | +-----------+ +-----------+ |
* |___________________________________|
*/
public void unlock() {
final Node currentThreadNode = this.node.get();
currentThreadNode.locked = false; // 下一个线程的 lock() 会成功读取该状态,终止循环(自旋)
node.set(new Node()); // 避免死循环,同时为下一次当前线程上锁做准备。
}
}
1.3、AQS
AbstractQueuedSynchronizer
简称 AQS,详见 java.util.concurrent.locks.AbstractQueuedSynchronizer
,广泛使用在 Semaphore
、CountDownLatch
、ReentrantLock
、ReentrantReadWriteLock
、ThreadPoolExecutor.Worker
等并发场景下。
AQS 核心思想:如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。
- 改进点:
- 自旋缺点:锁持有时间长时,阻塞获取锁的线程,避免CPU空转。
- 功能单一:用显示队列,即Node增加显示前驱(prev)后继(next)节点,扩展节点状态([boolean]locked -> [int]status)。
- 举例: 队列一个线程取消获取锁时,会修改节点状态。当前线程释放锁时,会根据节点状态唤醒后继正常等待的节点,从而解除阻塞。
- 节点状态:
-
SIGNAL 表示该节点正常等待
-
PROPAGATE 应将 releaseShared 传播到其他节点
-
CONDITION 该节点位于条件队列,不能用于同步队列节点
-
CANCELLED 由于超时、中断或其他原因,该节点被取消
-
/**
* +-----------+
* | state |
* | 临界区 |
* (----------->| (线程1) |<--------)
* ( +-----------+ )
* ( ( ) )
* ( ( ) )
* ( ( ) )
* +-----------+ +-----------+ +-----------+ +-----------+
* |status | |status | |status | |status |
* |Thread1 | |Thread2 | |Thread3 | |Thread4 |
* |prev |<--|prev |<--|prev |<--|prev |
* | next|-->| next|-->| next|-->| next|
* +-----------+ +-----------+ +-----------+ +-----------+
* head Node Node tail
*
*/
class AQS {
static class Node {
volatile Node prev; // initially attached via casTail
volatile Node next; // visibly nonnull when signallable
Thread waiter; // visibly nonnull when enqueued
volatile int status; // written by owner, atomic bit ops by others
}
/**
* 也是一个哨兵模式,并且懒加载
*/
private transient volatile Node head;
/**
* 实例化后,通过 casTail 方法来修改
*/
private transient volatile Node tail;
// 共享变量,使用volatile修饰保证线程可见性
private volatile int state;
/** tries once to CAS a new dummy node for head
* 详见 AbstractQueuedSynchronizer.tryInitializeHead()
* */
private void tryInitializeHead() {
Node h = new Node(); // 哨兵模式
if (CAS(this, null, h))
tail = h;
}
}