Java中的公平锁和非公平锁实现详解

 

前言

Java语言中有许多原生线程安全的数据结构,好比ArrayBlockingQueueCopyOnWriteArrayListLinkedBlockingQueue,它们线程安全的实现方式并不是经过synchronized关键字,而是经过java.util.concurrent.locks.ReentrantLock来实现。 恰好对这个很感兴趣, 所以写一篇博客详细分析此 “可重入锁实现原理”。 
ReentrantLock的实现是基于其内部类FairSync(公平锁)和NonFairSync(非公平锁)实现的。 其可重入性是基于Thread.currentThread()实现的: 若是当前线程已经得到了执行序列中的锁, 那执行序列以后的全部方法均可以得到这个锁。html

公平锁:java

  • 公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每个请求当前锁的线程。公平锁则在于每次都是依次从队首取值。
  • 锁的实现方式是基于以下几点: 
    • 表结点Node和状态statevolatile关键字。
    • sum.misc.Unsafe.compareAndSet的原子操做(见附录)。

非公平锁:node

  • 在等待锁的过程当中, 若是有任意新的线程妄图获取锁,都是有很大的概率直接获取到锁的。

ReentrantLock锁都不会使得线程中断,除非开发者本身设置了中断位。 
ReentrantLock获取锁里面有看似自旋的代码,可是它不是自旋锁。 
ReentrantLock公平与非公平锁都是属于排它锁。程序员

ReentrantLock的可重入性分析

这里有一篇对锁介绍甚为详细的文章 朱小厮的博客-Java中的锁.windows

synchronized的可重入性

参考这篇文章: Java内置锁synchronized的可重入性缓存

java线程是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation)”的(java中线程得到对象锁的操做是以每线程为粒度的,per-invocation互斥体得到对象锁的操做是以每调用做为粒度的)安全

ReentrantLock的可重入性

前言里面提到,ReentrantLock重入性是基于Thread.currentThread()实现的: 若是当前线程已经得到了锁, 那该线程下的全部方法均可以得到这个锁。ReentrantLock的锁依赖只有 NonfairSyncFairSync两个实现类, 他们的锁获取方式大同小异。数据结构

