JUC并发编程第十三章——读写锁、邮戳锁

本章路线总纲

无锁——>独占锁——>读写锁——>邮戳锁

1 关于锁的面试题

  • 你知道Java里面有那些锁
  • 你说说你用过的锁,锁饥饿问题是什么?
  • 有没有比读写锁更快的锁
  • StampedLock知道吗?(邮戳锁/票据锁)
  • ReentrantReadWriteLock有锁降级机制,你知道吗?

2 简单聊聊ReentrantReadWriteLock

类图:

读写锁的演变情况:

2.1 是什么?

读写锁说明

  • 一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程

演变

  • 无锁无序->加锁->读写锁->邮戳锁

读写锁意义和特点

  • 读写锁只允许读读共存,而读写和写写依然是互斥的,恰好大多实际场景是”读/读“线程间不存在互斥关系,只有”读/写“线程或者”写/写“线程间的操作是需要互斥的,因此引入了 ReentrantReadWriteLock
  • 一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但是不能同时存在写锁和读锁,也即资源可以被多个读操作访问,或一个写操作访问,但两者不能同时进行。
  • 只有在读多写少情景之下,读写锁才具有较高的性能体现。

2.2 特点

可重入、读写兼顾

结论:一体两面,读写互斥,读读共享,读没有完成的时候其他线程写锁无法获得

ReentrantReadWriteLock的缺点:

1. 锁饥饿问题:

  • ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,因此当前有可能会一直存在读锁,而无法获得写锁。

2. 锁降级:

  • 将写锁降级为读锁------>遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级为读锁
  • 如果一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁。
  • 如果释放了写锁,那么就完全转换为读锁
  • 如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略

2.3 读写锁案例

  • 使用读写锁之前,使用synchronized的情况
public class ReentrantReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache cache = new MyCache();

        //开启10个线程,写入数据
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.write(finalI + "", finalI + "");
            }, String.valueOf(i)).start();
        }

        //开启10个线程,读取数据
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.read(finalI + "");
            }, String.valueOf(i)).start();
        }
    }
}

//模拟一个缓存资源类,有读写两种功能
class MyCache {

    HashMap<String, String> map = new HashMap<>();

     ReentrantLock lock = new ReentrantLock();

    //读写都加锁
    public void write(String key, String value) {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "线程开始写入数据...");
            //延迟500ms模拟业务耗时,同时可以看出读写不能共同执行 (因为运行结果是先打印一个线程写入,再打印对应线程写入完成)
            TimeUnit.MILLISECONDS.sleep(500);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "线程完成写入数据!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void read(String key) {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "线程开始读取数据...");
            String val = map.get(key);
            TimeUnit.MILLISECONDS.sleep(200);
            System.out.println(Thread.currentThread().getName() + "线程读取到的数据是:\t" + val);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
运行结果:
1线程开始写入数据...
1线程完成写入数据!
2线程开始写入数据...
2线程完成写入数据!
3线程开始写入数据...
3线程完成写入数据!
4线程开始写入数据...
4线程完成写入数据!
5线程开始写入数据...
5线程完成写入数据!
6线程开始写入数据...
6线程完成写入数据!
7线程开始写入数据...
7线程完成写入数据!
9线程开始写入数据...
9线程完成写入数据!
8线程开始写入数据...
8线程完成写入数据!
10线程开始写入数据...
10线程完成写入数据!
1线程开始读取数据...
1线程读取到的数据是:	1
2线程开始读取数据...
2线程读取到的数据是:	2
3线程开始读取数据...
3线程读取到的数据是:	3
4线程开始读取数据...
4线程读取到的数据是:	4
5线程开始读取数据...
5线程读取到的数据是:	5
6线程开始读取数据...
6线程读取到的数据是:	6
7线程开始读取数据...
7线程读取到的数据是:	7
8线程开始读取数据...
8线程读取到的数据是:	8
9线程开始读取数据...
9线程读取到的数据是:	9
10线程开始读取数据...
10线程读取到的数据是:	10

说明:可以看出,开始写入/读取和完成写入/读取,都是成对出现的。这说明这写入/读取期间,其他线程不能执行写入/读取。读写/读读/写写都互斥了。

问题:我们希望的情况应该是,读写/写写都互斥,但读读可以并发读取。从而引出了读写锁(对写独占,对读共享)

