独占锁 ReentrantLock、读写锁 ReentrantReadWriteLock。


文章目录

  • 一、独占锁 ReentrantLock
  • 1、类图结构
  • 2、获取锁
  • (1)void lock() 方法
  • 非公平锁
  • 公平锁
  • (2)void lockInterruptibly() 方法
  • (3)boolean tryLock() 方法
  • (4)tryLock(long timeout, TimeUnit unit) 方法
  • 3、释放锁
  • void unlock() 方法
  • 二、读写锁 ReentrantReadWriteLock
  • 1、类图
  • 2、写锁的获取与释放
  • (1)void lock()
  • (2)lockInterruptibly()
  • (3)tryLock()
  • (4)tryLock(long timeout, TimeUnit unit)
  • (5)unlock()
  • 3、读锁的获取与释放
  • (1)void lock()
  • (2) lockInterruptibly()
  • (3)tryLock(long timeout, TimeUnit unit)
  • (4)tryLock(long timeout, TimeUnit unit)
  • (5)unlock()
  • 三、JDK 8 新增的 StampedLock 锁


一、独占锁 ReentrantLock

    ReentrantLock 是可重入锁,同时只能由一个线程可以获取该锁,其他获取该锁的线程会被阻塞而放入该锁的 AQS 阻塞队列中。

1、类图结构

iceberg java 读写 java 读写锁实现原理_公平锁

    从类图可以看到,ReentrantLock 最终还是使用 AQS 来实现的,并且根据参数来决定其内部是一个公平锁 还是 非公平锁,默认是非公平锁。
源码:

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
public ReentrantLock() {
        sync = new NonfairSync();
    }

    ✨其中 Sync 类继承自 AQS,它的子类 NonfairSync 和 FairSync 分别实现了获取锁的 非公平 与 公平策略。在这里,AQS 的 state 状态值表示线程获取该锁的可重入次数,在默认情况下,state 的值为 0 表示 当前锁没有被任何线程持有。当一个线程 第一次获取该锁时,会尝试使用 CAS 设置 state 的值为 1,如果 CAS 成功,则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没有释放锁的情况下,第二次获取该锁后,状态值 +1,这就是可重入次数。在该线程释放该锁时,会尝试使用 CAS 让状态值 -1,如果 -1 后状态值为 0,则当前线程释放该锁。✨
    

2、获取锁
(1)void lock() 方法

    当一个线程调用该方法,说明该线程希望获取该锁。如果锁当前没有被其他线程占用 并且 当前线程之前没有获取过该锁,则 当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为 1·,然后直接返回。如果当前线程之前已经获取过该锁,则 这次只是简单地把 AQS 的状态值 加1 后返回。如果该锁已经被其他线程持有,则调用该方法的线程会被放入 AQS 队列后阻塞挂起。
源码:

public void lock() {
        sync.lock();
    }

    根据创建 ReentrantLock 构造方法选择 sync 的实现是 类 NonfairSync 还是 FairSync,这个锁是 非公平锁 或者 公平锁。

非公平锁

     非公平锁是说 先尝试获取锁的线程 不一定比后尝试获取锁的线程 优先获取锁。
源码:

final void lock() {

			// CAS 设置状态值
            if (compareAndSetState(0, 1))

				// 设置锁持有者是当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else

				// (一)调用 AQS 的 acquire 方法
                acquire(1);
        }

    默认 AQS 状态值是 0,所以第一个调用 lock 方法的线程会通过 CAS 设置状态值为 1, CAS 成功则表示当前线程获取到了锁。锁被第一个线程释放掉之前,如果其他线程调用 lock 方法企图获取该锁, CAS 会失败,就会调用 acquire() 方法,传入的参数是 1。
(一):