可重入性的实现基于下面代码片断的 else if 语句多线程

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        ...
        // 尝试获取锁成功
    }
    else if (current == getExclusiveOwnerThread()) {
        // 是当前线程,直接获取到锁。实现可重入性。
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

此处有两个值须要关心:并发

/**
     * The current owner of exclusive mode synchronization.
     * 持有该锁的当前线程
     */
    private transient Thread exclusiveOwnerThread;

     -----------------两个值不在同一个类----------------

    /**
     * The synchronization state.
     * 0: 初始状态-无任何线程获得了锁
     * > 0: 被线程持有, 具体值表示被当前线程持有的执行次数
     * 
     * 这个字段在解锁的时候也须要用到。
     * 注意这个字段的修饰词: volatile
     */
    private volatile int state;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

ReentrantLock锁的实现分析

公平锁和非公平锁

ReentrantLock 的公平锁和非公平锁都委托了 AbstractQueuedSynchronizer#acquire 去请求获取。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • tryAcquire 是一个抽象方法,是公平与非公平的实现原理所在。
  • addWaiter 是将当前线程结点加入等待队列之中。公平锁在锁释放后会严格按照等到队列去取后续值而非公平锁在对于新晋线程有很大优点
  • acquireQueued 在屡次循环中尝试获取到锁或者将当前线程阻塞。
  • selfInterrupt 若是线程在阻塞期间发生了中断,调用 Thread.currentThread().interrupt() 中断当前线程。

ReentrantLock 对线程的阻塞是基于 LockSupport.park(this); (见 AbstractQueuedSynchronizer#parkAndCheckInterrupt)。 先决条件是当前节点有限次尝试获取锁失败。

公平锁和非公平锁在说的获取上都使用到了 volatile 关键字修饰的state字段, 这是保证多线程环境下锁的获取与否的核心。 
可是当并发状况下多个线程都读取到 state == 0时,则必须用到CAS技术,一门CPU的原子锁技术,可经过CPU对共享变量加锁的形式,实现数据变动的原子操做。 
volatile 和 CAS的结合是并发抢占的关键。

公平锁FairSync

公平锁的实现机理在于每次有线程来抢占锁的时候,都会检查一遍有没有等待队列,若是有, 当前线程会执行以下步骤:

if (!hasQueuedPredecessors() &&
    compareAndSetState(0, acquires)) {
    setExclusiveOwnerThread(current);
    return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5

其中hasQueuedPredecessors是用于检查是否有等待队列的。

public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

非公平锁NonfairSync

非公平锁在实现的时候屡次强调随机抢占:

if (c == 0) {
    if (compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

与公平锁的区别在于新晋获取锁的进程会有屡次机会去抢占锁。若是被加入了等待队列后则跟公平锁没有区别。

ReentrantLock锁的释放

ReentrantLock锁的释放是逐级释放的,也就是说在 可重入性 场景中,必需要等到场景内全部的加锁的方法都释放了锁, 当前线程持有的锁才会被释放! 
释放的方式很简单, state字段减一便可:

protected final boolean tryRelease(int releases) {
    //  releases = 1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

ReentrantLock等待队列中元素的唤醒

当当前拥有锁的线程释放锁以后, 且非公平锁无线程抢占,就开始线程唤醒的流程。 
经过tryRelease释放锁成功,调用LockSupport.unpark(s.thread); 终止线程阻塞。 
见代码:

private void unparkSuccessor(Node node) {
    // 强行回写将被唤醒线程的状态
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    // s为h的下一个Node, 通常状况下都是非Null的
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 不然按照FIFO原则寻找最早入队列的而且没有被Cancel的Node
        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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

ReentrantLock内存可见性分析

针对以下代码:

try {
    lock.lock();
    i ++;
} finally {
    lock.unlock();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

能够发现哪怕在不使用 volatile关键字修饰元素i的时候, 这里的i 也是没有并发问题的。

CAS和volatile, Java并发的基石

volatile 是Java语言的关键字, 功能是保证被修饰的元素(共享变量):

任何进程在读取的时候,都会清空本进程里面持有的共享变量的值,强制从主存里面获取; 
任何进程在写入完毕的时候,都会强制将共享变量的值写会主存。 
volatile 会干预指令重排。 
volatile 实现了JMM规范的 happen-before 原则。

在多核多线程CPU环境下, CPU为了提高指令执行速度,在保证程序语义正确的前提下,容许编译器对指令进行重排序。也就是说这种指令重排序对于上层代码是感知不到的,咱们称之为 processor ordering.

JMM 容许编译器在指令重排上自由发挥,除非程序员经过 volatile等 显式干预这种重排机制,创建起同步机制,保证多线程代码正确运行。见文章:Java并发:volatile内存可见性和指令重排

当多个线程之间有互相的数据依赖的以后, 就必须显式的干预这个指令重排机制

CAS是CPU提供的一门技术。在单核单线程处理器上,全部的指令容许都是顺序操做;可是在多核多线程处理器上,多线程访问同一个共享变量的时候,可能存在并发问题。

使用CAS技术能够锁定住元素的值。Intel开发文档, 第八章 
编译器在将线程持有的值与被锁定的值进行比较,相同则更新为更新的值。 
CAS一样遵循JMM规范的 happen-before 原则。 
JAVA CAS原理深度分析博客

公平锁和非公平锁在说的获取上都使用到了 volatile 关键字修饰的state字段, 这是保证多线程环境下锁的获取与否的核心。 
可是当并发状况下多个线程都读取到 state == 0时,则必须用到CAS技术,一门CPU的原子锁技术,可经过CPU对共享变量加锁的形式,实现数据变动的原子操做。 
volatile 和 CAS的结合是并发抢占的关键。

JSR-133编译器编写手册

JMM规范经历了多代迭代, JSR-133为较为通用的一版规范。

编译器编写手册文档见: The JSR-133 Cookbook for Compiler Writers (非官方指南)

上面小章节描述到了 volatile能够避免掉的指令重排, 那它怎么避免的呢?

在内存的读写过程当中, 无非 读/写 二者操做的四种组合:

  • LoadStore
  • LoadLoad
  • StoreStore
  • StoreLoad

volatile关键字经过提供“内存屏障”的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。而大多数的处理器都支持内存屏障的指令。

  • volatile读操做的后面插入一个LoadLoad屏障。
  • volatile写操做的后面插入一个StoreLoad屏障。

那这个StoreLoad /LoadLoad有什么用处呢? 见 Intel开发文档, 第八章。 简单的说StoreLoad就是触发后续指令中的线程缓存回写到内存; 而LoadLoad会触发线程从新从主存里面读数据进行处理。

Synchronization mechanisms in multiple-processor systems may depend upon a strong memory-ordering model. Here, a program can use a locking instruction such as the XCHG instruction or the LOCK prefix to ensure that a read-modify-write operation on memory is carried out atomically. Locking operations typically operate like I/O operations in that they wait for all previous instructions to complete and for all buffered writes to drain to memory.

ReentrantLock内存可见性

在上述博客中的: ReentrantLock锁的实现分析#公平锁和非公平锁 中讲到:ReentrantLock 经过 volatile 和 CAS 的搭配实现锁的功能。

顺带的, volatile 关键字修饰的 state 字段读和后续的锁释放中的 state 字段写, 共同组成了保证ReentrantLock内存可见性的内存屏障。 此屏障保证了ReentrantLock的内存可见性

CAS的相似volatile内存屏障原理

参见文章 深刻理解Java内存模型(五)——锁 
以下文档部分摘录

volatile是经过在Java编译时,添加字节码来实现内存屏障功能。

CAS经过本地JNI调用,Java代码为 Unsafe.java, 层次调用为:unsafe.cpp > atomic.cpp > atomicwindowsx86.inline.hpp。调用的代码如是:

#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

如上面源代码所示,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。若是程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,若是程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不须要lock前缀提供的内存屏障效果)。

intel的手册对lock前缀的说明以下:

  1. 确保对内存的读-改-写操做原子执行。在Pentium及Pentium以前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其余处理器暂时没法经过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上作了一个颇有意义的优化:若是要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),而且该内存区域被彻底包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。因为在指令执行期间该缓存行会一直被锁定,其它处理器没法读/写该指令要访问的内存区域,所以能保证指令执行的原子性。这个操做过程叫作缓存锁定(cache locking),缓存锁定将大大下降lock前缀指令的执行开销,可是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
  2. 禁止该指令与以前和以后的读和写指令重排序。
  3. 把写缓冲区中的全部数据刷新到内存中。

上面的第2点和第3点所具备的内存屏障效果,足以同时实现volatile读和volatile写的内存语义。

原文:https://blog.csdn.net/qyp199312/article/details/70598480

相关文章
相关标签/搜索