Java8锁相关整理

291 阅读13分钟

一、简介

本文档主要总结JDK中关于ReentrantLock和ReentrantReadWriteLock的相关知识。

首先,介绍AbstractQueuedSynchronizer,在此基础上分析以上两个锁的实现细节。

二、AbstractQueuedSynchronizer

AQS的核心思想是定义了一个同步状态(state)和一组原子操作(如CAS操作)来对状态进行操作。

AQS为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。

ReentrantLockReentrantReadWriteLock都是基于AQS实现的同步器。

  1. ReentrantLock:ReentrantLock是AQS的一个典型应用,它是一个独占锁(排它锁),同一时刻只能有一个线程持有该锁。ReentrantLock实现了AQS的独占模式,它的同步状态state表示锁的持有状态,通过CAS操作来实现对锁的获取和释放。
  2. ReentrantReadWriteLock:ReentrantReadWriteLock是AQS的另一个重要应用,它是一个读写锁,允许多个线程同时读取共享资源,但在写操作时只允许一个线程独占。在ReentrantReadWriteLock中,state的高16位表示读锁的数量,低16位表示写锁的数量。

三、ReentrantLock

1.简介

ReentrantLock中的内部类Sync继承自AbstractQueuedSynchronizer。

AQS在其内部实现了一个队列,并且通过其内部属性state的值说明当前锁被获取(acquire)和释放(release)。

ReentrantLock是可重入的互斥锁,当state=0时,表示当前锁没有被持有,当state>=1说明当前锁被持有,state表示锁的重入的次数。

ReentrantLock整体结构组成如下图:

2.非公平锁-加锁方式

  • ReentrantLock 的非公平锁策略允许多个线程并发尝试获取锁,即使有其他线程正在等待获取锁。
  • 如果第一次 CAS 尝试获取锁失败,当前线程会进入阻塞队列,等待锁的释放,并通过第二次尝试再次获取锁。这样的设计在一定程度上减少了线程切换的开销。
  • 非公平锁允许一些后续的线程插队,可能导致前面排队的线程一直无法获取锁,但在某些场景下能够提高并发性能。

2.1 加锁过程图

非公平锁的加锁过程,如图:

其中,尝试获取锁的过程如下图:

2.2 详细代码

加锁过程详细代码如下:

// NonfairSync#lock
final void lock() {
    // 首先会尝试马上争抢锁
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
    // 如果争抢失败
        acquire(1);
}
// AQS#acquire
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

