J.U.C|读-写锁ReentrantReadWriteLock

一、写在前面


在上篇我们聊到了可重入锁(排它锁)ReentrantLcok ,具体参见《J.U.C|可重入锁ReentrantLock》

ReentrantLcok 属于排它锁,本章我们再来一起聊聊另一个我们工作中出镜率很高的读-写锁。

二、简介


重入锁ReentrantLock是排他锁(互斥锁),排他锁在同一时刻仅有一个线程可访问,但是在大多数场景下,大部分时间都是提供读服务的,而写服务占用极少的时间,然而读服务不存在数据竞争的问题,如果一个线程在读时禁止其他线程读势必会降低性能。所以就有了读写锁。

读写锁内部维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般排他锁有着显著的提升。

读写锁在同一时间可以允许多个读线程同时访问,但是写线程访问时,所有的读线程和写线程都会阻塞。

主要有以下特征:

  • 公平性选择:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。

  • 重进入:该锁支持重进入,以读写线程为列,读线程在获取到读锁之后,能再次获取读锁。而写线程在获取写锁后能够再次获取写锁,同时也可以获取读锁。

  • 锁降级:遵循获取写锁、读锁再释放写锁的次序,写锁能够降级成为读锁。

读写锁最多支持65535个递归写入锁和65535个递归读取锁。 锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁 读写锁ReentrantReadWriteLock实现接口ReadWriteLock,该接口维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。

读-写锁描述.jpg

三、主要方法介绍


读写锁ReentrantReadWriteLock 实现了ReadWriteLock 接口,该接口维护一对相关的锁即读锁和写锁。

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

ReadWriteLock定义了两个方法。readLock()返回用于读操作的锁,writeLock()返回用于写操作的锁。ReentrantReadWriteLock定义如下:

/** 内部类 读锁*/
private final ReentrantReadWriteLock.ReadLock readerLock;
 /** 内部类 写锁*/
private final ReentrantReadWriteLock.WriteLock writerLock;
/** 执行所有同步机制 */
final Sync sync;

// 默认实现非公平锁
public ReentrantReadWriteLock() {
    this(false);
}
// 利用给定的公平策略初始化ReentrantReadWriteLock
public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
 }
 
 // 返回写锁
 public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
 //返回读锁
 public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
 
 // 实现同步器,也是实现锁的核心
 abstract static class Sync extends AbstractQueuedSynchronizer {
    // 省略实现代码
 }
 
 // 公平锁的实现
 static final class FairSync extends Sync {
    // 省略实现代码
 }
 // 非公平锁的实现
 static final class NonfairSync extends Sync {
    // 省略实现代码
 }
 
 // 读锁实现
 public static class ReadLock implements Lock, java.io.Serializable {
    // 省略实现代码
 }
 // 写锁实现
 public static class WriteLock implements Lock, java.io.Serializable {
    // 省略实现代码
 }

ReentrantReadWriteLock 和 ReentrantLock 其实都一样,锁核心都是Sync, 读锁和写锁都是基于Sync来实现的。从这来分析其实ReentrantReadWriteLock就是一个锁,只不过其内部根据不同的场景设计了两个不同的实现方式。其读写锁为两个内部类: ReadLock、WriteLock 都实现了Lock 接口。

读写锁同样依赖自定义同步器来实现同步状态的, 而读写状态就是其自定义同步器的状态。回想ReentantLock 中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁中的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和写线程的状况,而该状态的设计成为关键。

如何在一个整型上维护多种状态,那就一个需要‘按位切割使用’这个变量,读写锁将变量切割成两部分,高16位表示读,低16位表示写。

分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为S,那么写状态等于 S & 0x0000FFFF(将高16位全部抹去),读状态等于S >>> 16(无符号补0右移16位)。代码如下:

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

/** Returns the number of shared holds represented in count  */
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count  */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

四、写锁的获取与释放


写锁是一个支持重入的排他锁,如果当前线程已经获取了写锁,则增加写状态。如果当前线程获取写锁时读锁已经被获取或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。

  • 写锁的获取
    写锁的获取入口通过WriteLock的lock方法
public void lock() {
   sync.acquire(1);
}

Sync的acquire(1)方法 定义在AQS中

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

tryAcquire(arg) 方法除了重入方法外,还增加了是否存在读锁的判断,如果读锁存在、则不能获取写锁。原因在于写操作要对所有的读操作的可见性。