public final void acquire(int arg) {

		// (二)
        if (!tryAcquire(arg) &&

			// 把当前线程放入 AQS 阻塞队列
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

     注意注释里有一句:This method can be used to implement method {@link Lock#lock}. 译: acquire 方法可以用来实现 lock() 方法。
(二):

protected final boolean tryAcquire(int acquires) {

			// (三)
            return nonfairTryAcquire(acquires);
        }
    }

(三):

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();

			// 如果当前锁的状态值是 0,说明当前锁空闲
			// 这个是很有必要的,因为可能这个线程先 lock 了,但是不持有锁
			// 持有锁的线程释放锁后,这个线程有机会竞争的
			// 应当再判断一次状态值是否为 0 的。
            if (c == 0) {

				// 尝试 CAS 获取锁,将状态值从 0 设置为 1
                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;
            }

			// 如果当前线程不是锁的持有者,则返回 false
            return false;
        }

    假设 线程 A 调用 lock() 方法执行到 nonfairTryAcquire 的代码,发现当前状态值不为 0,而且当前线程也不是锁持有者,就返回 false,然后当前线程被放入 AQS 阻塞队列。这时 线程 B 也调用了 lock() 方法执行到 nonfairTryAcquire 的代码,发现当前状态值为 0 (假设占有该锁的其他线程释放了该锁),所以通过 CAS 设置获取到了该锁。明明是 线程 A 先请求获取该锁的呀,这就是非公平的体现。 这里线程 B 在获取锁前并没有查看当前 AQS 队列里是否有比自己更早请求该锁的线程,而是使用了抢夺策略
    

公平锁

源码:

protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                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;
    }

    公平的 tryAcquire 与 非公平的类似,不同之处在于,在设置 CAS 之前,多了 hasQueuedPredecessors() 方法 ,该方法是实现公平性的核心代码,代码如下:

public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        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());
    }

    ✨可以看到,如果当前线程节点有前驱节点则直接返回 true,否则,如果当前 AQS 队列为空 或者 当前线程节点是 AQS 的第一个节点 则返回 false。其中,如果 h == t 说明当前队列为空,直接返回 false;如果 h != t && s == null 则说明有一个元素将要作为 AQS 的第一个节点入队列(enq 方法的第一个元素入队列是两步操作:首先创建一个哨兵头节点,然后将第一个元素插入哨兵节点后面),那么返回 true,如果 h != t 并且 s != nulls.thread != Thread.currentThread() 则说明 队列里面的第一个元素不是当前线程,那么返回 true。✨

(2)void lockInterruptibly() 方法

    该方法与 lock() 方法类似,它的不同在于,它对中断进行响应,就是当前线程在调用该方法时,如果其他线程调用了当前线程的 interupt() 方法,则当前线程会抛出 InterruptedException 异常,然后返回。
源码:

public void lockInterruptibly() throws InterruptedException {
 		// (一)
        sync.acquireInterruptibly(1);
    }

(一):

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))

			// 调用 AQS 可被中断的方法
            doAcquireInterruptibly(arg);
    }

    

(3)boolean tryLock() 方法

    尝试获取锁,如果当前该锁没有被其他线程持有,则 当前线程获取该锁并返回 true,否则返回 false,注意:该方法不会引起当前线程阻塞。

public boolean tryLock() {
		// (一)
        return sync.nonfairTryAcquire(1);
    }

(一):

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;
}

    可以看到,该方法与 非公平锁的 tryAcquire() 方法的代码类似,所以 tryLock() 使用的是非公平策略
    

(4)tryLock(long timeout, TimeUnit unit) 方法

    该方法尝试获取锁,与 tryLock() 不同之处在于,它设置了超时时间,如果超时时间到,没有获取到锁,则返回 false。
源码:

public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

    

3、释放锁
void unlock() 方法

    

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

(一):

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;

	// 如果不是锁持有者调用 unlock 则抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;

	// 如果当前可重入次数为 0,则清空锁持有线程
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }

	// 设置可重入次数为原始值 -1
    setState(c);
    return free;
}

    
例👀:使用 ReentrantLock 来实现一个简单的线程安全的 list。

import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockList {
    private ArrayList<String> arrayLIst=new ArrayList<>();
    
    // 独占锁
    private volatile ReentrantLock lock=new ReentrantLock();
    
    // 添加元素
    public void add(String e){
        lock.lock();
        try{
            arrayLIst.add(e);
        }finally {
            lock.unlock();
        }
    }
    
    // 删除元素
    public void remove(String e){
        lock.lock();
        try{
            arrayLIst.remove(e);
        }finally {
            lock.unlock();
        }
    }
    
    // 获取元素
    public String get(int index){
        lock.lock();
        try{return arrayLIst.get(index) ;}
        finally {
            lock.unlock();
        } 
    }
}

(就是给几个方法的前后加锁、释放锁,过于简单了😿。)
    可以看到,通过在操作 array 元素前进行加锁保证同一时间只有一个线程可以对 array 数组进行修改,但是也只能有一个线程对 array 元素进行访问。
    
