Java多线程读写锁ReentrantReadWriteLock类详解

目录
  • ReentrantReadWriteLock
    • 读读共享
    • 写写互斥
    • 读写互斥
  • 源码分析
    • 写锁的获取与释放
    • 读锁的获取与释放
  • 参考文献

真实的多线程业务开发中,最常用到的逻辑就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务),这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的。所以在JDK中提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行效率。

读写锁表示两个锁,一个是读操作相关的锁,称为共享锁;另一个是写操作相关的锁,称为排他锁。

下面我们通过代码去验证下读写锁之间的互斥性

ReentrantReadWriteLock

读读共享

首先创建一个对象,分别定义一个加读锁方法和一个加写锁的方法,

public class MyDomain3 {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void testReadLock() {
        try {
            lock.readLock().lock();
            System.out.println(System.currentTimeMillis() + " 获取读锁");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
    }

    public void testWriteLock() {
        try {
            lock.writeLock().lock();
            System.out.println(System.currentTimeMillis() + " 获取写锁");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }

}

创建线程类1 调用加读锁方法

public class Mythread3_1 extends Thread {

    private MyDomain3 myDomain3;

    public Mythread3_1(MyDomain3 myDomain3) {
        this.myDomain3 = myDomain3;
    }

    @Override
    public void run() {
        myDomain3.testReadLock();
    }
}
@Test
    public void test3() throws InterruptedException {
        MyDomain3 myDomain3 = new MyDomain3();
        Mythread3_1 readLock = new Mythread3_1(myDomain3);
        Mythread3_1 readLock2 = new Mythread3_1(myDomain3);
    readLock.start();
    readLock2.start();

        Thread.sleep(3000);
    }

执行结果:

1639621812838 获取读锁
1639621812839 获取读锁

可以看出两个读锁几乎同时执行,说明读和读之间是共享的,因为读操作不会有线程安全问题。

写写互斥

创建线程类2,调用加写锁方法

public class Mythread3_2 extends Thread {

    private MyDomain3 myDomain3;

    public Mythread3_2(MyDomain3 myDomain3) {
        this.myDomain3 = myDomain3;
    }

    @Override
    public void run() {
        myDomain3.testWriteLock();
    }
}
@Test
    public void test3() throws InterruptedException {
        MyDomain3 myDomain3 = new MyDomain3();
        Mythread3_2 writeLock = new Mythread3_2(myDomain3);
        Mythread3_2 writeLock2 = new Mythread3_2(myDomain3);

        writeLock.start();
        writeLock2.start();

        Thread.sleep(3000);
    }

执行结果:

1639622063226 获取写锁
1639622064226 获取写锁

从时间上看,间隔是1000ms即1s,说明写锁和写锁之间互斥。

读写互斥

再用线程1和线程2分别调用读锁与写锁

@Test
    public void test3() throws InterruptedException {
        MyDomain3 myDomain3 = new MyDomain3();
        Mythread3_1 readLock = new Mythread3_1(myDomain3);
        Mythread3_2 writeLock = new Mythread3_2(myDomain3);

    readLock.start();
        writeLock.start();

        Thread.sleep(3000);
    }

执行结果:

1639622338402 获取读锁
1639622339402 获取写锁

从时间上看,间隔是1000ms即1s,和代码里面是一致的,证明了读和写之间是互斥的。

注意一下,"读和写互斥"和"写和读互斥"是两种不同的场景,但是证明方式和结论是一致的,所以就不证明了。

最终测试结果下:

  • 1、读和读之间不互斥,因为读操作不会有线程安全问题
  • 2、写和写之间互斥,避免一个写操作影响另外一个写操作,引发线程安全问题
  • 3、读和写之间互斥,避免读操作的时候写操作修改了内容,引发线程安全问题

总结起来就是,多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。

源码分析

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

读写锁将变量切分成了两个部分,高16位表示读,低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; }

其实是通过位运算。假设当前同步状态值为c,写状态等于c & EXCLUSIVE_MASK (c&0x0000FFFF(将高16位全部抹去)),读状态等于c>>>16(无符号补0右移16位)。当写状态增加1时,等于c+1,当读状态增加1时,等于c+(1<<16),也就是c+0x00010000。

根据状态的划分能得出一个推论:c不等于0时,当写状态(c & 0x0000FFFF)等于0时,则读状态(c>>>16)大于0,即读锁已被获取。

写锁的获取与释放

通过上面的测试,我们知道写锁是一个支持重入的排它锁,看下源码是如何实现写锁的获取

protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
            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)
                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;
        }

