我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Java多线程读写锁ReentrantReadWriteLock类详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Java多线程读写锁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;
 

static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

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) {
            
            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类详解的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持编程网。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Java多线程读写锁ReentrantReadWriteLock类详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

如何解析Java多线程读写锁ReentrantReadWriteLock类

这篇文章将为大家详细讲解有关如何解析Java多线程读写锁ReentrantReadWriteLock类,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。真实的多线程业务开发中,最常用到的逻辑就是
2023-06-22

详解Java ReentrantReadWriteLock读写锁的原理与实现

ReentrantReadWriteLock读写锁是使用AQS的集大成者,用了独占模式和共享模式。本文和大家一起理解下ReentrantReadWriteLock读写锁的实现原理,需要的可以了解一下
2022-11-13

一文了解Java读写锁ReentrantReadWriteLock的使用

ReentrantReadWriteLock称为读写锁,它提供一个读锁,支持多个线程共享同一把锁。这篇文章主要讲解一下ReentrantReadWriteLock的使用和应用场景,感兴趣的可以了解一下
2022-11-13

Java多线程中读写锁分离设计模式怎么用

小编给大家分享一下Java多线程中读写锁分离设计模式怎么用,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!主要完成任务:1.read read 并行化2.read
2023-06-25

多线程之死锁详解

死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果无外力干涉,这些线程将无法继续执行下去。死锁的产生通常需要满足以下四个条件:1. 互斥条件:每个资源只能被一个线程占用,即一次只能有一个线程访问该资源。2. 请求
2023-09-13

Java多线程Thread类的使用详解

这篇文章主要介绍了Java多线程Thread类的使用及注意事项,在java标准库中提供了一个Thread类来表示/操作线程,Thread类也可以视为是java标准库提供的API
2022-12-03

Java多线程死锁问题详解(wait和notify)

线程之间形成相互等待资源的环时,就会形成顺序死锁,下面这篇文章主要给大家介绍了关于Java多线程死锁问题(wait和notify)的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
2023-01-05

C语言多线程开发中死锁与读写锁问题怎么解决

今天小编给大家分享一下C语言多线程开发中死锁与读写锁问题怎么解决的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。死锁有时,一个
2023-06-30

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录