  • 使用读写锁
public class ReentrantReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache cache = new MyCache();

        //开启10个线程,写入数据
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.write(finalI + "", finalI + "");
            }, String.valueOf(i)).start();
        }

        //开启10个线程,读取数据
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(() -> {
                cache.read(finalI + "");
            }, String.valueOf(i)).start();
        }
    }
}

//模拟一个缓存资源类,有读写两种功能
class MyCache {

    HashMap<String, String> map = new HashMap<>();

    ReentrantLock lock = new ReentrantLock();

    ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    //读写都加锁
    public void write(String key, String value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "线程开始写入数据...");
            //延迟500ms模拟业务耗时,同时可以看出读写不能共同执行 (因为运行结果是先打印一个线程写入,再打印对应线程写入完成)
            TimeUnit.MILLISECONDS.sleep(500);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "线程完成写入数据!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public void read(String key) {
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "线程开始读取数据...");
            String val = map.get(key);
            TimeUnit.MILLISECONDS.sleep(200);
            System.out.println(Thread.currentThread().getName() + "线程读取到的数据是:\t" + val);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }
    }
}
运行结果:
1线程开始写入数据...
1线程完成写入数据!
2线程开始写入数据...
2线程完成写入数据!
3线程开始写入数据...
3线程完成写入数据!
4线程开始写入数据...
4线程完成写入数据!
5线程开始写入数据...
5线程完成写入数据!
6线程开始写入数据...
6线程完成写入数据!
7线程开始写入数据...
7线程完成写入数据!
8线程开始写入数据...
8线程完成写入数据!
9线程开始写入数据...
9线程完成写入数据!
10线程开始写入数据...
10线程完成写入数据!
1线程开始读取数据...
9线程开始读取数据...
7线程开始读取数据...
6线程开始读取数据...
5线程开始读取数据...
3线程开始读取数据...
4线程开始读取数据...
2线程开始读取数据...
10线程开始读取数据...
8线程开始读取数据...
10线程读取到的数据是:10
4线程读取到的数据是:	4
2线程读取到的数据是:	2
8线程读取到的数据是:	8
3线程读取到的数据是:	3
7线程读取到的数据是:	7
6线程读取到的数据是:	6
5线程读取到的数据是:	5
1线程读取到的数据是:	1
9线程读取到的数据是:	9

说明:可以看出,所有写操作还是跟之前一样,全部互斥。但读操作可以并发读取。

结论

使用ReadWriteLock实现读写操作,一体两面,读写互斥,读读共享,但是读没有完成时候其它线程写锁无法获取


2.4 锁降级

ReentrantReadwriteLock锁降级:

  • 将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样),锁的严苛程度变强叫做升级,反之叫做降级。

ReentrantReadwriteLock的特性:

写锁降级成为读锁

  1. 如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁。
  2. 规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序。
  3. 如果释放了写锁,那么就完全转换为读锁。

总之:

  • 如果一个线程先获取写锁,在获取写锁和释放写锁之间可以再获取读锁,如果获取了读锁,之前获取的写锁且被释放了。那么之前的写锁,就降级为现在的读锁了。

why?要有这么个特性?

----后面解释,大概目的:锁降级是为了让当前线程感知到数据的变化,目的是保证数据的可见性


2.5 写锁可以降级为读锁,但读锁不可升级为写锁

重入还允许通过获取写入锁定,然后读取锁然后释放写锁从写锁到读取锁,但是从读锁升级到写锁是不可能的

锁降级的目的:锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性

样例1

