本文先阐述 Java 中各种锁的概念。然后,介绍锁的核心实现 AQS。

  • 互斥,即同一时刻只允许一个线程访问共享资源;
  • 同步,即线程之间如何通信、协作。

一、并发锁简介

1.1 可重入锁

可重入锁,顾名思义,指的是线程可以重复获取同一把锁。 即同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。

可重入锁可以在一定程度上避免死锁。

  • ReentrantLock 、ReentrantReadWriteLock 是可重入锁。这点,从其命名也不难看出。
  • synchronized 也是一个可重入锁。

1.2 公平锁与非公平锁

  • 公平锁 - 公平锁是指多线程按照申请锁的顺序来获取锁。
  • 非公平锁 - 非公平锁是指多线程不按照申请锁的顺序来获取锁 。这就可能会出现优先级反转(后来者居上)或者饥饿现象(某线程总是抢不过别的线程,导致始终无法执行)。

synchronized 只支持非公平锁;ReentrantLockReentrantReadWriteLock,默认是非公平锁,但支持公平锁。

1.3 排它锁与共享锁

  • 独享锁 - 独享锁是指 锁一次只能被一个线程所持有。
  • 共享锁 - 共享锁是指 锁可被多个线程所持有。 synchronized 、ReentrantLock 只支持独享锁。ReentrantReadWriteLock 其写锁是独享锁,其读锁是共享锁。读锁是共享锁使得并发读是非常高效的,读写,写读,写写的过程是互斥的。

1.4 悲观锁与乐观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是处理并发同步的策略。

  • 悲观锁 - 悲观锁对于并发采取悲观的态度,认为:不加锁的并发操作一定会出问题。悲观锁适合写操作频繁的场景。
  • 乐观锁 - 乐观锁对于并发采取乐观的态度,认为:不加锁的并发操作也没什么问题。对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用不断尝试更新的方式更新数据。乐观锁适合读多写少的场景。

悲观锁与乐观锁在 Java 中的典型实现:

  • 悲观锁在 Java 中的应用就是通过使用 synchronized 和 Lock 显示加锁来进行互斥同步,这是一种阻塞同步。

  • 乐观锁在 Java 中的应用就是采用 CAS 机制(CAS 操作通过 Unsafe 类提供,但这个类不直接暴露为 API,所以都是间接使用,如各种原子类)。

1.5 偏向锁、轻量级锁、重量级锁

所谓轻量级锁与重量级锁,指的是锁控制粒度的粗细。显然,控制粒度越细,阻塞开销越小,并发性也就越高。

Java 1.6 以后,针对 synchronized 做了大量优化,引入 4 种锁状态: 无锁状态、偏向锁、轻量级锁和重量级锁。锁可以单向的从偏向锁升级到轻量级锁,再从轻量级锁升级到重量级锁 。

1.6 分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁。所谓分段锁,就是把锁的对象分成多段,每段独立控制,使得锁粒度更细,减少阻塞开销,从而提高并发性。这其实很好理解,就像高速公路上的收费站,如果只有一个收费口,那所有的车只能排成一条队缴费;如果有多个收费口,就可以分流了。

Java 1.7 以前的 ConcurrentHashMap 就是分段锁的典型案例。ConcurrentHashMap 维护了一个 Segment 数组,一般称为分段桶。

当有线程访问 ConcurrentHashMap 的数据时,ConcurrentHashMap 会先根据 hashCode 计算出数据在哪个桶(即哪个 Segment),然后锁住这个 Segment。

二、AQS

AQS 提供了对独享锁与共享锁的支持。AbstractQueueSychronizer

java.util.concurrent.locks 包中的相关锁(常用的有 ReentrantLock、 ReadWriteLock)都是基于 AQS 来实现。这些锁都没有直接继承 AQS,而是定义了一个 Sync 类去继承 AQS。为什么要这样呢?因为锁面向的是使用用户,而同步器面向的则是线程控制,那么在锁的实现中聚合同步器而不是直接继承 AQS 就可以很好的隔离二者所关注的事情。

2.1 应用

AQS 提供了对独享锁与共享锁的支持。

独享锁 API 获取、释放独享锁的主要 API 如下:

1
2
3
4
public final void acquire(int arg)
public final void acquireInterruptibly(int arg)
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
public final boolean release(int arg)
  • acquire - 获取独占锁。
  • acquireInterruptibly - 获取可中断的独占锁。
  • tryAcquireNanos - 尝试在指定时间内获取可中断的独占锁。在以下三种情况下回返回:
    • 在超时时间内,当前线程成功获取了锁;
    • 当前线程在超时时间内被中断;
    • 超时时间结束,仍未获得锁返回 false。
  • release - 释放独占锁。

共享锁 API 获取、释放共享锁的主要 API 如下:

1
2
3
4
public final void acquireShared(int arg)
public final void acquireSharedInterruptibly(int arg)
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
public final boolean releaseShared(int arg)
  • acquireShared - 获取共享锁。
  • acquireSharedInterruptibly - 获取可中断的共享锁。
  • tryAcquireSharedNanos - 尝试在指定时间内获取可中断的共享锁。
  • release - 释放共享锁。

2.2 原理

ASQ 原理要点:

  • AQS 使用一个整型的 volatile 变量来 维护同步状态。状态的意义由子类赋予。
  • AQS 维护了一个 FIFO 的双链表,用来存储获取锁失败的线程。

AQS 围绕同步状态提供两种基本操作“获取”和“释放”,并提供一系列判断和处理方法,简单说几点:

  • state 是独占的,还是共享的;
  • state 被获取后,其他线程需要等待;
  • state 被释放后,唤醒等待线程; 线程等不及时,如何退出等待。 至于线程是否可以获得 state,如何释放 state,就不是 AQS 关心的了,要由子类具体实现。