第3行到第11行,简单说了下整个方法的实现逻辑,这里要夸一下,这段注释就很容易的让人知道代码的功能。下面我们分析一下,第13到第15行,分别拿到了当前线程对象current,lock的加锁状态值c 以及写锁的值w,c!=0 表明 当前处于有锁状态,再继续分析第16行到25行,有个关键的Note:(Note: if c != 0 and w == 0 then shared count != 0):简单说就是:如果一个有锁状态但是没有写锁,那么肯定加了读锁。

第18行if条件,就是判断加了读锁,但是当前线程不是锁拥有的线程,那么获取锁失败,证明读写锁互斥。

第20行到第25行,走到这步,说明 w !=0 ,已经获取了写锁,只要不超过写锁最大值,那么增加写状态然后就可以成功获取写锁。

如果代码走到第26行,说明c==0,当前没有加任何锁,先执行 writerShouldBlock()方法,此方法用来判断写锁是否应该阻塞,这块是对公平与非公平锁会有不同的逻辑,对于非公平锁,直接返回false,不需要阻塞,下面是公平锁执行的判断

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

对于公平锁需要判断当前等待队列中是否存在 等于当前线程并且正在排队等待获取锁的线程。

写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。

读锁的获取与释放

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取。JDK源码如下:

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

第4行到第6行,如果写锁被其他线程持有,则直接返回false,获取读锁失败,证明不同线程间写读互斥。

 第8行,readerShouldBlock() 获取读锁是否应该阻塞,这儿也同样要区分公平锁和非公平锁,公平锁模式需要判断当前等待队列中是否存在 等于当前线程并且正在排队等待获取锁的线程,存在则获取读锁需要等待。

非公平锁模式需要判断当前等待队列中第一个是等待写锁的,则方法返回true,获取读锁需要等待。

fullTryAcquireShared() 主要是处理读锁获取的完整版本,它处理tryAcquireShared()中没有处理的CAS错误和可重入读锁的处理逻辑。

参考文献

1:《Java并发编程的艺术》

2:《Java多线程编程核心技术》