锁降级:获取写锁 ——> 获取读锁 ——> 释放写锁 ——> 释放读锁      ✔ 可以完成

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LockDownGradingDemo {

    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        // 例一:正常两个A、B线程
//        new Thread(() -> {
//            readLock.lock();
//            System.out.println("---A线程读取---");
//            readLock.unlock();
//        }, "A").start();
//
//        new Thread(() -> {
//            writeLock.lock();
//            System.out.println("---B线程写入---");
//            writeLock.unlock();
//        }, "B").start();


        // 例二:only one 同一个线程
        writeLock.lock();
        System.out.println("---写入---");
        // 一些其它的业务操作...

        readLock.lock();
        System.out.println("---读取---");
        // 一些其它的业务操作...

        writeLock.unlock();
        readLock.unlock();
    }

}

输出结果:
---写入---
---读取---

说明: 

  • 同一个线程的写后立刻读是可以的,即将写入锁降级为读锁是支持的,这种就是锁降级

样例2

锁降级:获取读锁 ——> 获取写锁 ——> 释放读锁 ——> 释放写锁      X 不可以完成

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LockDownGradingDemo2 {

    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        // 例二:only one 同一个线程
        readLock.lock();
        System.out.println("---读取---");
        // 一些其它的业务操作...

        writeLock.lock();
        System.out.println("---写入---");
        // 一些其它的业务操作...

        readLock.unlock(); // 这个位置和下面那个位置效果一样

        writeLock.unlock();
//        readLock.unlock();
    }

}


输出结果:
---读取---
// ...程序未结束

说明:

  • 如果有线程读没有完成的时候,写线程无法获取锁,必须要等着读锁释放所锁后才有机会写,这是悲观锁的策略

1、2例子对比小结:

  • 其实想想很容易理解:同一个线程,先读,还没有读完(读锁readLock没有unlock),我又去写。那么我之前的不就是脏数据了?因此应该先全部读完,才能执行写操作。
  • 而例子1中,先写,就算没写完(写锁没有释放),我立马去读,由于读操作不会导致数据不一致。因此,这是合理的。


2.6 写锁和读锁是互斥的

        写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作

因此,分析读写锁ReentrantReadWriteLock,会发现它有个潜在的问题:

  • 即ReentrantReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁也就是写入必须等待,这是一种悲观的读锁,人家还在读着那,你先别去写,省的数据乱。

2.7 Oracle公司ReentrantReadWriteLock使用样例

* <p><b>Sample usages</b>. Here is a code sketch showing how to perform
* lock downgrading after updating a cache (exception handling is
* particularly tricky when handling multiple locks in a non-nested
* fashion):
*
* <pre> {@code
* class CachedData {
*   Object data;
*   volatile boolean cacheValid;
*   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
*
*   void processCachedData() {
*     rwl.readLock().lock();// 1
*     if (!cacheValid) {
*       // Must release read lock before acquiring write lock
*       rwl.readLock().unlock();// 2
*       rwl.writeLock().lock();// 3
*       try {
*         // Recheck state because another thread might have
*         // acquired write lock and changed state before we did.
*         if (!cacheValid) {
*           data = ...//在此做一些写操作
*           cacheValid = true;
*         }
*         // Downgrade by acquiring read lock before releasing write lock
*         rwl.readLock().lock();// 4
*       } finally {
*         rwl.writeLock().unlock(); // 5 Unlock write, still hold read
*       }
*     }
*
*     try {
*       use(data);
*     } finally {
*       rwl.readLock().unlock();// 6
*     }
*   }
* }}</pre>

代码解读:

  • 1-6 六个加锁/释放锁的操作。1-2对应读锁、3-5对应写锁、4-6对应读锁。volatile类型的cacheValid变量,保证其可见性
  • 首先,线程第一次进来,资源类CacheData是没有被修改过的。先加读锁1,if判断 ( !cacheValid ) 的值为true。在2的位置释放读锁。
  • 接着准备写操作,先获取写锁3。并进行双端检索 (防止其它线程恰好修改了)。做完写操作后,把cacheValid改为true。为了立刻读取到我刚刚修改的数据data,必须发生锁降级,在释放写锁5之前获取读锁4。原因:如果我先把写锁释放了,再获取读锁,出现了没有锁的空档期。在此期间锁可能被其他线程获取并修改数据,无法保证读锁立马能被同一个线程获取,可能在我使用data数据的期间,data数据又被修改了!
  • 在4的位置已经获取了读锁,代码运行到5的位置释放写锁。发生锁降级。之后在use(data)这行使用刚刚修改的data数据,最后在6位置释放读锁。让其他线程继续抢锁。

        这里只有锁降级才能保证,同一个线程我先执行写操作,再继续读我刚刚写的数据。在整个线程执行业务的过程中,一直是加锁(不是写锁就是读锁)状态,没有出现空档期,因此整个操作保证了原子性。