2.2.1 AQS 的数据结构

阅读 AQS 的源码,可以发现:AQS 继承自 AbstractOwnableSynchronize。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /** 等待队列的队头,懒加载。只能通过 setHead 方法修改。 */
    private transient volatile Node head;
    /** 等待队列的队尾,懒加载。只能通过 enq 方法添加新的等待节点。*/
    private transient volatile Node tail;
    /** 同步状态 */
    private volatile int state;
}
  • state - AQS 使用一个整型的 volatile 变量来 维护同步状态。 这个整数状态的意义由子类来赋予,如ReentrantLock 中该状态值表示所有者线程已经重复获取该锁的次数,Semaphore 中该状态值表示剩余的许可数量。
  • headtail - AQS 维护了一个 Node 类型(AQS 的内部类)的双链表来完成同步状态的管理。 这个双链表是一个双向的 FIFO 队列,通过 head 和 tail 指针进行访问。当 有线程获取锁失败后,就被添加到队列末尾。 再来看一下 Node 的源码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static final class Node {
    /** 该等待同步的节点处于共享模式 */
    static final Node SHARED = new Node();
    /** 该等待同步的节点处于独占模式 */
    static final Node EXCLUSIVE = null;

    /** 线程等待状态,状态值有: 0、1、-1、-2、-3 */
    volatile int waitStatus;
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;

    /** 前驱节点 */
    volatile Node prev;
    /** 后继节点 */
    volatile Node next;
    /** 等待锁的线程 */
    volatile Thread thread;

  	/** 和节点是否共享有关 */
    Node nextWaiter;
}

很显然,Node 是一个双链表结构。

  • waitStatus - Node 使用一个整型的 volatile 变量来 维护 AQS 同步队列中线程节点的状态。waitStatus 有五个状态值:
    • CANCELLED(1) - 此状态表示:该节点的线程可能由于超时或被中断而 处于被取消(作废)状态,一旦处于这个状态,表示这个节点应该从等待队列中移除。
    • SIGNAL(-1) - 此状态表示:后继节点会被挂起,因此在当前节点释放锁或被取消之后,必须唤醒(unparking)其后继结点。
    • CONDITION(-2) - 此状态表示:该节点的线程处于等待条件状态,不会被当作是同步队列上的节点,直到被唤醒(signal),设置其值为 0,再重新进入阻塞状态。
    • PROPAGATE(-3) - 此状态表示:下一个 acquireShared 应无条件传播。
    • 0 - 非以上状态

2.2.2 独占锁的获取和释放

1、获取独占锁 AQS 中使用 acquire(int arg) 方法获取独占锁,其大致流程如下:

  1. 先尝试获取同步状态,如果获取同步状态成功,则结束方法,直接返回。
  2. 如果获取同步状态不成功,AQS 会不断尝试利用 CAS 操作将当前线程插入等待同步队列的队尾,直到成功为止。
  3. 接着,不断尝试为等待队列中的线程节点获取独占锁。

2、释放独占锁 AQS 中使用 release(int arg) 方法释放独占锁,其大致流程如下:

  1. 先尝试获取解锁线程的同步状态,如果获取同步状态不成功,则结束方法,直接返回。
  2. 如果获取同步状态成功,AQS 会尝试唤醒当前线程节点的后继节点。

3、获取可中断的独占锁 AQS 中使用 acquireInterruptibly(int arg) 方法获取可中断的独占锁。

acquireInterruptibly(int arg) 实现方式相较于获取独占锁方法( acquire)非常相似,区别仅在于它会通过 Thread.interrupted 检测当前线程是否被中断,如果是,则立即抛出中断异常(InterruptedException)。

4、获取超时等待式的独占锁 AQS 中使用 tryAcquireNanos(int arg) 方法获取超时等待的独占锁。

doAcquireNanos 的实现方式 相较于获取独占锁方法( acquire)非常相似,区别在于它会根据超时时间和当前时间计算出截止时间。在获取锁的流程中,会不断判断是否超时,如果超时,直接返回 false;如果没超时,则用 LockSupport.parkNanos 来阻塞当前线程。

2.2.4 共享锁的获取和释放

1、获取共享锁 AQS 中使用 acquireShared(int arg) 方法获取共享锁。

acquireShared 方法和 acquire 方法的逻辑很相似,区别仅在于自旋的条件以及节点出队的操作有所不同。

成功获得共享锁的条件如下:

  • tryAcquireShared(arg) 返回值大于等于 0 (这意味着共享锁的 permit 还没有用完)。
  • 当前节点的前驱节点是头结点。 2、释放共享锁 AQS 中使用 releaseShared(int arg) 方法释放共享锁。

releaseShared 首先会尝试释放同步状态,如果成功,则解锁一个或多个后继线程节点。释放共享锁和释放独享锁流程大体相似,区别在于:

对于独享模式,如果需要 SIGNAL,释放仅相当于调用头节点的 unparkSuccessor。

3、获取可中断的共享锁 AQS 中使用 acquireSharedInterruptibly(int arg) 方法获取可中断的共享锁。

acquireSharedInterruptibly 方法与 acquireInterruptibly 几乎一致,不再赘述。

4、获取超时等待式的共享锁 AQS 中使用 tryAcquireSharedNanos(int arg) 方法获取超时等待式的共享锁。

tryAcquireSharedNanos 方法与 tryAcquireNanos 几乎一致,不再赘述。