读写锁ReentrantReadWriteLock

        读写锁ReentrantReadWriteLock是JDK1.5提供的一个工具锁,适用于读多写少的场景,将读写分离,从而提高并发性。读写锁允许的情况:一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。

        ReentrantReadWriteLock可用于提高某些集合的并发性。仅当集合预计很大时,读线程比写线程多,并且需要用超过同步开销的开销时,使用ReentrantReadWriteLocks通常是值得的。

        ReentrantReadWriteLock实现了ReadWriteLock接口。

          ReadWriteLock接口

        ReadWriteLock接口暴露了两个Lock对象,一个用来读,另一个用来写。读取ReadWriteLock锁守护的数据,必须先获得读取的锁;当需要修改ReadWriteLock锁守护的数据时,必须先获得写入的锁。读写锁加锁策略允许多个同时存在的读锁,但只允许一个写者。也就是说,读锁是共享锁,写锁是排他锁,读锁和写锁不能同时存在。

  • 读锁 - 如果没有线程锁定ReadWriteLock进行写入,则多线程可以访问读锁。
  • 写锁 - 如果没有线程正在读或写,那么一个线程可以访问写锁。

1. 可重入

        顾名思义,ReentrantReadWriteLock是可重入锁,它的读锁、写锁都是可重入的。

public class ReentrantLockTest {

    private static final ReentrantReadWriteLock reentrantReadWriteLock
            = new ReentrantReadWriteLock(true);

    private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock
            .readLock();
    private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock
            .writeLock();

    public void reentrantRead() {
        readLock.lock();
        read();
        readLock.unlock();
    }

    public void reentrantWrite() {
        writeLock.lock();
        write();
        writeLock.unlock();
    }

    public static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    public static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockTest test = new ReentrantLockTest();
        test.reentrantRead();
        test.reentrantWrite();
    }
}

        运行结果:

2. 公平锁

       ReentrantReadWriteLock可以事公平锁,只需在构造函数的参数中传入 true:

ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true); 

       在获取读锁之前,线程会检查 readerShouldBlock() 方法,同样,在获取写锁之前,线程会检查 writerShouldBlock() 方法,来决定是否需要插队或者是去排队:

final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
    return hasQueuedPredecessors();
}

      在公平锁的情况下,只要等待队列中有线程在等待,也就是 hasQueuedPredecessors() 返回 true 的时候,那么 writer 和 reader 都会阻塞,也就是一律不允许插队。因此,对于公平锁而言,在某个线程释放锁之后,等待的线程获取锁的策略是以请求获取锁的时间为标准的,即使先请求获取锁的线程先拿到锁。下面的测试代码做了一个简单的验证:

package com.java.concurrency.in.practice.ch12;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class FairLocking {

    public static final boolean FAIR = true;
    private static final int NUM_THREADS = 3;

    private static volatile int expectedIndex = 0;

    public static void main(String[] args) throws InterruptedException {
        ReentrantReadWriteLock.WriteLock lock = new ReentrantReadWriteLock(FAIR).writeLock();

        // we grab the lock to start to make sure the threads don't start until we're ready
        lock.lock();

        for (int i = 0; i < NUM_THREADS; i++) {
            new Thread(new ExampleRunnable(i, lock)).start();

            // a cheap way to make sure that runnable 0 requests the first lock 
            // before runnable 1
            // 这里休眠,可以保证每个线程在第一次循环的时候,都没有获得锁,从第二次
            // 循环开始后,每个线程轮流去尝试获得锁
            Thread.sleep(10);
        }

        // let the threads go
        lock.unlock();
    }

    private static class ExampleRunnable implements Runnable {
        private final int index;
        private final ReentrantReadWriteLock.WriteLock writeLock;

        public ExampleRunnable(int index, ReentrantReadWriteLock.WriteLock writeLock) {
            this.index = index;
            this.writeLock = writeLock;
        }

        public void run() {
            while(true) {
                writeLock.lock();
                try {
                    // this sleep is a cheap way to make sure the previous thread loops
                    // around before another thread grabs the lock, does its work,
                    // loops around and requests the lock again ahead of it.
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    //ignored
                }

                if (index != expectedIndex) {
                    System.out.printf("Unexpected thread obtained lock! " +
                            "Expected: %d Actual: %d%n", expectedIndex, index);
                    System.exit(0);
                }

                expectedIndex = (expectedIndex+1) % NUM_THREADS;
                writeLock.unlock();
            }
        }
    }
}

       这段测试代码给每个线程绑定了一个下标,每个线程在各自的循环中轮流去获取写锁。如果不是遵循先请求先获取的方式,那么期望的下标值跟获得锁的线程下标必然会不一致。运行这段代码,会发现一直在循环中,证明了公平性的阻塞策略。

 3. 非公平锁的插队策略

        在构造函数的参数中传入 false,就是非公平锁。ReentrantReadWriteLock默认是非公平锁。非公平锁对读写锁排队的实现如下:

final boolean writerShouldBlock() {
    return false; // writers can always barge
}
final boolean readerShouldBlock() {
    return apparentlyFirstQueuedIsExclusive();
}

        可以看到, 写锁的线程返回值是 false,所以写锁是随时可以插队的;而对于读锁线程来说,就没那么简单了,它需要判断队列中第一个等待的结点是否是写线程,如果是,则读线程不允许插队,否则读线程可以闯入。也就是说,读锁只有在头结点不是写线程的情况是可以插队!

3.1 写者闯入

        写者是随时可以插队的,以下代码可以验证:

package com.java.concurrency.in.practice.ch12;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class WriteLockBargepQueue {

    private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        System.out.println(Thread.currentThread().getName() + "尝试获得读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write() {
        System.out.println(Thread.currentThread().getName() + "尝试获得写锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> write(),"线程一").start();
        new Thread(() -> read(),"线程二").start();
        new Thread(() -> read(),"线程三").start();
        new Thread(() -> write(),"线程四").start();
        new Thread(() -> read(),"线程五").start();
        new Thread(() -> read(),"线程六").start();

    }
}

        运行结果:

线程二尝试获得读锁
线程一尝试获得写锁
线程三尝试获得读锁
线程二得到读锁,正在读取
线程四尝试获得写锁
线程五尝试获得读锁
线程六尝试获得读锁
线程二释放读锁
线程一得到写锁,正在写入
线程一释放写锁
线程四得到写锁,正在写入
线程四释放写锁
线程三得到读锁,正在读取
线程五得到读锁,正在读取
线程六得到读锁,正在读取
线程三释放读锁
线程五释放读锁
线程六释放读锁

Process finished with exit code 0

         线程三原本排在线程四之前,但是当线程一释放写锁后,线程四优先拿到写锁。

3.2 读者闯入

        读者已经获得锁,写锁排在等待队列的首位,接着读者线程加入队列中。排在写锁后面,如果允许读者线程闯入,这样看似提高了效率,但如果想要读取的线程不停地增加,读线程不断闯入,那么降导致写锁线程长时间拿不到写锁,造成写者饥饿。下面用代码验证复原一下这个场景:

package com.java.concurrency.in.practice.ch12;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadLockBargepQueue {

    private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        System.out.println(Thread.currentThread().getName() + "尝试获得读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write() {
        System.out.println(Thread.currentThread().getName() + "尝试获得写锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> read(),"Thread-2r").start();
        new Thread(() -> read(),"Thread-4r").start();
        new Thread(() -> write(),"Thread-3w").start();
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                new Thread(() -> read(),"线程" + i).start();
            }
        }).start();
    }

}

        这段代码中,2号、4号线程是读线程,获得读锁;紧随其后是3号写线程,它在队首等待,其后是50个读线程,测试结果如下:

        3w线程在2r、4r线程之后,当2r、4r线程获得读锁后,3w线程自然就排在队首了。 

        如测试结果所示,2r、4r线程释放后,3w线程拿到写锁。可见,当写线程排在队首时,读者是无法闯入的。 

        如果调整一下顺序,使得排在队首的是读线程,那么读者就可以闯入了:

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> read(),"Thread-2r").start();
        new Thread(() -> read(),"Thread-4r").start();
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                new Thread(() -> read(),"读者线程" + i).start();
            }
        }).start();
        Thread.sleep(5);
        new Thread(() -> write(),"Thread-3w").start();
    }

       2线程本来排在3线程之前,但3线程却提前拿到读锁。 

        ReentrantReadWriteLock使用的注意事项 读锁不支持条件变量 重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待 重入时支持降级: 持有写锁的情况下可以去获取读锁