protected final boolean tryAcquire(int acquires) {
            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)
                /**
                 * w == 0 表示存在读锁(同步状态不等于0说明已有线程获取到锁(读/写)
                 * 而写锁状态为0则说明不存在写锁,所以只能是读锁了)
                 * current != getExclusiveOwnerThread()) 不是自己获取的写锁
                 * 
                 * 如果存在读锁或者持有写锁的线程不是自己,直接返回false
                 */
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                    
                 // 如果获取写锁的数量超过最大值65535 ,直接异常
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                // 设置共享状态
                setState(c + acquires);
                return true;
            }
            /**
             * writerShouldBlock() 是否需要阻塞写锁,这里直接返回的是false
             *compareAndSetState(c, c + acquires) 设置写锁的状态
             * /
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
             // 设置持有锁的线程为自己
            setExclusiveOwnerThread(current);
            return true;
        }
小结

写锁的获取基本和RenntrantLock 类似
判断当前是否有线程持有写锁(写锁的状态是否为0)
写锁的状态不为0,如果存在读锁或者写锁不是自己持有则直接返回fasle。
判断申请写锁数量是否超标(> 65535),超标则直接异常,反之则设置共享状态。
写锁状态为0,如果写锁需要阻塞或者CAS设置共享状态失败,则直接返回false,否则获取锁成功,设置持锁线程为自己。

来张图加深下理解

读写锁-写锁获取流程图.jpg
  • 写锁的释放

写锁的释放和ReentrantLock 极为相似, 每次释放就是状态减1 ,当状态为0表示释放成功。

写锁释放的入口WriteLock中的unlock方法

public void unlock() {
      sync.release(1)
 }

Sync 中release方法由AQS中实现的

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease(arg) 方法释放共享状态,非常简单就是共享状态减1,为0表示释放成功

protected final boolean tryRelease(int releases) {
            // 判断锁持有者是否是自己
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
             // 共享状态值 - release
            int nextc = getState() - releases;
            // 判断写锁数量是否为0
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }
小结

写锁的释放很简单

  • 首先判断锁持有者不是自己则直接异常
  • 是自己则将共享状态 -1
  • 判断写锁数量是否为0,如果为0将持有锁线程变量设为null
  • 设置共享状态

来张图加深下理解

读写锁-写锁释放流程图.jpg

五、读锁的获取与释放


读锁为一个可重入的共享锁,它能够被多个线程同时持有,在没有其他写线程访问时,读锁总是或获取成功,锁需要的也就是(线程安全的)增加读状态。

  • 读锁的获取

读锁的获取可以通过ReadLock.lock()方法。

public void lock() {
    //读锁是一个可重入共享锁,委托给内部类Sync实现
     sync.acquireShared(1);
}

Sync的acquireShared(1)方法定义在AQS中

public final void acquireShared(int arg) {
        // AQS 中 尝试获取共享状态,如果共享状态大于等于0则说明获取锁成功,否则加入同步队列。
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

tryAcquireShared(int unused)方法中,如果其他线程获取了写锁,则读锁获取失败线程将进入等待状态,如果当前线程获取写锁或者写锁未被获取则利用CAS(线程安全的)增加同步状态,成功则获取锁。

protected final int tryAcquireShared(int unused) {
            
            Thread current = Thread.currentThread();
            // 获取共享状态
            int c = getState();
            // 判断是否有写锁 && 持有写锁的线程是否是自己,为true直接返回-1 
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
             //获取共享资源的数量
            int r = sharedCount(c);
           
           /**
              * readerShouldBlock():判断锁是否需要等待(公平锁原则)
              * r < MAX_COUNT:判断锁的数量是否超过最大值65535
              * compareAndSetState(c, c + SHARED_UNIT): 设置共享状态(读锁状态)
              */
              
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                // r==0 :当前没有任何线程获取读锁
                if (r == 0) {
                    // 设置当前线程为第一个获取读锁的线程
                    firstReader = current;
                    // 计数设置为1
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    // 表示重入锁,在计数其上+1
                    firstReaderHoldCount++;
                } else {
                    
                    /**
                     *  HoldCounter 主要是一个类来记录线程获取锁的数量
                     *  cachedHoldCounter 缓存的是最后一个获取锁线程的HoldCounter对象
                     */
                     
                    HoldCounter rh = cachedHoldCounter;
                    // 如果缓存不存在,或者线程不是自己
                    if (rh == null || rh.tid != getThreadId(current))
                        // 从当前线程本地变量ThreadLocalHoldCounter 中获取HoldCounter 并赋值给 cachedHoldCounter, rh
                        cachedHoldCounter = rh = readHolds.get();
                     // 如果缓存的HoldCounter 是当前的线程的,且计数为0 
                    else if (rh.count == 0)
                        // 将rh 存到ThreadLocalHoldCounter 中,将计数+1 
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            /**
             * 进入fullTryAcquireShared(current) 条件
             * 1: readerShouldBlock()  = true  
             * 2: r < MAX_COUNT = false  读锁达到最大
             * 3: 设置共享状态失败
            return fullTryAcquireShared(current);
        }

NonfairSync 中的 readerShouldBlock() 方法判断当前申请读锁的线程是否需要阻塞

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

apparentlyFirstQueuedIsExclusive() 判断同步队列中老二节点是否是独占式(获取写锁请求)是返回ture 否则返回false

final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        // 主要条件判断下一个节点是否是获取写锁线程在排队
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }

自旋来获取读锁,个人感觉对tryAcquireShared(int unused) 方法获取读锁失败的一种补救,其实现逻辑基本相同。

final int fullTryAcquireShared(Thread current) {
            // 线程内部计数器
            HoldCounter rh = null;
            // 自旋
            for (;;) {
                // 获取共享状态
                int c = getState();
               
               /**
                 * exclusiveCount(c) !=0:存在独占锁(写锁)
                 * getExclusiveOwnerThread() != current 判断是否是自己持有写锁
                 * 再次是写锁是否是自己
                 */
                 
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                   
                } else if (readerShouldBlock()) {//判断读锁是否需要阻塞
                    // 如果需要阻塞,表示除了当前线程持有写锁外,还有其他线程在等待获取写锁,故,即使申请读锁的线程已经持有写锁(写锁内部再次申请读锁,俗称锁降级)还是会失败,因为有其他线程也在申请写锁,此时,只能结束本次申请读锁的请求,转而去排队,否则,将造成死锁
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                        // 到这里其实就写锁的一个让步, 清楚HoldCounter 缓存
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
                // 下面逻辑和tryAcquireShared(int unused) 基本相同不再解释了
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }
小结