🎭总结
    ReentrantLock 底层是使用 AQS 实现的可重入独占锁。AQS 状态值为 0 表示当前锁空闲,大于等于 1 说明该锁已经被占用。该锁内部有公平锁 和 非公平锁,默认情况下是 非公平的实现。而且该锁是独占锁,所以,某时只有一个线程可以获取该锁。


    解决线程安全问题使用 ReentrantLock 就可以,但是 ReentrantLock 是独占锁,某时只有一个线程可以获取该锁,而实际上 会有 写少读多 的场景,ReentrantReadWriteLock 采用读写分离的策略,允许多个线程同时获取读锁。


二、读写锁 ReentrantReadWriteLock

1、类图

iceberg java 读写 java 读写锁实现原理_StampedLock_02

    ✨读写锁的内部维护了 ReadLock 和 WriteLock,它们依赖 Sync 实现具体功能,而 Sync 继承自 AQS,并且也提供了 公平 与 非公平 的实现。
AQS 中的 state 值,它的高 16 位表示读状态,也就是 获取到读锁的次数;它的低 16 位表示获取到写锁的线程的可重入次数。✨

源码:

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 6317671515068378041L;

        static final int SHARED_SHIFT   = 16;

		// 共享锁(读锁)状态单位值为 65536
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);

		// 共享锁线程最大个数 65535
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;

		// 排他锁(写锁)掩码,二进制,15 个 1
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

       // 返回读锁线程数
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        
        // 返回写锁可重入个数
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

    ReentrantReadWriteLock 中的 firstReader 用来记录第一个读取到读锁的线程,firstReaderHoldCount 记录第一个获取到读锁的线程 获取读锁的可重入次数。cachedHoldCounter 用来记录最后一个获取读锁的线程获取锁的可重入次数,它是 HoldCounter 类型,HoldCounter 源码:

static final class HoldCounter {
            int count = 0;
            // 线程
            final long tid = getThreadId(Thread.currentThread());
        }

    ReentrantReadWriteLock 中 还有 readHolds 变量,是 ThreadLocalHoldCounter 类型,用来存放除去第一个获取读锁线程外的其他线程 获取读锁的可重入次数。ThreadLocalHoldCounter 继承自 ThreadLocal ,源码:

private transient ThreadLocalHoldCounter readHolds;
static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

    

2、写锁的获取与释放

    在 ReentrantReadWriteLock 中,写锁使用 WriteLock 来实现。

(1)void lock()

    写锁是个独占锁, 某时只有一个线程可以获取该锁,如果当前没有线程获取到 读锁 和 写锁,则 当前线程可以获取到写锁 然后返回;如果当前已经有线程获取到读锁 和 写锁,则 当前线程会被阻塞挂起,而且 写锁是可重入锁,如果当前线程已经获取了该锁,再次获取只是简单地把可重入次数 + 1 后直接返回。
源码;

public void lock() {

			// sync 继承自 AQS
            sync.acquire(1);
        }

调用 AQS 的 acquire 方法,就会去调用子类的 tryAcquire 方法,源码:

protected final boolean tryAcquire(int acquires) {
   
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
	
	// 状态值不为 0 说明 读锁 或者 写锁 已经被某线程获取
    if (c != 0) {
    
    	// 状态值不为 0 ,而低 16 位为0,说明高 16 位不为0,即 有线程获取了读锁。直接返回 false
        // (Note: if c != 0 and w == 0 then shared count != 0)
        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;
    }

	// (一)这个分支是第一个写线程获取写锁
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

    
注意这段代码:

if (w == 0 || current != getExclusiveOwnerThread())
            return false;

    可以看到,或逻辑分为两种情况:(1)表示写锁获取次数的 低 16 位为 0,而这时 状态值不为 0,说明已经有线程获取了读锁,而这时写锁获取结果是失败。(这也说明,只要有线程在读,就不能有线程获取写锁。 不可能出现高 16 位 和 低 16 位都不为 0 的情况,因为想要“写”的低 16 位不为 0,就得经写锁的获取,而 w 为 0,就会返回 false 写锁获取失败。)(2)有线程获取写锁,但不是当前线程,写锁获取失败,这是写锁独占性的体现。·
(一):writerShouldBlock() 方法分为 非公平 和 公平 两种实现
非公平:

final boolean writerShouldBlock() {
        return false; // writers can always barge
    }

    说明 非公平锁,是 抢占式,执行之后的 compareAndSetState(c, c + acquires) ,即 CAS 尝试获取写锁,获取成功 则设置当前锁的持有者为当前线程并返回 true,否则返回 false。

公平:

final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }

    说明 公平锁,是通过 判断当前线程节点是否有前驱节点,如果有,则当前线程放弃获取写锁的权限,直接返回 false,来实现公平。

