从 ReentrantReadWriteLock 说独享锁(排他锁)与共享锁

阅读前面的文章,我们知道 ReentrantLock 是一个高效的锁,它既能用作公平锁,又能用作非公平锁!但是有了 ReentrantLock 后,为什么还需要 ReentrantReadWriteLock 呢?

那就需要从排他锁和共享锁说起!

独享锁

排他锁,又叫做独享锁。顾名思义就是指该锁一次只能被一个线程所持有。如果线程 T 对数据 A 加上排它锁后,则其他线程不能再对 A 加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK 中的 synchronized 和 JUC 中 Lock 的实现类就是互斥锁。

共享锁

共享锁是指该锁可被多个线程所持有。如果线程 T 对数据 A 加上共享锁后,则其他线程只能对 A 再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

独享锁与共享锁也是通过 AQS 来实现的,通过实现不同的方法,来实现独享或者共享。

下面我们一起来看看 ReentrantReadWriteLock 的源码:

ReentrantReadWriteLock 源码分析ReentrantReadWriteLock 有两把锁:ReadLock 和 WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现 ReadLock 和 WriteLock 是靠内部类 Sync 实现的锁。Sync 是 AQS 的一个子类,这种结构在 CountDownLatch、ReentrantLock、Semaphore 里面也都存在。

在 ReentrantReadWriteLock 里面,读锁和写锁的锁主体都是 Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以 ReentrantReadWriteLock 的并发性相比一般的互斥锁有了很大提升。

那读锁和写锁的具体加锁方式有什么区别呢?在了解源码之前我们需要回顾一下其他知识。
在最开始提及AQS的时候我们也提到了 state 字段(int类型,32位),该字段用来描述有多少线程获持有锁。

在独享锁中这个值通常是 0 或者 1(如果是重入锁的话state值就是重入的次数),在共享锁中 state 就是持有锁的数量。但是在 ReentrantReadWriteLock 中有读、写两把锁,所以需要在一个整型变量 state 上分别描述读锁和写锁的数量(或者也可以叫状态)。于是将 state 变量“按位切割”切分成了两个部分,高 16 位表示读锁状态(读锁个数),低 16 位表示写锁状态(写锁个数)。如下图所示:

ReentrantReadWriteLock 读写锁的高低16位

明白了这个之后,我们再来看写锁的加锁源码:

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState(); // 取到当前锁的个数
    int w = exclusiveCount(c); // 取写锁的个数w
    if (c != 0) { // 如果已经有线程持有了锁(c!=0)
        // (Note: if c != 0 and w == 0 then shared count != 0)
        // 如果写线程数(w)为0(换言之存在读锁) 或者持有锁的线程不是当前线程就返回失败
        if (w == 0 || current != getExclusiveOwnerThread()) 
            return false;
        // 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
        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)) 
    // 如果当且写线程数为0,并且当前线程需要阻塞那么就返回失败;或者如果通过CAS增加写线程数失败也返回失败。
        return false;
    // 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者
    setExclusiveOwnerThread(current); 
    return true;
}
  • 这段代码首先取到当前锁的个数 c,然后再通过c来获取写锁的个数 w。因为写锁是低 16 位,所以取低16位的最大值与当前的 c 做与运算( int w = exclusiveCount(c); ),高 16 位和 0 与运算后是 0,剩下的就是低位运算的值,同时也是持有写锁的线程数目。
  • 在取到写锁线程的数目后,首先判断是否已经有线程持有了锁。如果已经有线程持有了锁(c!=0),则查看当前写锁线程的数目,如果写线程数为0(即此时存在读锁)或者持有锁的线程不是当前线程就返回失败(涉及到公平锁和非公平锁的实现)。
  • 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个 Error。
  • 如果当且写线程数为0(那么读线程也应该为0,因为上面已经处理 c!=0 的情况),并且当前线程需要阻塞那么就返回失败;如果通过 CAS 增加写线程数失败也返回失败。
  • 如果 c=0,w=0 或者 c>0,w>0(重入),则设置当前线程或锁的拥有者,返回成功!

tryAcquire() 除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:必须确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。

因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。写锁的释放与 ReentrantLock 的释放过程基本类似,每次释放均减少写状态,当写状态为 0 时表示写锁已被释放,然后等待的读写线程才能够继续访问读写锁,同时前次写线程的修改对后续的读写线程可见。

接着是读锁的代码:

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

可以看到在 tryAcquireShared(int unused) 方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠 CAS 保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。

此时,我们再回头看一下互斥锁 ReentrantLock 中公平锁和非公平锁的加锁源码:

互斥锁ReentrantLock中公平锁和非公平锁的加锁源码

我们发现在 ReentrantLock 虽然有公平锁和非公平锁两种,但是它们添加的都是独享锁。根据源码所示,当某一个线程调用 lock 方法获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用 CAS 更新 state 成功后就会成功抢占该资源。而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。所以可以确定 ReentrantLock 无论读操作还是写操作,添加的锁都是都是独享锁。

Java 中的各种带有锁的原子类

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整型数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。
  • AtomicReference :原子更新引用类型
  • AtomicReferenceFieldUpdater :原子更新引用类型里的字段
  • AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和应用类型
  • AtomicIntegerArray :原子更新整型数组里的元素
  • AtomicLongArray :原子更新长整型数组里的元素
  • AtomicReferenceArray : 原子更新引用类型数组的元素
  • AtomicBooleanArray :原子更新布尔类型数组的元素
  • AtomicBoolean :原子更新布尔类型
  • AtomicInteger: 原子更新整型
  • AtomicLong: 原子更新长整型

好吧,关于锁,我暂时就先写到这里。Java 中还有很多各种各样的锁实现类。以后,我遇到具体的,在给大家一一的写!

从 ReentrantReadWriteLock 说独享锁(排他锁)与共享锁

: » 从 ReentrantReadWriteLock 说独享锁(排他锁)与共享锁

原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/251933.html

(0)
上一篇 2022年5月3日
下一篇 2022年5月3日

相关推荐

发表回复

登录后才能评论