这个类的构造函数接受一个可选的公平参数。当设置为true时,在争用项下,锁倾向于授予对等待时间最长的线程的访问权限。 但是,请注意,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的多个线程中的一个可以连续多次获得该锁,而其他活动线程则没有进展,目前也没有保存该锁。

4. 支持锁降级

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

        锁降级主要是为了防止数据没有刷回到主内存,导致其他线程取到的值不一致!如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。 锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

        锁降级也可以有助于提高效率。如果一直使用写锁,最后释放写锁,虽然线程安全,但是有时候没这个必要,假设只有一处需要更新数据,后面的只是读,这个时候还用写锁就不能多个线程读了,浪费资源,这个时候就用锁的降级提高整体效率。

         以下代码是一个典型的锁降级例子:

package com.java.concurrency.in.practice.ch12;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class CachedData {

    Object data;
    // 保证可见性
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();


    void processCachedData() {
        // 首先获取读锁
        rwl.readLock().lock();
        // 判断缓存是否有效,有效直接输出
        if (!cacheValid) {
            // 无效就把读锁释放,上写锁
            // 锁降级从写锁获取到开始,这个时候有可能有其他线程抢先获取了写锁
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                // 因为可能有其他写线程抢到写锁并更新了缓存,所以需要再次判断如果缓存还是无效
                if (!cacheValid) {
                    // 更新data,缓存设为有效
                    data = new Object();
                    cacheValid = true;
                }
                // 因为我们想要使用数据(在后续打印出data),所以请求读锁
                rwl.readLock().lock();
            } finally {
                // 确保写锁释放,整个时候就是读锁了,然后打印data
                rwl.writeLock().unlock();
            }
        }
        try {
            System.out.println(data);
        } finally {
            // 确保读锁能释放
            rwl.readLock().unlock();
        }
    }
}

        锁降级的正确步骤是:持有写锁 -> 持有读锁 -> 释放写锁 -> 释放读锁,为什么要在写锁释放之前获取读锁呢?如果在释放写锁后再拿到读锁,当线程A写锁释放,线程B抢先拿到写锁,并修改数据,此时A再拿到读锁,读到将是被线程B破坏掉的数据,从而产生“脏读”。锁降级的本质也是锁的重入性,可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失。

5. 不支持锁升级

        ReentrantReadWriteLock不支持锁的升级。

        在ReentrantReadWriteLock中,读锁和写锁之间是互斥的。当一个线程持有读锁时,其他线程可以继续获取读锁,但不能获取写锁。这是为了保证读操作的并发性,即多个线程可以同时读取共享资源。

        当一个线程持有读锁时,如果尝试获取写锁,由于写锁的独占性,写锁无法被其他线程获取。如果两个以上读线程都想要升级为写锁,并且不释放读锁,就会陷入永久等待的状态,造成死锁。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/126365.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【广州华锐互动】VR安防网络综合布线仿真实训打造沉浸式的教学体验