(2)lockInterruptibly()

源码:

public void lockInterruptibly() throws InterruptedException {
            sync.acquireSharedInterruptibly(1);
        }

    它会对中断进行响应,当其他线程调用了该线程的 interrupt() 方法 中断了当前线程时,当前线程会抛出 InterruptedException 异常。
    

(3)tryLock()

    尝试获取写锁,如果当前没有其他线程持有写锁 或 读锁,则 当前线程获取写锁 会成功,然后返回 true。如果当前已经有其他线程持有写锁 或 读锁 则方法直接返回 false,且当前线程并不会被阻塞。 如果当前线程已经持有了该写锁 则 简单增加 AQS 的状态值后 直接返回 true。
源码:

public boolean tryLock( ) {

			// (一)
            return sync.tryWriteLock();
        }

(一):

final boolean tryWriteLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c != 0) {
                int w = exclusiveCount(c);
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
            }
            if (!compareAndSetState(c, c + 1))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

    

(4)tryLock(long timeout, TimeUnit unit)
public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
        }

    与 tryAcquire() 的不同之处在于,多了时间参数,如果尝试获取写锁失败,则会把当前线程挂起指定时间,待超时时间到后 当前线程被激活,如果还是没有获取到写锁,则返回 false。另外,该方法会对中断进行响应,也就是说,当其他线程调用了该线程的 interrupt() 方法 中断了当前线程时,当前线程会抛出 InterruptedException 异常。
    

(5)unlock()

    尝试释放锁,如果当前线程持有该锁,调用该方法会 AQS 的状态值减1,如果 减1 后当前状态值为 0,则当前线程会释放该锁,否则仅仅 减 1而已。如果当前线程没有持有该锁,而调用该方法,会抛出 IllegalMonitorStateException 异常。

public void unlock() {

			// (一)
            sync.release(1);
        }

(一):