如果违背锁降级的步骤,如果违背锁降级的步骤, 如果违背锁降级的步骤

  • 如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程D获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。

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

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

相关文章

「动态规划」如何求乘积最大子数组?

152. 乘积最大子数组https://leetcode.cn/problems/maximum-product-subarray/description/ 给你一个整数数组nums&#xff0c;请你找出数组中乘积最大的非空连续子数组&#xff08;该子数组中至少包含一个数字&#xff09;&#xff0c;并返回该子数组所对应的乘积。测试用例的…

WebGL学习【焕新计划】

WebGL基础 在正式进入webgl之前&#xff0c;我想有必要简单了解一下渲染管线&#xff0c;毕竟它贯穿webgl学习的整个过程。 渲染管线流程图&#xff1a; webgl着色器简单语法&#xff1a; 在GLSL&#xff08;OpenGL Shading Language&#xff09;中&#xff0c;常见的变量类…

企业化运维(4)_tomcat

###1.配置tomcat### 可以将tomcat部署在server2主机&#xff0c;与nginx主服务器分开&#xff0c;便于进行交互存储。 下载安装jdk与tomcat&#xff0c;并开启服务&#xff0c;便可以在浏览器进行访问。 [rootserver3 ~]# rpm -ivh jdk-8u121-linux-x64.rpm [rootserver3 ~]#…

Mybatis-Plus多种批量插入方案对比

背景 六月某日上线了一个日报表任务&#xff0c;因是第一次上线&#xff0c;故需要为历史所有日期都初始化一次报表数据 在执行过程中发现新增特别的慢&#xff1a;插入十万条左右的数据&#xff0c;SQL执行耗费高达三分多钟 因很早就听闻过mybatis-plus的[伪]批量新增的问题&…

万事开头难——Java实现俄罗斯小方块【第一步】

目录 技术实现&#xff1a; 1.初始化游戏窗口&#xff1b; 1.1 什么是窗口&#xff1a; 1.2 Swing 1.3 JFrame创建窗口&#xff1a; 1.3.1创建窗口的逻辑 1.3.2.设置简单的页面 1.3.3.优化 1.3.4.设置标题 1.4 创建游戏窗口 技术实现&#xff1a; 1.初始化游戏窗口&am…

【CT】LeetCode手撕—20. 有效的括号

题目 原题连接&#xff1a;20. 有效的括号 1- 思路 模式识别 模式1&#xff1a;括号左右匹配 ——> 借助栈来实现 ——> Deque<Character> deque new LinkedList<>()模式2&#xff1a;顺序匹配 ——> 用 if 判断 具体思路 1.遇到左括号 直接入栈相应…

ARM32开发--电源管理单元

知不足而奋进 望远山而前行 目录 文章目录 前言 学习目标 学习内容 PMU 电源域 VDD/VDDA域 备份域 1.2V域 省电模式 睡眠模式 深度睡眠模式 待机模式 几种模式总结 WFI和WFE指令 案例需求 模式初始化 源码 总结 前言 在嵌入式系统中&#xff0c;有效的电池管…

【AI基础】第六步:纯天然保姆喂饭级-安装并运行qwen2-7b

整体步骤类似于 【AI基础】第五步&#xff1a;纯天然保姆喂饭级-安装并运行chatglm3-6b-CSDN博客。 此系列文章列表&#xff1a; 【AI基础】概览 【AI基础】第一步&#xff1a;安装python开发环境-windows篇_下载安装ai环境python 【AI基础】第一步&#xff1a;安装python开发环…

面试题 17.05. 字母与数字

链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 题解&#xff1a;把字母看成1&#xff0c;把数字看为-1&#xff0c;将题目变为求的和为0的最长连续子数组 class Solution { public:vector<string> findLongestSubarray(vector<string>& array) …