随着科技的快速发展&#xff0c;综合布线技术在建筑、数据中心、网络基础设施等领域的应用越来越广泛。为了适应这一趋势&#xff0c;传统的教学方法已经无法满足现代教育的需求。因此&#xff0c;采用创新的教学手段&#xff0c;如虚拟现实&#xff08;VR&#xff09;技术&…

【Python 千题 —— 基础篇】菜品的价格

题目描述 题目描述 食堂今天准备了很多好吃的菜。“beef” 12 元一份&#xff1b;“rice” 1 元一份&#xff1b;“fish” 8 元一份&#xff1b;其它菜品 5 元一份。请你根据输入的字符串&#xff0c;使用 if-elif-else 语句判断该菜品需要花费多少钱。 输入描述 输入一个菜…

【被面试官吊打系列】啥,你没说面试要考智力题呀 (上) ?

你好&#xff0c;我是安然无虞。 文章目录 1. 二进制问题分金条问题毒药问题 2. 先手必胜问题轮流拿石子抢30的必胜策略Nim游戏 3. 水桶问题5L和6L的水桶怎么量出3L的水&#xff1f;3L和5L的水桶怎么量出4L的水&#xff1f;一个装了10L水的桶&#xff0c;一个7L的空桶还有一个…

Windows搭建minio存储

minio功能类似以ftp 小白教程&#xff0c;一看就会&#xff0c;一做就成。 1.下载软件 https://dl.min.io/server/minio/release/windows-amd64/minio.exe 2.部署配置 我是在D盘下创建了minio目录 minio.exe是软件minio.log是日志&#xff08;不用创建&#xff09;minio900…

借钱正成为互联网一大坑,影响你的房贷,悄悄吞噬消费者

如今各个APP都可以给消费者提供贷款&#xff0c;由于网贷已坑了不少人&#xff0c;许多用户都选择了拒绝&#xff0c;不过APP的另一大坑却在悄悄影响消费者的征信&#xff0c;对消费者包括房贷在内的贷款产生影响。 互联网的这个坑就是先用后付功能&#xff0c;表面上各个APP以…

Node.js中的文件系统(file system)模块

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

GPT-4 Turbo 发布 | 大模型训练的新时代:超算互联网的调度与调优

★OpenAI&#xff1b;ChatGPT;Sam Altman&#xff1b;Assistance API&#xff1b;GPT4 Turbo&#xff1b;DALL-E 3&#xff1b;多模态交互&#xff1b;算力调度&#xff1b;算力调优&#xff1b;大模型训练&#xff1b;GH200&#xff1b;snowflake&#xff1b;AGI&#xff1b;A…

内存条选购注意事项(电脑,笔记本)

电脑内存条的作用、选购技巧以及注意事项详解 - 郝光明的个人空间 - OSCHINA - 中文开源技术交流社区 现在的电脑直接和内存条联系 电脑上的所有输入和输出都只能依靠内存条 现在买双条而不是单条 买两个相同的内存条最好 笔记本先分清是低电压还是标准电压&#xff0c;DD…

java多线程文件下载器

文章目录 1.简介2.文件下载的核心3.文件下载器的基础代码3.1 HttpURLConnection3.2 用户标识 4.下载信息4.1 计划任务4.2 ScheduledExecutorService&#x1f340; schedule方法&#x1f340; scheduleAtFixedRate方法&#x1f340; scheduleWithFixedDelay方法 5.线程池简介5.1…

“隐身术”成现实,中科院院士现场表演

&#xff08;图源&#xff1a;哔哩哔哩&#xff09; 在“bilibili超级科学晚”活动现场&#xff0c;中国科学院院士褚君浩为我们揭示了“隐身术”的原理。原来&#xff0c;这种神奇的技能是一种科学手段。 褚君浩院士为大家介绍了一种名为“柱镜光栅”的特殊材料&#xff0c;柱…

Zotero拓展功能之Zotero Style