public final boolean release(int arg) {

		// (二)
        if (tryRelease(arg)) {

			// 激活阻塞队列里的一个线程
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    注意 release 方法的注释里有一句:This method can be used to implement method {@link Lock#unlock}. 译:release 方法可以用来实现 unlock() 方法。

(二)Sync 里对于 tryRelease 的具体实现:

protected final boolean tryRelease(int releases) {

			// 看是否 写锁拥有者 调用的 unlock  
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();

			// 获取可重入值,这里没有考虑高 16 位,因为获取写锁时,读锁状态值肯定为 0
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;

			// 如果写锁可重入值为 0 则释放锁,否则只是简单更新状态值
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

    

3、读锁的获取与释放

    在 ReentrantReadWriteLock 中,写锁使用 WriteLock 来实现。

(1)void lock()

    获取读锁,如果当前没有线程持有锁,则 当前线程可以获读锁, AQS 的状态值 state 的 高 16 位的值会加 1 ,否则,如果其他一个线程持有写锁,当前线程会阻塞

public void lock() {
  			// (一)
            sync.acquireShared(1);
        }

(一):

public final void acquireShared(int arg) {
		// (二) 
        if (tryAcquireShared(arg) < 0)

			// 把当前线程放入 AQS 阻塞队列
            doAcquireShared(arg);
    }

(二)Sync 里对于 tryAcquireShared 的具体实现:

protected final int tryAcquireShared(int unused) {
 
            Thread current = Thread.currentThread();

			// 获取状态值
            int c = getState();

			// 判断写锁是否被占用
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;

			// 获取读锁计数
            int r = sharedCount(c);

			// 尝试获取锁,多个线程只有一个会成功,不成功的进入 fullTryAcquireShared 进行重试
			//(三)		
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {

				// 第一个线程获取读锁
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;

					   // 如果当前线程是第一个获取读锁的线程
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {

					// 记录最后一个获取读锁的线程或记录其他线程读锁的可重入锁
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

    如果当前要获取读锁的线程已经持有了写锁,则也可以获取读锁。 方法的注释里还说,如果另一个线程持有写锁,那么其他线程不能获取读锁。(想来也是,写的也可以读,没毛病,但是反过来不应当。) 但是要注意,当一个线程先获取了写锁,然后获取了读锁,处理事情完毕后,要记得把读锁 和 写锁 都释放掉,不能只释放写锁。
(三):NonfairSync 中 readerShouldBlock() 的实现

final boolean readerShouldBlock() {
			// (四)
            return apparentlyFirstQueuedIsExclusive();
        }

(四):

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

(五):

final boolean isShared() {
            return nextWaiter == SHARED;
        }

    可以看到,(三)、(四)返回 true 说明,有线程正在获取写锁,(“写” ~ 共享 ~ “SHARED”)。tryAcquireShared 方法中 (三)那块儿的逻辑是,如果队列中存在一个元素,则判断 第一个元素是不是正在尝试获取写锁,如果不是,则当前线程判断当前获取读锁的线程是否达到了最大值,最后执行 CAS 操作将 AQS 状态值的高 16 位增加 1。
    cachedHoldCounter 记录最后一个获取到读锁的线程获取读锁的可重入数,readHolds 记录了当前线程获取读锁的可重入数。
    

(2) lockInterruptibly()

    类似于 lock() 方法,不同在于, 它会对中断进行响应,当其他线程调用了该线程的 interrupt() 方法 中断了当前线程时,当前线程会抛出 InterruptedException 异常。
    

(3)tryLock(long timeout, TimeUnit unit)

    尝试获取读锁,如果当前没有其他线程持有写锁,则 当前线程获取读锁会成功,然后返回 true,如果当前已经有其他线程持有写锁,则 该方法直接返回 false,但当前线程并不会被阻塞。如果当前线程已经持有了该读锁 则 简单增加 AQS 的状态值高 16 位 后直接返回 true 。
    

(4)tryLock(long timeout, TimeUnit unit)

    与 tryAcquire() 的不同之处在于,多了时间参数,如果尝试获取读锁失败,则会把当前线程挂起指定时间,待超时时间到后 当前线程被激活,如果还是没有获取到读锁,则返回 false。另外,该方法会对中断进行响应,也就是说,当其他线程调用了该线程的 interrupt() 方法 中断了当前线程时,当前线程会抛出 InterruptedException 异常。
    

(5)unlock()

    释放锁。
源码:

public void unlock() {
  			// (一)
            sync.releaseShared(1);
        }

(一):

public final boolean releaseShared(int arg) {
		// (二)
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

(二):

protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }

			// 循环直到自己的读计数 -1, CAS 更新成功
            for (;;) {
            
            	// 获取当前状态值
                int c = getState();

				// 状态值减去读计数单位
                int nextc = c - SHARED_UNIT;

				// CAS 更新状态值
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    
                    return nextc == 0;
            }
        }

    在循环中,首先获取状态值,然后状态值减去 读计数单位,使用 CAS 更新状态值,如果更新成功则查看当前状态值是否为 0,为 0 则说明当前已经没有读线程占用读锁啦,然后在 releaseShared 方法中就会调用 doReleaseShared() 方法,释放一个 由于获取写锁而被阻塞的线程(因为有线程在读的时候,不能有线程获取写锁,会被阻塞),如果当前状态值不为 0,则说明当前还有线程持有读锁,所以 tryReleaseShared 返回 false,退出循环,如果 tryReleaseShared 中的 CAS 更新状态值失败,也就是 (compareAndSetState(c, nextc) 返回值是 false,则自旋重试直到成功。

    
👀:ReentrantReadWriteLock 实现线程安全的 list:

import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentranLockList {
    private ArrayList<String> arrayLIst=new ArrayList<>();

    // 独占锁
    private final ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
    private final Lock readLock=lock.readLock();
    private final Lock writeLock=lock.writeLock();
    

    // 添加元素
    public void add(String e){
        writeLock.lock();
        try{
            arrayLIst.add(e);
        }finally {
            writeLock.unlock();
        }
    }

    // 删除元素
    public void remove(String e){
        writeLock.lock();
        try{
            arrayLIst.remove(e);
        }finally {
            writeLock.unlock();
        }
    }

    // 获取元素
    public String get(int index){
        readLock.lock();
        try{return arrayLIst.get(index) ;}
        finally {
            readLock.unlock();
        }
    }
}

    get 方法使用的是读锁,这样多个读线程来同时访问 list 的元素,在读多写少的情况下性能会更好。

    

🎭总结

    读写锁 ReentrantReadWriteLock 的底层是使用 AQS 实现的,用 AQS 状态值的高 16 位表示获取到读锁的个数,低 16 位标识获取到写锁的线程的可重入次数,并通过 CAS 对其操作实现了读写分离,这在读多写少的场景下比较适用。

iceberg java 读写 java 读写锁实现原理_Java并发可重入读写锁_03


    

三、JDK 8 新增的 StampedLock 锁

    该锁提供了三种模式的读写控制,当调用获取锁的方法时,会返回一个 long 类型的变量,称之为 戳记 stamp,这个戳记代表了锁的状态。try 系列获取锁的方法,当获取锁失败后 会返回为 0 的 stamp 值。当调用释放锁 和 转换锁的方法时 需要传入 获取锁时返回的 stamp 值。
    
三种读写模式的锁:

  • 🔒写锁 writeLock
        独占锁。某时只有一个线程可以获取该锁,当一个线程获取该锁后,其他请求 读锁 和 写锁的线程必须等待。也就是说,当前没有线程持有读锁 或者 写锁时,才可以获取到该锁。请求该锁成功后 会返回一个 stamp 变量用来表示该锁的版本。当释放该锁时,需要调用 unlockWrite 方法 并传递读取锁时的 stamp 参数。并且它提供了非阻塞的 tryWriteLock 方法。
        
  • 🔒悲观读锁 readLock:
        共享锁。在没有线程获取独占写锁的情况下,多个线程可以同时获取该锁。如果已有线程持有写锁,则 其他线程请求获取该读锁会被阻塞(类似于 ReentranReadWriteLock 的读锁,不同的是 这里读锁是不可重入锁)。 这里的悲观是说,在具体操作数据前,它会悲观地认为 其他线程可能要对自己操作的数据进行修改,所以需要先对数据加锁,这是在 读少写多 的情况下的一种考虑。请求该锁成功会返回一个 stamp 参数,表示该锁的版本,当释放该锁时,需要调用 unlockRead 方法并传递 stamp 参数。并且它提供了非阻塞的 tryWriteLock 方法。
        
  • 🔒乐观读锁 tryOptimisticRead:
        相对于悲观锁, 数据操作前没有通过 CAS 设置锁的状态,(那么也就不需要显式地释放锁,而且适合于 读多写少 的场景)仅通过位运算测试。如果当前没有线程持有写锁,则 简单地返回一个 非 0 的 stamp 版本信息。获取该 stamp 后 在具体操作数据前,还需要调用 validate 方法验证该 stamp 是否已经不可用。由于没有使用到真的锁,在保证数据一致性上 需要复制一份要操作的变量到方法栈,并且在操作数据时 可能其他写线程已经修改了数据,而 我们操作的是方法栈里的数据,也就是一个快照📸 ,可能返回的不是最新的数据。    
        
        StampedLock 还支持这三种锁在一定条件下进行互相转换,比如 LongConvertToWriteLock(long stamp) 期望把 stamp 标示的锁 升级为写锁,满足以下三个条件,就可以晋升写锁成功,并返回有效的 stamp :
    (1)当前锁已经是写锁模式了。
    (2)当前锁处于读锁模式,并且没有其他线程是读锁模式。
    (3)当前处于乐观读模式,并且当前写锁可用。
        StampedLock 的读写锁都是不可重入锁 ,所以 在获取锁后 释放锁前 不应该再调用会获取锁的操作,以避免造成调用线程被阻塞.。当多个线程同时尝试获取读锁 和 写锁 时,哪个线程先获取到锁是随机的。并且 锁不是直接实现实现 Lock 或 ReadWriteLock 接口,而是其在内部自己维护一个双向阻塞的 队列
        
    👀:管理二维点
import java.util.concurrent.locks.StampedLock;

/**
 * 使用 StampedLock 管理二维点*/
public class Point {
    // 成员变量,表示一个点的二维坐标
    private double x,y;

    // 锁实例,保证操作的原子性
    private final StampedLock stampedLock=new StampedLock();

    /**
     * 使用参数的增量值,改变当前 point 坐标的位置
     * @param deltaX
     * @param deltaY
     */
    void move(double deltaX,double deltaY){
        // 独占锁——写锁
        long stamp=stampedLock.writeLock();
        try{
            x += deltaX;
            y += deltaY;
        }finally {

            // 释放锁
            stampedLock.unlockWrite(stamp);
        }
    }

    /**
     * 计算当前位置到原点的距离
     * @return
     */
    // 乐观读锁
    double distanceFromOrigin(){

        // 尝试获取乐观读锁
        long stamp = stampedLock.tryOptimisticRead();

        // (1)将全部变量复制到方法体栈内
        double currentX = x,currentY = y;

        // (2)检查读取读锁戳记后,锁有没有被其他写线程排他性抢占
        if(!stampedLock.validate(stamp)){

            // 如果被抢占,则 获取一个共享读锁
            stamp = stampedLock.readLock();
            try{
                // 将全部变量复制到方法体栈中
                currentX = x;
                currentY = y;
            }finally {
                // 释放共享读锁
                stampedLock.unlock(stamp);
            }
        }
        // 返回计算结果
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // 使用悲观锁获取读锁,并尝试转换为写锁
    void moveIfAtOrigin(double newX,double newY){
        long stamp = stampedLock.readLock();
        try{
            // 如果当前点在原点,则移动
            while(x == 0.0 && y == 0.0){
                // 尝试将获取的读锁升级为写锁
                long ws=stampedLock.tryConvertToWriteLock(stamp);

                // 升级成功,则更新戳记,并设置坐标值
                if(ws != 0L){
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                }else {
                    // 读锁升级写锁失败,则释放读锁,显式获取独占写锁,然后循环重试
                    stampedLock.unlockRead(stamp);
                    stamp=stampedLock.writeLock();
                }
            }
        }finally {
            // 释放锁
            stampedLock.unlock(stamp);
        }
    }
}

    要注意 (1) 和 (2)的顺序不能互换的。假如交换了,先执行了 validate ,假如 validate 通过了,要复制 x,y 值到本地方法栈,而在复制的过程中有其他线程修改了 x 或 y 的值,那就造成数据的不一致。但是吧,如果不交换,复制x ,y值到本地方法栈时,也可能有其他线程修改 x 或 y 的值, 只是说,复制了之后,还需要再调用 validate 检测一下呢,如果这时有线程修改 x 或 y的值,那么肯定是有线程在调用 validate 之前,调用了 tryOptimisticRead 后获取了写锁,这样进行 validate 时就会失败。


validate 方法源码:

/**
     * Returns true if the lock has not been exclusively acquired
     * since issuance of the given stamp. Always returns false if the
     * stamp is zero. Always returns true if the stamp represents a
     * currently held lock. Invoking this method with a value not
     * obtained from {@link #tryOptimisticRead} or a locking method
     * for this lock has no defined effect or result.
     *
     * @param stamp a stamp
     * @return {@code true} if the lock has not been exclusively acquired
     * since issuance of the given stamp; else false
     */
 public boolean validate(long stamp) {
        U.loadFence();
        return (stamp & SBITS) == (state & SBITS);
    }

    注释译为:
    如果锁没有被独占式获取,就返回 true;
    如果戳记 stamp 值是 0,就返回 false。
    如果戳记 stamp 代表一个当前被持有的锁,就返回 true。
    传入的 stamp 值如果不是从 tryOptimisticRead 或者一个 lock 方法中获取的值,调用这个方法是没有意义的。


    即使 x,y 没有被声明成 volatile ,也能保证内存可见性,主要是因为锁🔒,因为 当前没有线程持有读锁 或者 写锁时,才可以获取到写锁。
    
    使用乐观读锁容易犯错误,必须要保证使用顺序:

// 非阻塞获取版本信息
long stamp = lock.tryOptimisticRead();

// 复制变量到线程本地堆栈
copyVaraibale2ThreadMemory();

// 校验
if(! lock.validate(stamp){

       // 获取读锁
       long stamp = lock.readLock();

            try{
            // 复制变量到线程本地堆栈
	 		copyVaraibale2ThreadMemory();
				}finally{
					// 释放悲观锁
					lock.unlock(stamp);
						}
				}

    

🎭总结:

    StampedLock 提供的读写锁与 ReentrantLock 类似,只是前者提供的是不可重入锁。但是前者通过乐观锁在多线程多读的情况下提供了更好的性能,因为乐观锁不需要进行 CAS 设置锁的状态。

iceberg java 读写 java 读写锁实现原理_ReetrantLock_04