// NonfairSync#nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
   final Thread current = Thread.currentThread();
   int c = getState();
   if (c == 0) { // 锁未被占用
       if (compareAndSetState(0, acquires)) {
           setExclusiveOwnerThread(current);
           return true;
       }
   }
   else if (current == getExclusiveOwnerThread()) { // 锁被当前线程占用(重入)
       int nextc = c + acquires;
       if (nextc < 0) // overflow
           throw new Error("Maximum lock count exceeded");
       setState(nextc);
       return true;
   }
   return false;
}
// AQS#addWaiter
// 将线程节点添加到队列尾部
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果当前链表(队列)为空
    enq(node);
    return node;
}
// AQS#enq
// 处理当前队列为空的情况
private Node enq(final Node node) {
    for (;;) {
        // 获取当前节点的尾结点
        Node t = tail;
        if (t == null) { // Must initialize
            // 创建了空的头结点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
} 
// AQS#acquireQueued
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋 阻塞
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 如果当前的节点前面还存在其他线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
} 
// 设置前面的节点的状态为SINGAL(当前面的节点释放锁时,唤醒后面的节点)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
		// 前面节点已经被取消,则将其从链表上去除 
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
// 阻塞当前线程 并且返回当前线程的中断状态
private final boolean parkAndCheckInterrupt() {
     LockSupport.park(this);
     return Thread.interrupted();
}

3.公平锁-加锁方式

  • ReentrantLock 的公平锁策略保证了线程获取锁的顺序按照线程的请求顺序来进行,先到先得。
  • 公平锁在尝试获取锁时,如果阻塞队列中已经有节点,会直接进入阻塞队列,而不进行 CAS 尝试直接获取锁。这样的设计保证了线程获取锁的顺序,但也可能增加线程切换的开销。
  • 当锁的持有者释放锁后,头部线程会再次进行 CAS 尝试来获取锁,成功后进入临界区执行。这样的设计避免了多个线程同时获取锁,从而保证了公平性。

3.1 加锁过程图

公平锁的加锁过程,如图:

其中,尝试获取锁的过程如下图:

3.2 详细代码

//FairSync#lock
final void lock() {
    acquire(1);
}
//AQS#acquire
// 此时arg = 1
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
// FairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
     // 如果当前锁不被任何其他线程持有,c = 0
    int c = getState();
    if (c == 0) {
        // 检查当前的队列是否有节点正在排队(与非公平锁的不同,会先检查是否有线程在自己之前申请锁)
        if (!hasQueuedPredecessors() &&
        // 如果没有节点正在排队,CAS修改当前的state为1
            compareAndSetState(0, acquires)) {
            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;
} 

后续同上非公平锁...

4.解锁过程

// ReentrantLock#unlock
public void unlock() {
    sync.release(1);
}

// AQS#release
public final boolean release(int arg) {
   if (tryRelease(arg)) {
       Node h = head;
       // 如果后继还有线程排队
       if (h != null && h.waitStatus != 0)
           unparkSuccessor(h);
       return true;
   }
   return false;
} 

// ReentrantLock#tryRelease 
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 判断是否是当前线程的锁
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 锁的次数为0 释放锁
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
//AQS#unparkSuccessor
// unpark 当前的node的后继节点
private void unparkSuccessor(Node node) {
    /*
    * If status is negative (i.e., possibly needing signal) try
    * to clear in anticipation of signalling.  It is OK if this
    * fails or if status is changed by waiting thread.
    */
    int ws = node.waitStatus;
    if (ws < 0)
       compareAndSetWaitStatus(node, ws, 0);

    // 正常情况下需要unpark的节点就是当前的节点的后继节点
    // 但是如果后继的线程(节点)cancel或者为null,则从后往前遍历
    // 找到第一个需要unpark的节点
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
               s = t;
    }

    if (s != null)
       LockSupport.unpark(s.thread);
} 

四、ReentrantReadWriteLock

1.简介

ReentrantReadWriteLock的类关系图:

AQS中维护了一个state状态变量,使用高低位切割实现state状态变量维护两种状态,即高16位表示读状态,低16位表示写状态。

Sync继承AQS实现了如下的核心抽象函数:

  • tryAcquire
  • release
  • tryAcquireShared
  • tryReleaseShared

其中,其中tryAcquire、release是为WriteLock写锁准备的;tryAcquireShared、tryReleaseShared是为ReadLock读锁准备的。

写锁可以降级为读锁,防止更新丢失

Sync中还定义了HoldCounter与ThreadLocalHoldCounter:

  • HoldCounter用来记录读锁重入数
  • ThreadLocalHoldCounter是ThreadLocal变量,用来存放除第一个获取读锁线程外的其他线程的读锁重入数

NofairSyncFairSync主要实现:非公平模式和公平模式下获取锁时,读写锁是否应该阻塞。

// NofairSync
// 非公平模式下写锁总是阻塞的
final boolean writerShouldBlock() {
    return false; // writers can always barge
}
// 非公平模式下 如果等待队列中 下一个节点是写锁 则读锁阻塞
// 为等待的写入操作提供机会,避免写入操作长时间无法执行
final boolean readerShouldBlock() {
    /* As a heuristic to avoid indefinite writer starvation,
     * block if the thread that momentarily appears to be head
     * of queue, if one exists, is a waiting writer.  This is
     * only a probabilistic effect since a new reader will not
     * block if there is a waiting writer behind other enabled
     * readers that have not yet drained from the queue.
     */
    return apparentlyFirstQueuedIsExclusive();
}

final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}
final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
    return hasQueuedPredecessors();
}

