ReetrantReadWriteLock底层原理

文章目录

一、读写锁介绍

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁(读多写少)。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源(读读可以并发);但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了(读写,写读,写写互斥)。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了 一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁。

线程进入读锁的前提条件:

  • 没有其它线程的写锁
  • 没有写请求或者有写请求,但是调用线程和持有锁的线程是同一个

线程进入写锁的前提条件:

  • 没有其他线程的读锁
  • 没有其他线程的写锁

读写锁有以下三个重要的特性:

  • 公平选择性:支持非公平和公平的锁获取方式,吞吐量还是非公平优于公平
  • 可重入:读锁和写锁都支持线程重入,以读写线程为例,读锁获取读锁后,能够再次获取读锁,写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
  • 锁降级:遵循获取写锁,再获取读锁最后释放写锁的次序,写锁能够降级为读锁。

二、ReentrantReadWriteLock底层原理

看源码需要了解三个核心问题

  1. 读写锁是怎样实现分别记录读写状态的?
  2. 写锁时怎么获取和释放的?
  3. 读锁时怎么获取和释放的?

1. 读写锁的设计

首先看它的类信息:

java 复制代码
public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {

}

可以发现该类实现了ReadWriteLock这个接口

java 复制代码
public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

该接口就实现了读锁和写锁的规范,以下就是相关的类图

下面看看ReentrantReadWriteLock的读写锁的实现逻辑,首先看写锁:

java 复制代码
public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        private final Sync sync;
        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
        public void lock() {
            sync.acquireShared(1);
        }
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireSharedInterruptibly(1);
        }
        public boolean tryLock() {
            return sync.tryReadLock();
        }
        public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
        }
        public void unlock() {
            sync.releaseShared(1);
        }
        public Condition newCondition() {
            throw new UnsupportedOperationException();
        }
        public String toString() {
            int r = sync.getReadLockCount();
            return super.toString() +
                "[Read locks = " + r + "]";
        }
    }
  • ReadLock:是一个ReetrantReadWriteLock的静态内部类
  • Sync:和ReentrantLock一样,Sync也是ReetrantReadWriteLock的一个静态内部抽象类,它继承了AbstractQueuedSynchronizer,实现了AQS的逻辑,然后它会有两种实现,分别是FairSync公平锁,和NonfairSync非公平锁
  • lock:可以发现加读锁加的是AQS的共享锁
  • tryLock:尝试获取读锁

然后看看写锁是怎么实现的

java 复制代码
 public static class WriteLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -4992448646407690164L;
        private final Sync sync;
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
        public void lock() {
            sync.acquire(1);
        }
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }
        public boolean tryLock( ) {
            return sync.tryWriteLock();
        }
        public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }
        public void unlock() {
            sync.release(1);
        }
        public Condition newCondition() {
            return sync.newCondition();
        }
        public String toString() {
            Thread o = sync.getOwner();
            return super.toString() + ((o == null) ?
                                       "[Unlocked]" :
                                       "[Locked by thread " + o.getName() + "]");
        }
        public boolean isHeldByCurrentThread() {
            return sync.isHeldExclusively();
        }
        public int getHoldCount() {
            return sync.getWriteHoldCount();
        }
    }

下面我们需要思考一个核心问题,读写锁的状态是怎么用AQS底层的state状态来维护的。其实这里的核心问题就是,如何用一个变量维护多种状态。在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用"按位切割使用"的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。分割之后,读写锁是如何迅速确定读锁和写锁的状态呢? 其实底层是通过位运算来实现的。假如当前同步状态为S, 那么写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1。读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于 S+(1<<16),也就是S+0x00010000。根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。

源码如下:

java 复制代码
//该部分在Sync类中
        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; }
  • exclusiveCount:获得持有写状态的锁的次数
  • sharedCount:获得持有读状态的锁的线程数量,不同于写锁,读锁利用同时被多个线程持有,而每个线程持有的读锁支持重入的特性,所以需要每个线程持有的读锁数量单独计数,这就需要HoldCounter计数器。

HoldCounter计数器

读锁的内在机制其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter 计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。

java 复制代码
static final class HoldCounter {
            int count = 0;
            // Use id, not reference, to avoid garbage retention
            final long tid = getThreadId(Thread.currentThread());
        }
  static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

写锁的获取

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

java 复制代码
protected final boolean tryAcquire(int acquires) {
//获取当前线程
            Thread current = Thread.currentThread();
            //获取当前state的值
            int c = getState();
            //获取写锁的重入次数
            int w = exclusiveCount(c);
            //state!=0则表示当前有写锁或读锁
            if (c != 0) {
                 //判断是否是重入
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //获取重入锁
                setState(c + acquires);
                return true;
            }
            // writerShouldBlock有公平与非公平的实现, 非公平返回false,会尝试通过cas加锁,c==0 写锁未被任何线程获取,当前线程是否阻塞或者cas尝试获取锁
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
                //设置锁由当前线程独占
            setExclusiveOwnerThread(current);
            return true;
        }

上面简单的代码就实现了下面的逻辑:

  • 读写互斥
  • 写写互斥
  • 写锁支持同一个线程重入
  • writeShouldBlock写锁是否阻塞实现取决公平与非公平的策略

写锁的释放

java 复制代码
     protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
                //设置状态
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            //如果state为0,表示释放写锁
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }


读锁的获取

java 复制代码
protected final int tryAcquireShared(int unused) {
			//获取当前线程
            Thread current = Thread.currentThread();
            //获取state
            int c = getState();
            //exclusiveCount(c) != 0 判断是否有写锁
            //getExclusiveOwnerThread() != current),判断当前线程是否是写锁的持有者
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                //cas加读锁
                compareAndSetState(c, c + SHARED_UNIT)) {
                //r==0表示第一次获取读锁
                if (r == 0) {
                //设置第一个读为当前线程
                    firstReader = current;
                    //设置当前读锁的重入次数
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {          //第一个读的重入
                    firstReaderHoldCount++;
                } else {
                //若不是第一个读,则用HoldCounter记录
                    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);
            //第一次读锁获取失败,再次尝试(fullTryAcquireShared)
        }

上面代码的逻辑就实现了:

  • 读锁共享,读读不互斥
  • 读锁可重入,每个获取读锁的线程都会记录对应的重入数
  • 读写互斥,锁降级场景除外
  • 支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放
  • readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)


读锁的释放

java 复制代码
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;
            }
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
相关推荐
-seventy-5 分钟前
Java Web 工程全貌
java
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ10 分钟前
idea 删除本地分支后,弹窗 delete tracked brank
java·ide·intellij-idea
言慢行善11 分钟前
idea出现的问题
java·ide·intellij-idea
杨荧22 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的宠物咖啡馆平台
java·开发语言·jvm·vue.js·spring boot·spring cloud·开源
Ling_suu1 小时前
Spring——单元测试
java·spring·单元测试
ModelBulider1 小时前
十三、注解配置SpringMVC
java·开发语言·数据库·sql·mysql
苹果酱05671 小时前
C语言 char 字符串 - C语言零基础入门教程
java·开发语言·spring boot·mysql·中间件
csucoderlee1 小时前
eclipse mat leak suspects report和 component report的区别
java·ide·eclipse
代码小鑫1 小时前
A032-基于Spring Boot的健康医院门诊在线挂号系统
java·开发语言·spring boot·后端·spring·毕业设计
训山2 小时前
4000字浅谈Java网络编程
java·开发语言·网络