编者注:本文为历史博文归档,内容涉及 JDK、框架与工具链版本请以当前官方文档为准。文中引用外链图片可能失效,阅读时请注意时效性。

1. JUC 概况

以下是 Java JUC(java.util.concurrent)包的主体结构:

  • Atomic:AtomicInteger 等原子类
  • Locks:Lock, Condition, ReadWriteLock
  • Collections:Queue, ConcurrentMap 等并发集合
  • Executors 框架:Future, Callable, Executor
  • Tools:CountDownLatch, CyclicBarrier, Semaphore

2. 原子操作

当多个线程执行一个操作时,如果其中任何一个线程要么完全执行完此操作,要么没有执行此操作的任何步骤,那么这个操作就是原子的(Atomic)。引入原子操作的主要原因是为了降低 synchronized 带来的性能开销。

以下以 AtomicInteger 为例,介绍其常用方法:

  • int addAndGet(int delta):以原子方式将给定值与当前值相加。实际上等于线程安全版本的 i = i + delta 操作。
  • boolean compareAndSet(int expect, int update):如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。如果成功返回 true,否则返回 false 且不修改原值。
  • int decrementAndGet():以原子方式将当前值减 1。相当于线程安全版本的 --i 操作。
  • int getAndAdd(int delta):以原子方式将给定值与当前值相加。相当于线程安全版本的 t=i; i+=delta; return t; 操作。
  • int getAndDecrement():以原子方式将当前值减 1。相当于线程安全版本的 i-- 操作。
  • int getAndIncrement():以原子方式将当前值加 1。相当于线程安全版本的 i++ 操作。
  • int getAndSet(int newValue):以原子方式设置为给定值,并返回旧值。相当于线程安全版本的 t=i; i=newValue; return t; 操作。
  • int incrementAndGet():以原子方式将当前值加 1。相当于线程安全版本的 ++i 操作。

3. 指令重排

程序的执行顺序并不能总是保证符合代码编写的顺序,CPU 为了优化性能可能会进行指令重排。

要保证程序的最终结果等同于它在严格的顺序化环境下的结果,指令的执行顺序就可能与代码的顺序不一致。

在多核 CPU 高压力下,两个线程交替执行,xy 的输出结果不确定。可能的结果如下:

序号可能结果
1x = 0,y = 1
2x = 1,y = 1
3x = 1,y = 0
4x = 0,y = 0

4. Happens-before 法则(Java 内存模型)

如果动作 B 要看到动作 A 的执行结果(无论 A/B 是否在同一个线程里面执行),那么 A/B 就需要满足 happens-before 关系。

Happens-before 规则

  • Program order rule:同一个线程中的每个 Action 都 happens-before 于出现在其后的任何一个 Action。
  • Monitor lock rule:对一个监视器的解锁 happens-before 于每一个后续对同一个监视器的加锁。
  • Volatile variable rule:对 volatile 字段的写入操作 happens-before 于每一个后续的同一个字段的读操作。
  • Thread start ruleThread.start() 的调用会 happens-before 于启动线程里面的动作。
  • Thread termination rule:Thread 中的所有动作都 happens-before 于其他线程检查到此线程结束,或者 Thread.join() 中返回,或者 Thread.isAlive() == false
  • Interruption rule:一个线程 A 调用另一个线程 B 的 interrupt() 都 happens-before 于线程 A 发现 B 被 A 中断(B 抛出异常或者 A 检测到 B 的 isInterrupted() 或者 interrupted())。
  • Finalizer rule:一个对象构造函数的结束 happens-before 与该对象的 finalizer 的开始。
  • Transitivity:如果 A 动作 happens-before 于 B 动作,而 B 动作 happens-before 于 C 动作,那么 A 动作 happens-before 于 C 动作。

因为 CPU 可以不按我们写代码的顺序执行内存的存取过程(即指令乱序或并行运行),只有上面的 happens-before 所规定的情况下,才保证顺序性。

JMM 的特性

  • 多个 CPU 之间的缓存也不保证实时同步。
  • JMM 不保证创建过程的原子性,读写并发时,可能看到不完整的对象(因此涉及双重检查锁定 DCL 时需注意)。

volatile 语义

volatile 实现了类似 synchronized 的语义,却又没有锁机制。它确保对 volatile 字段的更新以可预见的方式告知其他的线程。

  1. 禁止重排序:Java 存储模型不会对 volatile 指令的操作进行重排序,保证对 volatile 变量的操作是按照指令的出现顺序执行的。
  2. 内存可见性:volatile 变量不会被缓存在寄存器中(只拥有线程可见性),每次总是从主存中读取 volatile 变量的结果。