2.写锁加锁

2.1 函数调用链

加锁过程中,函数调用链如下:

其中,AQS.acquire()是父类提供的模板函数,包含了尝试加锁以及加锁失败的处理流程(创建节点、自旋阻塞重试)。加锁失败的处理流程同上文的ReentrantLock,这里重点关注尝试加锁过程。

2.2 加锁过程图

Sync.tryAcquire()详细过程如下:

2.3 详细代码

protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        // 不存在写锁(都是读锁) or (存在写锁、加锁的线程不是当前线程)
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 如果锁重入次数 达到最大值
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire 锁重入
        setState(c + acquires);
        return true;
    }
    // 当前锁未被任何线程占有 判断是否需要被阻塞再进行CAS加锁操作
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

3.写锁解锁

3.1 函数调用链

解锁流程函数调用链如下:

AQS.release()中除了tryRelease(),其他部分是释放独占锁成功的处理流程。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        // 锁释放成功 唤醒队列中的下一个节点
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

3.2 解锁过程图

Sync.tryRelease()流程图如下:

3.3 详细代码

protected final boolean tryRelease(int releases) {
    // 如果写锁不是当前线程持有
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 写锁数量-1
    int nextc = getState() - releases;
    // 写锁数量是否为0
    boolean free = exclusiveCount(nextc) == 0;
    if (free) 
        setExclusiveOwnerThread(null); // 写锁已经全部释放,设置持有写锁线程为null
    // 设置写数量
    setState(nextc);
    return free;
}

4.读锁加锁

4.1 函数调用链

读锁解锁过程的函数调用链如下:

AQS.acquireShared()tryacquireShared()是尝试加锁,doAcquireShared()是加锁失败后的处理流程。

// AQS#acquireShared
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

这里先看一下doAcquireShared()tryAcquireShared()在4.2和4.3详细介绍。

private void doAcquireShared(int arg) {
    // 为线程创建共享模式等待节点并将其添加到等待队列的尾部
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋,不断尝试获取共享资源
        for (;;) {
            // 获取当前节点的前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点是头节点(表示当前节点是队列中的第一个等待节点)
            if (p == head) {
                // 尝试获取共享资源
                int r = tryAcquireShared(arg);
                // 如果获取共享资源成功
                if (r >= 0) {
                    // 设置当前节点为头节点,同时唤醒后继节点,使其尝试获取共享资源
                    setHeadAndPropagate(node, r);
                    // help GC,帮助垃圾回收,将当前节点的前驱节点置为null
                    p.next = null;
                    // 如果在等待过程中发生过中断,则重新中断当前线程
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 判断是否需要将当前线程阻塞(park),并检查线程是否被中断过
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 如果获取共享资源失败,则取消当前节点的等待,将其从等待队列中移除
        if (failed)
            cancelAcquire(node);
    }
}

4.2 加锁过程图

Sync.tryAcquireShared()的加锁流程如下图:

4.3 详细代码

protected final int tryAcquireShared(int unused) {

    // 获取当前线程
    Thread current = Thread.currentThread();

    // 获取state
    int c = getState();

    // 如果写锁被其他线程持有,则失败,返回-1
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
        return -1;

    // 获取当前读锁数量
    int r = sharedCount(c);

    // 如果当前线程不应该被阻塞,并且读锁数量未达到饱和,且CAS操作成功
    if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
        // 如果读锁数量为0,则将当前线程设置为第一个读锁线程,并设置读锁计数为1
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            // 如果当前线程已经是第一个读锁线程,则读锁计数增加1
            firstReaderHoldCount++;
        } else {
            // 如果当前线程不是第一个读锁线程,则从缓存中获取读锁计数器,并将计数器加一
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                // 如果缓存cachedHoldCounter不是当前线程的读锁计数器,则通过readHolds.get()方法从缓存readHolds中获取
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                // 如果之前的计数器count为0,重新放回readHolds缓存中
                readHolds.set(rh);
            rh.count++;
        }
        
        // 返回1表示成功获取读锁
        return 1;
    }

    // 再次尝试获取读锁 与tryAcquireShared()类似,区别是自旋获取
    return fullTryAcquireShared(current);
}

