`

java读写锁—ReentrantReadWriteLock

阅读更多

前言

 

ReentrantReadWriteLock从字面上直接翻译过来应该是重入读写锁,首先需要指明的是读写锁本质上是“一个锁”的不同视图。ReentrantReadWriteLock是基于AQS实现的(对AQS的理解可以点击这里文中提到jdk1.8基于AQS直接实现的API有CountDownLatchSemaphoreReentrantLockThreadPoolExecutor

ReentrantReadWriteLock,前面4个已经总结过),这里提到的一个锁是指:不论是读线程、还是写线程都会同“一个锁”阻塞,并在同一个AQS队列中排队。

 

ReentrantReadWriteLock主要是利用读写分离的思想,读取数据使用读锁实现多个读线程可以并行读;写数据使用写锁实现只能由一个线程写入。相对于ReentrantLock,在读多写少的情况下,使用ReentrantReadWriteLock会有更好的性能表现。在《java并发编程实战》中,jdk的大神们用ReentrantReadWriteLock实现了读写线程安全的Map,这里以此为模板实现一个读写线程安全的list

 

/**
 * Created by gantianxing on 2018/1/6.
 */
public class ReadWriteList<E> {
    private final List<E> list;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock r = lock.readLock();
    private Lock w = lock.writeLock();
 
    public ReadWriteList(List<E> list){
        this.list = list;
    }
 
    //获取数据,用读锁
    public E get(int index){
        r.lock();
        try{
            return list.get(index);
        }finally {
            r.unlock();
        }
    }
 
    //写入数据,用写锁
    public void add(E e){
        w.lock();
        try{
            list.add(e);
        }finally {
            w.unlock();
        }
    }
}

 

 

该工具类,可以将任意类型的List转换成“读写线程安全”的ReadWriteList,当然有人说我们可以直接用CopyOnWriteArrayList。但如果如果程序中已经大量使用的ArrayListLinkedList,这时为了尽量少的改动原有代码就可以使用自己定义的 ReadWriteList,在读多写少的场景中比直接使用synchronized或者ReentrantLock性能会好很多。当然你也可以实现一个读写线程安全Map,把这两个工具类放到你的代码中。

 

通过对自己实现ReadWriteList的讲解,相信大家对读写锁ReentrantReadWriteLock的用法已经有了基本的了解。下面来看ReentrantReadWriteLock的具体实现原理。

 

ReentrantReadWriteLock实现原理

 

构造方法

文章开头就提到ReentrantReadWriteLock中的读写锁,本质上是一个锁,那具体是怎么实现的呢?首先从它的两个构造方法就可以看出:

public ReentrantReadWriteLock(boolean fair) {
        //根据条件创建公平、或非公锁
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);//创建读锁
        writerLock = new WriteLock(this);//创建写锁
}
 
//默认依然是非公平锁
public ReentrantReadWriteLock() {
        this(false);
}
 

 

其中不论FairSyncNonfairSync是都对AQS的实现,这里构造方法会根据条件创建“一个”公平或者非公平锁,接着以此锁为参数同时创建 ReadLock(读锁)和WriteLock(写锁)两个视图,也就是说本质上多个排队线程会被同一个锁阻塞在同一个AQS队列上。接着来看是如何对AQS实现的。

 

AQS的实现

ReentrantLock的实现一样,ReentrantReadWriteLock也有公平锁和非公平锁的实现,其内部定义了三个内部类:SyncFairSyncNonfairSync。其中Sync是抽象类 并且直接基础自AQSFairSyncNonfairSync都继承自Sync分别实现公平锁非公平锁

 

我们都知道AQSstate字段只有1个值,如何表述两种锁的状态呢?ReentrantReadWriteLock的具体做法是用一个32位的二进制标识状态state,其中高16位表示持有“读锁”的线程数(多线程共享),低16位表示持有“写锁”线程重入次数(只有一个线程),在内部抽象类Sync中定义了如下几个字段:

        

        /*  //这段注释就是说明高16位,和低16位
         * Read vs write count extraction constants and functions.
         * Lock state is logically divided into two unsigned shorts:
         * The lower one representing the exclusive (writer) lock hold count,
         * and the upper the shared (reader) hold count.
         */
        //以16位为界分割
        static final int SHARED_SHIFT   = 16;
        //相当于对高16位加1,也就是持有读锁的线程数加1
        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;
 
        /** 返回持有读锁的线程数*/
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** 返回写锁重入次数  */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

 

其中sharedCount方法取高16位,返回持有“读锁”的线程数;

exclusiveCount方法取低16位,返回“写锁”重入次数。到这里就解释了,为什么说读锁和写锁其实是同一个锁。

 

通过前几次对CountDownLatchSemaphoreReentrantLock的总结,相信大家对如何使用AQS实现锁都有了一定的认识:要实现排它锁,就重写AQStryAcquiretryRelease方法,比如ReentrantLock的实现;要实现共享锁,就重写AQStryAcquireSharedtryReleaseShared方法,比如CountDownLatchSemaphore的实现。有了这点基础的认识,我们再来看ReentrantReadWriteLock,本质上我们可以把ReentrantReadWriteLock读锁理解为排它锁,ReentrantReadWriteLock写锁理解为共享锁。

 

ReentrantReadWriteLock中对AQS的实现也确实如此,内部抽象类Sync同时实现了tryAcquiretryReleasetryAcquireSharedtryReleaseShared 4个方法(注意文中提到的排它锁就是写锁,共享锁就是读锁,这种称呼仅在ReentrantReadWriteLock的实现中有效):

 

排它锁(写锁)实现

tryAcquire(排它获取方法)

protected final boolean tryAcquire(int acquires) {
        Thread current = Thread.currentThread();
        int c = getState();//获取当前AQS状态(一共32位)
        int w = exclusiveCount(c);//返回排它锁重入次数,也就是低16位
        if (c != 0) {//c不为0,表示有可能是重入独占锁(对应写锁),也有可能是共享锁(对应读锁)
            // (Note: if c != 0 and w == 0 then shared count != 0)
            //如果重入独占锁状态不为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;
        }
        //如果前AQS状态为0,表示可以获取锁
        if (writerShouldBlock() || //writerShouldBlock仅仅用于表示公平和非公平
                !compareAndSetState(c, c + acquires))
            return false;
        setExclusiveOwnerThread(current);//设置排它锁的持有者为当前线程
        return true;
    }

 

结合注释很好理解,始终只有一个线程能获取到排它锁(写锁),本质上就是对AQSstate字段的低16+1WriteLock写锁加锁的完整过程:WriteLocklock方法-->AQSacquire--> SynctryAcquire获取锁方法-->获取失败调用AQS的加入队列方法acquireQueued-->失败后阻塞 调用AQSselfInterrupt()方法。

 

tryRelease(排它释放方法)

    

protected final boolean tryRelease(int releases) {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        int nextc = getState() - releases;//释放锁,
        boolean free = exclusiveCount(nextc) == 0;
        if (free) //因为有重入锁的情况,只有该线程全部释放,才能释放锁
            setExclusiveOwnerThread(null);
        setState(nextc);
        return free;
    }

 

排它锁的释放过程本质上是对AQSstate字段的低16-1,直到为0时释放锁。WriteLock写锁释放的完整过程:WriteLockunlock方法-->AQSrelease方法--> SynctryRelease释放锁方法-->释放成功后调用AQSunparkSuccessor方法,唤醒下一个线程获取锁。

 

共享锁(读锁)实现

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);//正在共享“读锁”的线程数
        if (!readerShouldBlock() && //公平锁和非公平锁实现的差别
                r < MAX_COUNT && //如果正在共享“读锁”的线程数超过最大值,获取锁失败
                compareAndSetState(c, c + SHARED_UNIT)) {//相当于对高16位+1
            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);//把重入次数放到每个读线程自己的ThreadLocal中
                rh.count++;
            }
            return 1;//获取成功
        }
        //完整版获取共享锁,主要用于compareAndSetState这步cas处理失败后,进行轮询获取
        return fullTryAcquireShared(current);
    }

 

获取共享锁的过程本质上是对AQSstate字段的高16+1,如果在有排它锁的情况下(并且不是当前线程持有),获取共享锁的线程直接阻塞。如果当前线程持有排它锁,该线程还可以继续获取共享锁,这就是所谓的降级。但不能升级,也就是持有共享锁的线程,不一定能获取到排它锁。

 

ReadLock读锁的加锁过程:ReadLocklock方法-->AQSacquireShared方法-->SynctryAcquireShared获取锁方法-->如果获取失败调用AQSdoAcquireShared阻塞当前线程。

 

tryReleaseShared(共享释放方法)

 

//释放共享锁(读锁)
    protected final boolean tryReleaseShared(int unused) {
        Thread current = Thread.currentThread();
 
        //shep1:减少线程持有重入读锁的次数
        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();//从自己的ThreadLocal中去掉
                if (count <= 0)
                    throw unmatchedUnlockException();
            }
            --rh.count;
        }
 
        //step2:CAS设置AQS的state字段的高16位(读锁线程数)
        for (;;) {
            int c = getState();
            int nextc = c - SHARED_UNIT; //相当于对高16位-1
            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;
        }
    }

 

实现比较复杂,结合注释也不难理解,其本质就是对AQSstate字段的高16-1

 

ReadLock读锁的加锁过程:ReadLockunlock方法-->AQSreleaseShared方法-->SynctryReleaseShared释放锁方法-->AQSdoReleaseShared方法。

 

共享锁(读锁)的分段转播性

 

在讲解读锁、写锁具体实现的时候,也许都注意到了:ReentrantReadWriteLock 实现了 -不冲突,也就是多读不阻塞;但--冲突。但这只是加锁部分,在释放锁后我们都知道需要唤醒后续线程,再次获取锁。如果AQS队列中的线程都是单纯的获取读锁或者写锁都很好实现:如果是写锁 只用唤醒队列中的头结点;如果是读锁 逐个唤醒所有的线程,这就是共享锁(读锁)的转播性。

 

但在ReentrantReadWriteLock的阻塞队列中同时包含了获取读锁以及写锁的线程,又是怎么实现的呢?AQS队列是一个双向链表,我们先来模拟这个队列:



 

头节点表示当前获取到锁的节点,这里线程1获取到写锁,后面的线程都会被阻塞;

当线程1释放写锁,线程2会获取到读锁,同时由于共享锁的传播性,线程3、线程4都会获取到“读锁”同时被唤醒。

 

那么问题来了,线程6是否会被唤醒呢?答案是否定的,共享锁的转播性是遇到写锁时会中止,具体实现代码见AQS中的doAcquireShared 方法,会遍历setHeadAndPropagate方法:

private void setHeadAndPropagate(Node node, int propagate) {
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //当前节点为空,或者是共享节点时,继续传播。否则停止传播
            if (s == null || s.isShared())
                doReleaseShared();//唤醒线程
        }
    }

 

也许有人会问这里没有for循环怎么实现传播唤醒的呀,别忘了被阻塞的线程是在doAcquireShared方法中阻塞的,另外这里存在一个head头结点的移动,被唤醒后继续执行doAcquireShared方法会继续去头结点线程进行唤醒,直到s == null || s.isShared()这句为false

 

这就是共享锁(读锁)的分段转播性,被写锁分开。

 

到这里ReentrantReadWriteLock读写锁的核心实现总结完毕,还有一些其他中断锁、延时锁、轮询锁的实现与ReentrantLock相同,不再累述。

 

总结

 

 

ReentrantReadWriteLock的实现相对ReentrantLock来说复杂了很多,加锁过程的核心实现原理:就是使用32二进制表示AQSstate(可以理解为 锁的状态),高16位表示持有读锁的线程数,低16位表示持有写锁线程的重入数。写锁的唤醒过程相对比较简单,读锁的唤醒过程,具备分段传播特性。

 

心灵鸡汤

基础:有同事问我怎么成为一名架构师,我会对他说做好程序员该做的事,很多人觉得我是一名架构师,其实我只是一名执着的程序员。

 

 

摘自--《天星老师语录》

 

 

  • 大小: 13.3 KB
0
0
分享到:
评论

相关推荐

    08、读写锁ReentrantReadWriteLock&StampLock详解-ev

    读写锁ReentrantReadWriteLock&StampLock详解_e读写锁ReentrantReadWriteLock&StampLock详解_e读写锁ReentrantReadWriteLock&StampLock详解_e读写锁ReentrantReadWriteLock&StampLock详解_e读写锁...

    java 读写锁代码

    下面我们将详细探讨Java读写锁的概念、实现原理以及如何在实际代码中应用。 1. **读写锁概念**: - 读写锁分为读锁(共享锁)和写锁(独占锁)。读锁允许多个线程同时读取数据,而写锁只允许一个线程进行写操作。 ...

    8、读写锁ReentrantReadWriteLock&StampLock详解.pdf

    根据提供的文件信息,本文将详细解析读写锁`ReentrantReadWriteLock`以及`StampLock`在Java并发编程中的应用场景及其实现原理。 ### 一、读写锁介绍 #### 1.1 读写锁的基本概念 读写锁是一种特殊的锁机制,它可以...

    Java 读写锁实现原理浅析

    本文主要介绍了 Java 读写锁实现原理浅析,包括读写锁的定义、读写锁的实现原理、ReentrantReadWriteLock 的实现机制等。 读写锁的定义 读写锁是一种机制,允许多个线程安全地访问共享资源。读写锁分为读锁和写锁...

    关于读写锁算法的Java实现及思考

    在Java中,`java.util.concurrent.locks`包下的`ReadWriteLock`接口提供了读写锁的抽象定义,具体实现由`ReentrantReadWriteLock`类提供。`ReentrantReadWriteLock`实现了`ReadWriteLock`接口,提供了`readLock()`和...

    读写锁_读写锁_

    在Java中,`java.util.concurrent.locks.ReentrantReadWriteLock`是标准的读写锁实现。这个类提供了可重入的特性,意味着一个线程在获得读锁或写锁后,可以再次请求相同的锁而不会被阻塞。这在处理递归调用时特别...

    Java编程读写锁详解

    Java编程读写锁详解 Java编程读写锁是Java并发编程中的一种重要机制,用于解决多线程访问同一个资源时的安全问题。在Java中,读写锁是通过ReadWriteLock接口实现的,该接口提供了readLock和writeLock两种锁的操作...

    Java的两种读写锁介绍

    本文将深入探讨Java中的两种读写锁:ReentrantReadWriteLock和StampedLock,并分析它们的工作原理、特点以及如何在实际开发中进行应用。 一、ReentrantReadWriteLock(可重入读写锁) 1. **简介**: ...

    各种锁汇总,乐观锁、悲观锁、分布式锁、可重入锁、互斥锁、读写锁、分段锁、类锁、行级锁等

    本文将深入探讨标题和描述中提及的各种锁,包括乐观锁、悲观锁、分布式锁、可重入锁、互斥锁、读写锁、分段锁、类锁以及行级锁。 1. **乐观锁**:乐观锁假设多线程环境中的冲突较少,所以在读取数据时不加锁,只有...

    彻底理解Java中的各种锁.pdf

    在Java中,ReentrantReadWriteLock类是读写锁的实现,它包含两个锁:读锁(共享锁)和写锁(独占锁)。读锁可以被多个线程同时持有,而写锁是独占的,当写锁被占用时,其他线程既不能获取读锁也不能获取写锁。 5. ...

    计算机软件-商业源码-利用读写锁保持线程同步.zip

    读写锁的概念源于操作系统理论,但在许多编程语言中都有实现,如Java的`java.util.concurrent.locks.ReentrantReadWriteLock`和C++的`std::shared_timed_mutex`。下面将详细探讨读写锁的工作原理、优缺点以及如何在...

    Java多线程 ReentrantReadWriteLock原理及实例详解

    Java的ReentrantReadWriteLock是Java并发包`java.util.concurrent.locks`中的一个重要工具,它提供了一种更细粒度的锁定机制,相比普通的独占锁(如ReentrantLock)在某些场景下能显著提高程序的并发性能。读写锁...

    Java多线程编程之读写锁ReadWriteLock用法实例

    在Java中,`java.util.concurrent.locks`包提供了`ReentrantReadWriteLock`类来实现可重入的读写锁。这个类包含两个核心方法:`readLock()`和`writeLock()`,分别用于获取读锁和写锁。读锁和写锁都是可重入的,这...

    java多线程-读写锁原理

    Java 5开始,`java.util.concurrent.locks`包提供了`ReentrantReadWriteLock`类来实现可重入的读写锁。 1. **读/写锁的Java实现**: `ReentrantReadWriteLock`类提供了读锁(`ReadLock`)和写锁(`WriteLock`)两...

    举例说明Java多线程编程中读写锁的使用

    Java的`java.util.concurrent.locks.ReentrantReadWriteLock`类提供了可重入的读写锁功能。下面我们将详细探讨读写锁的使用以及其在示例中的应用。 读写锁的核心特性: 1. **读锁(共享锁)**:多个线程可以同时...

    Java 多线程与并发(12-26)-JUC锁- ReentrantReadWriteLock详解.pdf

    在Java多线程并发编程中,ReentrantReadWriteLock(可重入读写锁)是一个重要的同步工具,它属于Java并发包(java.util.concurrent.locks)中的一个类。这个锁提供了比标准的synchronized关键字更细粒度的控制,允许...

    行业分类-设备装置-双模式读写锁.zip

    在Java中,`java.util.concurrent.locks.ReentrantReadWriteLock`类提供了可重入的读写锁实现。而在C++中,标准库中的`std::shared_mutex`和`std::unique_lock`可以用来实现读写锁。这些库提供了灵活的接口,允许...

    java-syn.zip_Java syn_Java syn锁_java同步锁syn_java锁 syn_syn同步事务锁

    - **读写锁**:`ReentrantReadWriteLock`允许多个读取者同时访问,但写入者具有独占权。 - **条件变量**:如`Lock`接口中的`Condition`,提供与`synchronized`中的`wait()`和`notify()`类似的功能,但更加灵活。 ...

    Java锁的种类以及区别

    - `ReadWriteLock`接口定义了读写锁的行为,其具体实现类如`ReentrantReadWriteLock`。 ### 总结 Java中的锁机制对于保证多线程环境下的数据一致性至关重要。通过了解不同类型的锁及其特性,开发者可以根据实际...

    Java对象锁和类锁全面解析(多线程synchronize

    为了提高效率,Java提供了更高级的锁机制,如读写锁(ReentrantReadWriteLock)、显式锁(java.util.concurrent.locks.Lock接口的实现,如ReentrantLock)、条件变量(Condition)等。这些高级锁允许更细粒度的控制...

Global site tag (gtag.js) - Google Analytics