注意volatile 并不能保证线程安全,也就是说 volatile 字段的操作不是原子性的,volatile 变量只能保证可见性。

5. CAS 操作

CAS (Compare and Swap) 是一种乐观锁机制。

CAS 有 3 个操作数:内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。

实现简单的非阻塞算法示例:

private volatile int value; // 借助 volatile 原语,保证线程间的数据是可见的

public final int get() {
    return value;
}

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    } // Spin 自旋等待直到返回为止
}

整个 J.U.C 包都是建立在 CAS 之上的。对于 synchronized 阻塞算法,J.U.C 在性能上有了很大的提升。但 CAS 会出现所谓的 "ABA" 问题。

6. Lock 锁

Synchronized 属于独占锁,高并发时性能不高。JDK 5 以后开始用 JNI 实现更高效的锁操作。

锁类层次结构:

  • Lock

    • ReentrantLock
    • ReentrantReadWriteLock

      • ReadLock
      • WriteLock
  • LockSupport
  • Condition

Lock 接口主要方法:

方法名称作用
void lock()获取锁。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。
void lockInterruptibly() throws InterruptedException如果当前线程未被中断,则获取锁。如果锁可用,则获取锁,并立即返回。
Condition newCondition()返回绑定到此 Lock 实例的新 Condition 实例。
boolean tryLock()仅在调用时锁为空闲状态才获取该锁。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。
void unlock()释放锁。

最佳实践

  1. 一般来说,获取锁和释放锁是成对的操作,这样可以避免死锁和资源的浪费。
  2. 务必在 finally 块中执行释放锁的操作。

7. AQS (AbstractQueuedSynchronizer)

AQS 是锁机制实现的核心所在,是 Lock/Executor 实现的前提。

AQS 实现原理

基本的思想是表现为一个同步器,AQS 支持下面两个核心操作:

acquire (获取同步状态):

while (synchronization state does not allow acquire) {
    enqueue current thread if not already queued;
    possibly block current thread;
}
dequeue current thread if it was queued;

release (释放同步状态):

update synchronization state;
if (state may permit a blocked thread to acquire)
    unpark one or more queued threads;

要支持这两个操作,需要实现三个条件:

  • Atomically managing synchronization state:原子性操作同步器的状态位。
  • Blocking and unblocking threads:阻塞和唤醒线程。
  • Maintaining queues:维护一个有序的队列。

1. 原子性地管理同步状态

使用一个 32 位整数来描述状态位:private volatile int state; 对其进行 CAS 操作,确保值的正确性。

2. 阻塞和唤醒线程

JDK 5.0 以后利用 JNI 在 LockSupport 类中实现了线程的阻塞和唤醒。

  • LockSupport.park():在当前线程中调用,导致线程阻塞。
  • LockSupport.park(Object blocker)
  • LockSupport.unpark(Thread thread)

3. 维护队列

在 AQS 中采用 CLH 队列 来解决有序队列的问题(CLH = Craig, Landin, and Hagersten)。

Node 里面是什么结构?

WaitStatus –> 节点的等待状态,一个节点可能位于以下几种状态:

  • CANCELLED = 1:节点操作因为超时或者对应的线程被 interrupt。节点不应该留在此状态,一旦达到此状态将从 CLH 队列中踢出。
  • SIGNAL = -1:节点的继任节点是(或者将要成为)BLOCKED 状态(例如通过 LockSupport.park() 操作),因此一个节点一旦被释放(解锁)或者取消就需要唤醒(LockSupport.unpark())它的继任节点。
  • CONDITION = -2:表明节点对应的线程因为不满足一个条件(Condition)而被阻塞。
  • 0:正常状态,新生的非 CONDITION 节点都是此状态。
非负值标识节点不需要被通知(唤醒)。

队列管理操作:

入队 (enqueue):
采用 CAS 操作,每次比较尾结点是否一致,然后插入到尾结点中。

do {
    pred = tail;
} while (!compareAndSet(pred, tail, node));

出队 (dequeue):

while (pred.status != RELEASED);
head = node;

加锁操作:

public final void acquire(int arg) {
    if (!tryAcquire(arg))
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg);
    selfInterrupt();
}

释放操作:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
"The synchronizer framework provides a ConditionObject class for use by synchronizers that maintain exclusive synchronization and conform to the Lock interface." —— Doug Lea《The java.util.concurrent Synchronizer Framework》

以下是 AQS 队列和 Condition 队列的出入结点的示意图,可以通过这几张图看出线程结点在两个队列中的出入关系和条件。


说明:本文内容基于 JDK 5/6 时期的技术背景整理,部分实现细节在后续 JDK 版本中可能有所优化或调整,具体请以最新官方文档为准。