专栏原创出处:github-源笔记文件 github-源码 ,欢迎 Star,转载请附上原文出处链接和本声明。

Java 并发编程专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java 并发编程

# 什么是锁

随着集成电路越来越发达,多计算核心的机器大行其道,为了解决多个并行执行分支对某一块资源的同步访问,操作系统层面提供了 互斥信号量 的概念。 在几乎所有的支持多线程编程模型的语言中,基本上都提供了与互斥信号量对应的概念,在 Java 中我们称之为锁。

# 锁的内存语义分析

本文将借助 ReentrantLock 的源代码,来分析锁内存语义的具体实现机制。

ReentrantLock 是可重入锁,即可多次加锁。涉及 volatile、CAS 参考《内存模型-volatile》《内存模型-CAS实现原理》

public class ReentrantLockExample {
    int value = 0;
    private final ReentrantLock lock = new ReentrantLock();

    public void writer() {
        lock.lock();        // 加锁
        try {
            value++;
        } finally {
            lock.unlock();  // 解锁
        }
    }

    public void reader() {
        lock.lock();        // 加锁
        try {
            int tmp = value;
            // do something ...
            System.out.println(tmp);
        } finally {
            lock.unlock();  // 解锁
        }
    }
}

在 ReentrantLock 中,调用 lock() 方法获取锁;调用 unlock() 方法释放锁。

ReentrantLock 的实现依赖于 Java 同步器框架 AbstractQueuedSynchronizer(本文简称之为 AQS)。
AQS 使用一个整型的 volatile 变量 (命名为 state) 来维护同步状态。

ReentrantLock-类图关系

ReentrantLock 内部实现了公平锁(FairSync)和非公平锁(NonfairSync)。
公平锁和非公平锁加锁逻辑存在差异,公平锁按线程排队优先级获取锁,非公平自然竞争。解锁逻辑完全一样

# 公平锁-加锁分析

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }
    
    // 公平锁-加锁最终执行方法
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState(); // 获取锁的开始,首先读 volatile 变量 state
        if (c == 0) { // 可以竞争
            if (!hasQueuedPredecessors() && // 是否排在线程队列头节点(公平)
                compareAndSetState(0, acquires)) { // CAS 方式修改 state
                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;
    }
}

# 非公平锁-加锁分析

static final class NonfairSync extends Sync {

    final void lock() {
        if (compareAndSetState(0, 1)) // 无竞争情况直接获取锁
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1); // 存在竞争使用 nonfairTryAcquire 竞争锁资源
    }

    // nonfairTryAcquire 实现中无需判断 hasQueuedPredecessors 线程优先级
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

// AbstractQueuedSynchronizer 中提供 CAS 操作
protected final boolean compareAndSetState(int expect, int update) {
     // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

# 公平锁非公平锁-解锁分析

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
          throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) { // 是否完全释放(重入锁,即多次加锁时需多次释放)
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c); // 释放锁的最后,写 volatile 变量 state
    return free;
}

释放锁的最后写 volatile 变量 state,在获取锁时首先读这个 volatile 变量。
根据 volatile 的 happens-before 规则,释放锁的线程在写 volatile 变量之前可见的共享变量,在获取锁的线程读取同一个 volatile 变量后将立即变得对获取锁的线程可见。

# 锁内存语义总结

由于 Java 的 CAS 同时具有 volatile 读和 volatile 写的内存语义,因此 Java 线程之间的通信现在有了下面 4 种方式。

  1. A 线程写 volatile 变量,随后 B 线程读这个 volatile 变量。
  2. A 线程写 volatile 变量,随后 B 线程用 CAS 更新这个 volatile 变量。
  3. A 线程用 CAS 更新一个 volatile 变量,随后 B 线程用 CAS 更新这个 volatile 变量。
  4. A 线程用 CAS 更新一个 volatile 变量,随后 B 线程读这个 volatile 变量。

锁的通用化的实现模式:

  • 声明共享变量为 volatile。
  • 使用 CAS 的原子条件更新来实现线程之间的同步。
  • 配合以 volatile 的读/写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。

# concurrent 包实现

volatile 变量的读/写和 CAS 可以实现线程之间的通信。把这些特性整合在一起,就形成了整个 concurrent 包得以实现的基石。

AQS,非阻塞数据结构和原子变量类 (java.util.concurrent.atomic 包中的类),这些 concurrent 包中的基础类都是使用这种模式来实现的,而 concurrent 包中的高层类又是依赖于这些基础类来实现的。

concurrent 包实现层级
Lock、同步器、阻塞队列、Executor、并发容器
↑      ↑
AQS、非阻塞数据结构、原子变量类
↑      ↑
volatile 变量的读写、CAS

# 思考

  • 为什么锁需要 [volatile 变量的读写、CAS] 来提供内存语义,仅使用 volatile 可以吗?
  • 如果加锁时 CAS 修改 state 状态失败怎么办?

# 参考

  • 并发编程的艺术
最后修改时间: 2/17/2020, 4:43:04 AM