读锁的获取稍微有点复杂,整个过程如下

  • 如果其他线程获取了写锁、则获取读锁失败。
  • 如果当前线程获取到了写锁或者写锁未被获取则利用CAS(线程安全的)增加读锁状态
  • 否则 fullTryAcquireShared(Thread current) 自旋方式再次来尝试获取。

读锁获取流程图如下

读写锁-读锁获取同步状态流程.jpg
  • 读锁的释放

读锁的释放通过ReadLock的unlock()方式释放的。

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

Sync的releaseShared(1)同样定义在AQS中

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

调用tryReleaseShared(int unused) 方法来释放共享状态。

protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            //判断当前线程释放是第一个获取读锁的线程
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                // 判断获取锁的次数释放为1,如果为1说明没有重入情况,直接释放firstReader = null;否则将该线程持有锁的数量 -1
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                // 如果当前线程不是第一个获取读锁的线程。
                
                // 获取缓存中的HoldCounter
                HoldCounter rh = cachedHoldCounter;
                // 如果缓存中的HoldCounter 不属于当前线程则获取当前线程的HoldCounter。
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    // 如果线程持有锁的数量小于等1 直接删除HoldCounter
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                // 持有锁数量大于1 则执行 - 1操作
                --rh.count;
            }
            // 自旋释放同步状态
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                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;
            }
        }
小结

锁的释放比较简单,

首先看当前线程是否是第一个获取读锁的线程,如果是并且没有发生重入,则将首次获取读锁变量设为null, 如果发生重入,则将首次获取读锁计数器 -1

其次 查看缓存中计数器是否为空或者是否是当前线程,如果为空或者不是则获取当前线程的计数器,如果计数器个数小于等1, 从ThreadLocl 中删除计数器,并计数器值-1,如果小于等于0异常 。

最后自旋修改同步状态。

读锁释放流程图如下


读写锁-读线程释放锁流程.jpg

总结


通过上面的源码分析,我们来总结下:

在线程持有读锁的情况下,该线程不能取得写锁(为了保证写操作对后续所有的读操作保持可见性)。

在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。

仔细想想,这个设计是合理的:因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读

一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。

因技术水平有限,如有不对的地方,欢迎拍砖

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容