到此这篇关于Java多线程读写锁ReentrantReadWriteLock类详解的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

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

    读写锁ReentrantReadWriteLock概述 读写锁ReentrantReadWriteLock,使用它比ReentrantLock效率更高. 读写锁表示两个锁,一个是读操作相关的锁,称为共享锁:另一个是写操作相关的锁,称为排他锁. 1.读和读之间不互斥,因为读操作不会有线程安全问题 2.写和写之间互斥,避免一个写操作影响另外一个写操作,引发线程安全问题 3.读和写之间互斥,避免读操作的时候写操作修改了内容,引发线程安全问题 多个Thread可以同时进行读取操作,但是同一时刻只允许一个

  • Java多线程之ReentrantReadWriteLock源码解析

    一.介绍 1.1 ReentrantReadWriteLock ReentrantReadWriteLock 是一个读写锁,允许多个读或者一个写线程在执行. 内部的 Sync 继承自 AQS,这个 Sync 包含一个共享读锁 ReadLock 和一个独占写锁 WriteLock. 该锁可以设置公平和非公平,默认非公平. 一个持有写锁的线程可以获取读锁.如果该线程先持有写锁,再持有读锁并释放写锁,称为锁降级. WriteLock支持Condition并且与ReentrantLock语义一致,而Re

  • Java多线程读写锁ReentrantReadWriteLock类详解

    目录 ReentrantReadWriteLock 读读共享 写写互斥 读写互斥 源码分析 写锁的获取与释放 读锁的获取与释放 参考文献 真实的多线程业务开发中,最常用到的逻辑就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务),这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的.所以在JDK中提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行效率. 读写锁表示两个锁,一个是读操作相关的锁

  • Java利用StampedLock实现读写锁的方法详解

    目录 概述 StampedLock介绍 演示例子 性能对比 总结 概述 想到读写锁,大家第一时间想到的可能是ReentrantReadWriteLock.实际上,在jdk8以后,java提供了一个性能更优越的读写锁并发类StampedLock,该类的设计初衷是作为一个内部工具类,用于辅助开发其它线程安全组件,用得好,该类可以提升系统性能,用不好,容易产生死锁和其它莫名其妙的问题.本文主要和大家一起学习下StampedLock的功能和使用. StampedLock介绍 StampedLock的状态

  • java多线程关键字final和static详解

    这篇文章主要介绍了java多线程关键字final和static详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 final关键字 1.final关键字在单线程中的特点: 1)final修饰的静态成员:必须在进行显示初始化或静态代码块赋值,并且仅能赋值一次. 2)final修饰的类成员变量,可以在三个地方进行赋值:显示初始化.构造代码块和构造方法,并且仅能赋值一次. 3)final修饰的局部变量,必须在使用之前进行显示初始化(并不一定要在定义是

  • Java多线程之搞定最后一公里详解

    目录 绪论 一:线程安全问题 1.1 提出问题 1.2 不安全的原因 1.2.1 原子性 1.2.2 代码"优化" 二:如何解决线程不安全的问题 2.1 通过synchronized关键字 2.2 volatile 三:wait和notify关键字 3.1 wait方法 3.2 notify方法 3.3 wait和sleep对比(面试常考) 四:多线程案例 4.1 饿汉模式单线程 4.2 懒汉模式单线程 4.3 懒汉模式多线程低性能版 4.4懒汉模式-多线程版-二次判断-性能高 总结

  • Java多线程模拟银行系统存钱问题详解

    目录 一.题目描述 二.解题思路 三.代码详解 多学一个知识点 一.题目描述 题目:模拟一个简单的银行系统,使用两个不同的线程向同一个账户存钱. 实现:使用特殊域变量volatile实现同步. 二.解题思路 创建一个类:SynchronizedBankFrame,继承JFrame类 写一个内部类Bank 定义一个account变量,来表示账户. deposit():一个存钱的方法 getAccount():显示账户余额的方法. 写一个内部类Transfer,实现Runnable接口 在run方法

  • Java多线程中ReentrantLock与Condition详解

    一.ReentrantLock类 1.1什么是reentrantlock java.util.concurrent.lock中的Lock框架是锁定的一个抽象,它允许把锁定的实现作为Java类,而不是作为语言的特性来实现.这就为Lock的多种实现留下了空间,各种实现可能有不同的调度算法.性能特性或者锁定语义.ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,但是添加了类似锁投票.定时锁等候和可中断锁等候的一些特性.此外,它还提供了在激烈争用情况下更

  • Java多线程之哲学家就餐问题详解

    一.题目 教材提供一个哲学家就餐问题的解决方案的框架.本问题要求通过pthreads 互斥锁来实现这个解决方案. 哲学家 首先创建 5 个哲学家,每个用数字 0~4 来标识.每个哲学家作为一个单独的 线程运行. 可使用 Pthreads 创建线程.哲学家在思考和吃饭之间交替.为了模拟这两种活动,请让线程休眠 1 到 3 秒钟.当哲学家想要吃饭时,他调用函数: pickup_forks(int philosopher _number) 其中,philosopher _number 为想吃饭哲学家的

  • java 多线程与并发之volatile详解分析

    目录 CPU.内存.缓存的关系 CPU缓存 什么是CPU缓存 为什么要有多级CPU Cache Java内存模型(Java Memory Model,JMM) JMM导致的并发安全问题 可见性 原子性 有序性 volatile volatile特性 volatile 的实现原理 总结 CPU.内存.缓存的关系 要理解JMM,要先从计算机底层开始,下面是一份大佬的研究报告 计算机在做一些我们平时的基本操作时,需要的响应时间是不一样的!如果我们计算一次a+b所需要的的时间: CPU读取内存获得a,1

  • Java多线程中的Balking模式详解

    目录 1.场景 2.详细说明 3.Balking模式的本质:停止并返回 源代码如下: 总结 1.场景 自动保存功能: 为防止电脑死机,而定期将数据内容保存到文件中的功能. 2.详细说明 当数据内容被修改时,内容才会被保存.即当写入的内容与上次写入的内容一致时,其实就没有必要执行写入操作.也就是说,以”数据内容是否一致”作为守护条件.若数据内容相同,则不执行写入操作,直接返回. 3.Balking模式的本质:停止并返回 如果现在不合适执行该操作,或者没有必要执行该操作,就停止处理,直接返回—-Ba

  • Java多线程案例之阻塞队列详解

    目录 一.阻塞队列介绍 1.1阻塞队列特性 1.2阻塞队列的优点 二.生产者消费者模型 2.1阻塞队列对生产者的优化 三.标准库中的阻塞队列 3.1Java提供阻塞队列实现的标准类 3.2Blockingqueue基本使用 四.阻塞队列实现 4.1阻塞队列的代码实现 4.2阻塞队列搭配生产者与消费者的代码实现 一.阻塞队列介绍 1.1阻塞队列特性 阻塞队列特性: 一.安全性 二.产生阻塞效果 阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.阻塞队列能是一种线程安全的数据结构, 并且具有

随机推荐