Zotero Style拓展功能 一、列&#xff1a; 1.简介 首先你必须知道Zotero的基本功能&#xff1a;右键任意一个列的名字&#xff0c;会弹出一个右键菜单&#xff0c;你可以勾选/取消勾选一个列&#xff0c;并且在最后有两个按钮&#xff0c;一个是“列设置”&#xff0c;一个是…

VS2015模块库交接出现环境报错 error MSB8031 和 error C1189

问题报错 1.错误 MSB8031 Building an MFC project for a non-Unicode character set is deprecated. You must change the project property to Unicode or download an additional library. 错误 MSB8031不赞成为非Unicode字符集生成MFC项目。您必须将项目属性更改为Unicode&…

数据公网传输加密隧道技术

参考&#xff1a; https://wenku.baidu.com/view/c2bfb9b4d6bbfd0a79563c1ec5da50e2524dd1a1.html?wkts1699578126402

【Linux基础IO篇】用户缓冲区、文件系统、以及软硬链接

【Linux基础IO篇】用户缓冲区、文件系统、以及软硬链接 目录 【Linux基础IO篇】用户缓冲区、文件系统、以及软硬链接深入理解用户缓冲区缓冲区刷新问题缓冲区存在的意义 File模拟实现C语言中文件标准库 文件系统认识磁盘对目录的理解 软硬链接软硬链接的删除文件的三个时间 作者…

【Excel】函数sumif范围中符合指定条件的值求和

SUMIF函数是Excel常用函数。使用 SUMIF 函数可以对报表范围中符合指定条件的值求和。 Excel中sumif函数的用法是根据指定条件对若干单元格、区域或引用求和。 sumif函数语法是&#xff1a;SUMIF(range&#xff0c;criteria&#xff0c;sum_range) sumif函数的参数如下&#xff…

安徽首届道医传承十八绝技发布会在合肥成功举办

近日&#xff0c;在安徽合肥举行了首届道医传承十八绝技发布会&#xff0c;本次会议由安徽渡罗门生物科技有限公司、北京道武易医文化传播有限公司、楼观台道医文化研究院联合举办。现场吸引了来自全国各地民族医学领域的专家学者参与讨论与交流。本次会议旨在促进道医的交流与…

OCR技术狂潮:揭秘最新发展现状,引爆未来智能时代

OCR&#xff08;Optical Character Recognition&#xff0c;光学字符识别&#xff09;技术自20世纪以来经历了长足的发展&#xff0c;随着计算机视觉、人工智能和深度学习等领域的进步&#xff0c;OCR技术在准确性、速度和适用范围上都取得了显著的进展。以下是OCR技术发展的现…

在gitlab中指定自定义 CI/CD 配置文件

文章目录 1. 介绍2. 配置操作3. 配置场景3.1 CI/CD 配置文件在当前项目step1&#xff1a;在当前项目中创建目录&#xff0c;编写流水线文件存放在该目录中step2&#xff1a;在当前项目中配置step3&#xff1a;运行流水线测试 3.2 CI/CD 配置文件位于外部站点上step1&#xff1a…

嵌入式养成计划-47----QT--基于QT的OpenCV库实现人脸识别功能

一百二十一、基于QT的OpenCV库实现人脸识别功能 121.1 UI 界面 登录按钮现在没啥实际作用&#xff0c;因为没加功能&#xff0c;可以添加在识别成功后运行的功能代码 121.2 思路 显示人脸&#xff1a; 通过 VideoCapture 这个类下面的 open() 方法打开摄像头&#xff0c;对…

进口猫罐头在排行榜中是否靠前?排行榜中靠前的猫罐头测评

养猫这6年&#xff0c;我对猫咪的日常饮食把关一直很严格。这些年我给我家猫们购买过很多不同品牌、不同口味的罐头&#xff0c;在猫罐头的挑选、分析上还是有一些经验的。今天&#xff0c;我将和大家一起探讨进口猫罐头在排行榜中是否靠前&#xff1f;同时&#xff0c;我将为大…