MySQL查询练习题1.平均工资2.查询各部门的总薪水3.查询总薪水排名第二的部门4.查询姓名重复的员工信息5.查询各部门薪水大于900的男性员工的平均薪水

创建一个员工表emp&#xff0c;包含字段&#xff1a;姓名name&#xff0c;性别sex&#xff0c;部门depart&#xff0c;工资salary create table emp(name varchar(30) not null,sex varchar(30) not null,depart int not null,salary int not null); 插入数据打印为 mysql>…

Windows Server 2008 r2 IIS .NET

Windows Server 2008 r2 IIS .NET

2-2 基于matlab的变邻域

基于matlab的变邻域&#xff0c;含变惯性权重策略的自适应离散粒子群算法&#xff0c;适应函数是多式联运路径优化距离。有10城市、30城市、75城市三个案例。可直接运行。 2-2 路径规划 自适应离散粒子群算法 - 小红书 (xiaohongshu.com)

MySQL基础——多表查询和事务

目录 1多表关系 2多表查询概述 3连接查询 3.1内连接 3.2左外连接 3.3右外连接 3.4自连接 4联合查询 5子查询 5.1标量子查询(子查询结果为单个值) 5.2列子查询(子查询结果为一列) 5.3行子查询(子查询结果为一行) 5.4表子查询(子查询结果为多行多列) 6事务简介和操…

模拟电子技术基础(二)--PN结

PN结的本质 芯片都是由硅晶体制成&#xff0c;单个硅原子最外层有带有4个电子 在纯硅当中这些电子会两两形成共价键&#xff0c;此时周围形成非常稳定的八电子结构 在一个回路中&#xff0c;灯泡不亮&#xff0c;不导通&#xff0c;因为电池无法吸引其中的电子离开&#xff0c…

机器学习在医学领域中的应用|文献精析·24-06-13

小罗碎碎念 2024-06-13&#xff5c;文献精析&#xff1a;机器学习在医学领域中的应用 为了系统性地和大家梳理一下机器学习在医学领域中的应用&#xff0c;我特意去找了一篇文献&#xff0c;把其中有价值的信息筛选出来了。但是我没选的内容不代表不重要&#xff0c;感兴趣的可…

高分论文密码---大尺度空间模拟预测与数字制图

大尺度空间模拟预测和数字制图技术和不确定性分析广泛应用于高分SCI论文之中&#xff0c;号称高分论文密码。大尺度模拟技术可以从不同时空尺度阐明农业生态环境领域的内在机理和时空变化规律&#xff0c;又可以为复杂的机理过程模型大尺度模拟提供技术基础。我们将结合一些经典…

Java SE进阶必备:数组中的命令行参数详解

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一…

5000天后的世界

为何可以预见未来 1993年&#xff0c;在互联网的黎明时代&#xff0c;凯文凯利创办了《连线》杂志。他曾经采访过以比尔盖茨、史蒂夫乔布斯、杰夫贝佐斯为代表的一众风云创业家。《连线》杂志是全球发行的世界著名杂志&#xff0c;一直致力于报道科学技术带来的经济、社会变革…

Linux screen命令使用

文章目录 1. 前言2. screen是什么?3. screen使用场景描述3. screen常用命令4. 小结5. 参考 1. 前言 实际开发中用到的云服务器&#xff0c;如果项目使用的是python&#xff0c;需要利用项目运行一些时间较长的项目程序脚本的话&#xff0c;由于我们通过ssh连接远端服务器&…

【深度学习】TCN,An Empirical Evaluation of Generic Convolutional【二】

文章目录 膨胀卷积什么是膨胀卷积膨胀卷积公式PyTorch代码 从零开始手动实现一个1D膨胀卷积&#xff0c;不使用PyTorch的nn.Conv1d1. 基本概念2. 手动实现1D膨胀卷积 TCN结构如何使用TCN源码说明1. Chomp1d 类2. TemporalBlock 类3. TemporalConvNet 类 使用方法 膨胀卷积 什么…