5.读锁解锁

5.1 函数调用链

读锁解锁过程的函数调用链如下:

// AQS#releaseShared
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        // 锁释放成功后唤醒后续节点
        doReleaseShared();
        return true;
    }
    return false;
}

下面详细介绍Sync.tryReleaseShared()

5.2 解锁过程图

Sync.tryReleaseShared()的工作流程图如下:

5.3 详细代码

protected final boolean tryReleaseShared(int unused) {
    // 获取当前线程
    Thread current = Thread.currentThread();

    // 如果当前线程是第一个获取读锁的线程
    if (firstReader == current) {
        // 如果第一个获取读锁的线程持有计数器为1,则将其置为null,表示没有读锁被持有
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            // 否则将计数器减一,表示该线程释放了一个读锁
            firstReaderHoldCount--;
    } else {
        // 如果当前线程不是第一个获取读锁的线程
        HoldCounter rh = cachedHoldCounter;
        // 检查缓存的读锁计数器是否属于当前线程,如果不是,则从readHolds缓存中获取
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        // 获取当前线程持有的读锁数量
        int count = rh.count;
        // 如果持有的读锁数量小于等于1
        if (count <= 1) {
            // 从readHolds缓存中移除当前线程的读锁计数器
            readHolds.remove();
            // 如果持有的读锁数量小于等于0,表示读锁没有被正确获取过,抛出异常
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        // 将计数器减一,表示当前线程释放了一个读锁
        --rh.count;
    }

    // 尝试释放读锁
    for (;;) {
        // 获取当前状态
        int c = getState();
        // 计算释放一个读锁后的状态
        int nextc = c - SHARED_UNIT;
        // 使用CAS操作尝试更新状态为nextc
        if (compareAndSetState(c, nextc))
            // 释放读锁对读线程没有影响,但它可能会让等待的写线程继续执行
            return nextc == 0;
    }
}

五、总结

ReentrantLock和ReentrantReadWriteLock都是可重入锁,支持公平与非公平锁,主要区别在于ReentrantLock是独占锁,而ReentrantReadWriteLock是读写锁。

ReentrantLock:

  • 特性:
    • 独占锁:ReentrantLock是一种独占锁,同一时刻只能有一个线程持有该锁。
    • 可重入:同一个线程可以多次获取ReentrantLock而不会被自己阻塞,允许嵌套获取。
    • 公平与非公平:ReentrantLock可以通过构造函数指定是否支持公平锁,即等待时间最长的线程优先获取锁。

ReentrantReadWriteLock:

  • 特性:
    • 读写锁:ReentrantReadWriteLock是一种读写锁,允许多个线程同时读取共享资源,但在写操作时只允许一个线程独占。
    • 可重入:与ReentrantLock一样,ReentrantReadWriteLock也是可重入的。
    • 公平与非公平:ReentrantReadWriteLock也支持公平和非公平两种获取锁的方式。

在实际应用中,如果读操作远远多于写操作,使用ReentrantReadWriteLock可以提高并发性能。

因为读锁之间是不互斥的,允许多个线程同时读取,而写锁是独占的,只有在没有读取者和写入者时才能获取。

这种读写分离的设计允许多个读取线程并发执行,提高了并发读取的效率,从而提高了整体性能。

但是在写操作较多的情况下,可能会因为写锁争用而导致性能下降。