读写锁ReentrantReadWriteLockStampLock详解

news2024/11/20 9:39:18

        如何设计一把读写锁?ReentrantReadWriteLock

    读写锁设计思路

        读写状态的设计

        设计的精髓:用一个变量如何维护多种状态

        在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。

        分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为S,那么:

       1. 写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.

       2. 读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于S+(1<<16)

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

        代码实现

        java.util.concurrent.locks.ReentrantReadWriteLock.Sync

exclusiveCount(int c)
静态方法,获得持有写状态的锁的次数。
sharedCount(int c)
静态方法,获得持有读状态的锁的数量。不同于写锁,读锁可以同时被多个线程持有。而 每个线程持有的读锁支持重入的特性,所以需要对每个线程持有的读锁的数量单独计数,这就需要用到HoldCounter 计数器

        HoldCounter 计数器

        读锁的内在机制其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter 计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。

        通过 ThreadLocalHoldCounter 类,HoldCounter 与线程进行绑定。HoldCounter 是绑定线程的一个计数器,而 ThreadLocalHoldCounter 则是线程绑定的 ThreadLocal。

        HoldCounter是用来记录读锁重入数的对象

        ThreadLocalHoldCounter是ThreadLocal变量,用来存放不是第一个获取读锁的线程的其他线程的读锁重入数对象

1. 读写锁介绍

        读写锁ReadWriteLock,顾名思义一把锁分为读与写两部分,读锁允许多个线程同时获得,因为读操作本身是线程安全的。而写锁是互斥锁,不允许多个线程同时获得写锁。并且读与写操作也是互斥的。读写锁适合多读少写的业务场景。

2. ReentrantReadWriteLock介绍 

        针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁,描述如下:

线程进入读锁的前提条件:
        1、没有其他线程的写锁
        2、没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
线程进入写锁的前提条件:
        1、没有其他线程的读锁
        2、没有其他线程的写锁
而读写锁有以下三个重要的特性:
        1、 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
        2、可重入 :读锁和写锁都支持线程重入。以读写线程为例: 读线程获取读锁后,能够再次获取读锁。写线程在获取 写锁之后能够再次获取写锁,同时也可以获取读锁。
        3、锁降级 遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁

2.1 ReentrantReadWriteLock的使用

读写锁接口ReadWriteLock
一对方法,分别获得读锁和写锁 Lock 对象。
ReentrantReadWriteLock类结构
ReentrantReadWriteLock是可重入的读写锁实现类 。在它内部,维护了一对相关的锁,一个用于
只读操作,另一个用于写入操作。只要没有 Writer 线程,读锁可以由多个 Reader 线程同时持有。也
就是说, 写锁是独占的,读锁是共享的。

2.2 如何使用读写锁

package com.laoyang.Thread.readwritelock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();//读锁  共享锁
    private final Lock writeLock = lock.writeLock();//写锁
    private final String[] data = new String[10];

    public void write(int index, String value) {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"获取写锁");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            read(2);
            data[index] = value;
        } finally {
            System.out.println(Thread.currentThread().getName()+"释放写锁");
            writeLock.unlock();
        }
    }

    public String read(int index) {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"获取读锁");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return data[index];
        } finally {
            System.out.println(Thread.currentThread().getName()+"释放读锁");
            readLock.unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockExample rwl = new ReadWriteLockExample();
        // 测试读读,读写,写写场景
//        new Thread(()->{
//            //rwl.read(2);
//            rwl.write(2,"rwl");
//          //  rwl.read(2);
//        }).start();

        //测试读读
//        new Thread(()->rwl.read(2)).start();
//        new Thread(()->rwl.read(2)).start();
//        new Thread(()->rwl.write(2,"rwl")).start();
        //测试写写
        new Thread(()->rwl.write(2,"rwl")).start();
        new Thread(()->rwl.write(2,"rwl")).start();


    }
}

2.3 注意事项

        读锁不支持条件变量

        重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待
        重入时支持降级: 持有写锁的情况下可以去获取读锁

2.4 应用场景

        以下是使用ReentrantReadWriteLock的常见场景:

                1、读多写少:ReentrantReadWriteLock适用于读操作比写操作频繁的场景,因为它允许多个读线程同时访问共享数据,而写操作是独占的。

                2、缓存:ReentrantReadWriteLock可以用于实现缓存,因为它可以有效地处理大量的读操作,同时保护缓存数据的一致性。

读写锁在缓存中的应用
package com.laoyang.Thread.readwritelock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Cache {
    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    // 获取一个key对应的value
    public static final Object get(String key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }

    // 设置key对应的value,并返回旧的value
    public static final Object put(String key, Object value) {
        w.lock();
        try {
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }

    // 清空所有的内容
    public static final void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }



}
        上述示例中,Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。写操作put(String key,Object value)方法和clear()方法,在更新 HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而 只有写锁被释放之后,其他读写操作才能继续。 Cache使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式。

3. 锁降级 

        锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失。

3.1 锁降级的使用示例       
        因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数 据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作。
        
        锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性 ,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
        RentrantReadWriteLock不支持锁升级 (把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。
美团面试三连
面试官:了解锁吗?
小明:了解,还经常用过。
面试官:说说synchronized和lock的区别吧
小明:synchronized是可重入锁,由于lock是一个接口,重入性取决于实现,synchronized不支持中
断,而lock可以。。。。。。。。。。。。。。。。 面试官:好了,那有没有比这两种锁更快的锁呢?
小明:在读多写少的情况下,读写锁比他们的效率更高。
面试官:那有没有比读写锁更快的锁呢?
小明:。。。。。。。。。。

 

4. StampedLock介绍 

        如果我们深入分析ReentrantReadWriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。
为了进一步提升并发执行效率,Java 8引入了新的读写锁: StampedLock
        StampedLock和ReentrantReadWriteLock相比,改进之处在于: 读的过程中也允许获取写锁后 写入!在原先读写锁的基础上新增了一种叫乐观读(Optimistic Reading)的模式。该模式并不会加锁,所以不会阻塞线程,会有更高的吞吐量和更高的性能。

        4.1 StampedLock的使用

        StampLock三种访问模式
        Writing(独占写锁):writeLock 方法会使线程阻塞等待独占访问,可类比ReentrantReadWriteLock 的写锁模式,同一时刻有且只有一个写线程获取锁资源;
        Reading(悲观读锁):readLock方法,允许多个线程同时获取悲观读锁,悲观读锁与独占写锁互斥,与乐观读共享。
        Optimistic Reading(乐观读):这里需要注意了, 乐观读并没有加锁 ,也就是不会有 CAS 机制并且没有阻塞线程。仅当当前未处于 Writing 模式 tryOptimisticRead 才会返回非 0 的邮戳(Stamp), 如果在获取乐观读之后没有出现写模式线程获取锁,则在方法validate返回 true ,允许多个线程获取乐观读以及读锁,同时允许一个写线程获取写锁。
        最好使用如下模板,否则容易出现bug:
         思考: 为何 StampedLock 性比 ReentrantReadWriteLock 好?
        关键在于StampedLock 提供的乐观读。ReentrantReadWriteLock 支持多个线程同时获取读锁,但是当多个线程同时读的时候,所有的写线程都是阻塞的。 StampedLock 的乐观读允许一个写线程获 取写锁,所以不会导致所有写线程阻塞,也就是当读多写少的时候,写线程有机会获取写锁,减少了 线程饥饿的问题,吞吐量大大提高。
         思考: 允许多个乐观读和一个写线程同时进入临界资源操作,那读取的数据可能是错的怎么办?
        
乐观读不能保证读取到的数据是最新的,所以将数据读取到局部变量的时候需要通过lock.validate(stamp) 校验是否被写线程修改过,若是修改过则需要上悲观读锁,再重新读取数据到局部变量。

4.2 演示乐观读

package com.laoyang.Thread.readwritelock;


import java.util.concurrent.locks.StampedLock;


/**
 * @author Fox
 */
public class StampedLockTest {

    public static void main(String[] args) throws InterruptedException {
        Point point = new Point();

        //第一次移动x,y
        new Thread(() -> point.move(100, 200)).start();
        Thread.sleep(100);
        new Thread(() -> point.distanceFromOrigin()).start();
        Thread.sleep(500);
        //第二次移动x,y
        new Thread(()-> point.move(300,400)).start();

    }
}


class Point {
    private final StampedLock stampedLock = new StampedLock();

    private double x;
    private double y;

    public void move(double deltaX, double deltaY) {
        // 获取写锁
        long stamp = stampedLock.writeLock();
        System.out.println("获取到writeLock");
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            // 释放写锁
            stampedLock.unlockWrite(stamp);

            System.out.println(("释放writeLock"));
        }
    }

    /**
     * 计算当前坐标到原点的距离
     * 乐观读
     * @return
     */
    public double distanceFromOrigin() {
        // 获得一个乐观读  (无锁)
        long stamp = stampedLock.tryOptimisticRead();
        // 注意下面两行代码不是原子操作
        // 假设x,y = (100,200)
        // 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
        double currentX = x;
        System.out.println("第1次读,x:{},y:{},currentX:{}" + x + y+ currentX);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//         此处已读取到y,如果没有写入,读取是正确的(100,200)
//         如果有写入,读取是错误的(100,400)
        double currentY = y;
        System.out.println("第2次读,x:{},y:{},currentX:{},currentY:{}"+
                x+ y+ currentX+ currentY);


        // 检查乐观读锁后是否有其他写锁发生
        if (!stampedLock.validate(stamp)) {
            // 获取一个悲观读锁
            stamp = stampedLock.readLock();
            try {
                currentX = x;
                currentY = y;

                System.out.println("最终结果,x:{},y:{},currentX:{},currentY:{}"+
                        x+ y+ currentX+ currentY);
            } finally {
                // 释放悲观读锁
                stampedLock.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }



}

4.3 使用场景和注意事项

        对于读多写少的高并发场景 StampedLock的性能很好,通过乐观读模式很好的解决了写线程“饥饿”的问题,我们可以使用StampedLock 来代替ReentrantReadWriteLock ,但是需要注意的是 StampedLock 的功能仅仅是 ReadWriteLock 的子集,在使用的时候,还是有几个地方需要注意一下。

       1、 StampedLock 写锁是不可重入的,如果当前线程已经获取了写锁,再次重复获取的话就会死锁,使用过程中一定要注意;

        2、悲观读、写锁都不支持条件变量 Conditon ,当需要这个特性的时候需要注意;

        3. 如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。所以,使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【网络基础】——传输层

目录 前言 传输层 端口号 端口号范围划分 知名端口号 进程与端口号的关系 netstat UDP协议 UDP协议位置 UDP协议格式 UDP协议特点 面向数据报 UDP缓冲区 UDP的使用注意事项 基于UDP的应用层协议 TCP协议 TCP简介 TCP协议格式 确认应答机制&#…

性能测试需求分析

1、客户方提出 客户方能提出明确的性能需求&#xff0c;说明对方很重视性能测试&#xff0c;这样的企业一般是金融、电信、银行、医疗器械等&#xff1b;他们一般对系统的性能要求非常高&#xff0c;对性能也非常了解。提出需求也比较明确。 曾经有一个银行项目&#xff0c;已经…

漏洞复现--华测监测预警系统2.2任意文件读取

免责声明&#xff1a; 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直…

Asp.net core Web Api 配置swagger中文

启动项目&#xff0c;如图&#xff1a; 原来是英文的&#xff0c;我们要中文的&#xff0c;WeatherForecastController.cs是一个示例&#xff0c;删除即可&#xff0c;WeatherForecast.cs同时删除&#xff0c;当然不删除也行&#xff0c;这里是删除&#xff0c;创建自己的控制器…

CORE: Cooperative Reconstruction for Multi-Agent Perception 论文阅读

论文连接 CORE: Cooperative Reconstruction for Multi-Agent Perception 0. 摘要 本文提出了 CORE&#xff0c;一种概念简单、有效且通信高效的多智能体协作感知模型。 从合作重建的新颖角度解决了该任务&#xff1a; 合作主体共同提供对环境的更全面的观察整体观察可以作为…

Redis为什么这么快?高频面试题详解

Redis为什么这么快&#xff1f; Redis利用epoll来实现IO多路复用&#xff0c;将连接信息和事件放到队列中&#xff0c;一次放到文件事件分派器&#xff0c;事件分派器将事件分发给事件处理器。 Redis 是跑在单线程中的&#xff0c;所有的操作都是按照顺序线性执行的&#xf…

C进阶-动态内存管理

本章重点&#xff1a; 为什么存在动态内存分配&#xff1f; 动态内存函数的介绍 malloc free calloc realloc 常见的动态内存错误 经典的笔试题 柔性数组 1.为什么存在动态内存分配 开辟空间的方式有两个特点&#xff1a; 1. 空间开辟大小是固定的。 2. 数组在申明的…

Codeforces Round 846 (Div. 2) E. Josuke and Complete Graph 详解 数论分块

题目大意 题意来源 解题思路 首先我们假设存在 x x x满足 a , b ∈ [ l , r ] , g c d ( a , b ) x a,b\in[l,r],gcd(a,b)x a,b∈[l,r],gcd(a,b)x那么肯定 g c d ( ⌊ a / x ⌋ , ⌊ b / x ⌋ ) 1 就是互质 gcd(\lfloor a/x \rfloor, \lfloor b/x \rfloor)1就是互质 gcd(⌊a…

Js高级技巧—拖放

拖放基本功能实现 拖放是一种非常流行的用户界面模式。它的概念很简单&#xff1a;点击某个对象&#xff0c;并按住鼠标按钮不放&#xff0c;将 鼠标移动到另一个区域&#xff0c;然后释放鼠标按钮将对象“放”在这里。拖放功能也流行到了 Web 上&#xff0c;成为 了一些更传统…

如何制作电子画册更吸引读者?

电子画册是我们生活中最常见的书刊&#xff0c;无论是小公司还是大企业&#xff0c;都会制作一本属于自己的电子画册&#xff0c;可如何制作更加吸引读者的电子画册&#xff1f;这可难倒了好多人。 其实&#xff0c;制作一份好的电子画册需要一定的技巧和经验&#xff0c;比如…

【爬虫实战】python微博热搜榜Top50

一.最终效果 二.项目代码 2.1 新建项目 本文使用scrapy分布式、多线程爬虫框架编写的高性能爬虫&#xff0c;因此新建、运行scrapy项目3步骤&#xff1a; 1.新建项目: scrapy startproject weibo_hot 2.新建 spider: scrapy genspider hot_search "weibo.com" 3…

AI低代码维格云日历视图怎么用?

日历视图,是一个以天为单位,清晰展示当月所有日程的视图。在团队协作的过程中,我们常常会碰到以下场景: 制作项目日历,让团队成员知道每天需要完成什么任务; 制作排课表,给老师和教室安排课程; 制作会议日历,提醒团队成员进行每周计划与回顾; 制作营销日历,把握全年…

23面向对象案例1

目录 1、计算连续表达式的一个过程 2、优化后的代码 为什么不能return resultn&#xff1f; 3、用面向对象的方法可以解决冗余的问题&#xff0c;但是还是不能解决result的值可以被随意修改的问题 4、解决不能被随意修改的问题&#xff0c;可以将类属性改成私有变量吗&…

Marvin攻击25 年之久的RSA解密漏洞重现

1998 年发现的与 SSL 服务器中的 PKCS #1 v1.5 填充相关的缺陷&#xff08;据信已得到解决&#xff09;至今仍然影响着多个广泛使用的项目。 经过衡量端到端操作的广泛测试后&#xff0c;红帽研究人员发现了原始定时攻击的几种变体&#xff0c;统称为“马文攻击”&#xff0c;…

343. 整数拆分 96.不同的二叉搜索树

343. 整数拆分 设dp[i]表示拆分 数字i 出来的正整数相乘值最大的值 (i - j) * j,和dp[i - j] * j是获得dp[i]的两种乘法&#xff0c;在里面求最大值可以得到当前dp[i]的最大值&#xff0c;但是这一次的得出的最大值如果赋值给dp[i]&#xff0c;可能没有没赋值的dp[i]大&#…

springboot aop详解

springboot aop的实现方式有哪些 在 Spring Boot 中&#xff0c;AOP&#xff08;面向切面编程&#xff09;是通过 Spring Framework 提供的 AOP 模块来实现的。Spring AOP 提供了几种实现方式来应用切面编程&#xff0c;包括以下几种&#xff1a; 基于代理的 AOP&#xff1a; …

CSS变量 var()的用法

写在前面 这里介绍一下开发中常用的css变量var()&#xff0c;它可以实现样式的动态设置&#xff0c;使用方法主要分为全局使用和局部使用两种。 如何定义CSS变量var() 在CSS文件中&#xff0c;变量需要使用 – 作为前缀来定义&#xff0c;后面跟上变量名和值&#xff0c;如&a…

基于人工电场优化的BP神经网络(分类应用) - 附代码

基于人工电场优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于人工电场优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.人工电场优化BP神经网络3.1 BP神经网络参数设置3.2 人工电场算法应用 4.测试结果…

ARM +FPGA GPIB IP核实现

目前在数据发生其技术上居领先的是美国的 Tektronix 公司和 Agilent 公司。 Agilent 公司的台式脉冲 / 数据发生器家族的最高时钟频率达 3GHz &#xff08;定 时发生器&#xff09;&#xff0c;数据发生器 E81200 在通道数为 8CH 时数据速率为 660Mb/s, 即可以产…

22python生命周期的一种案例

第一个问题&#xff0c;什么叫生命周期&#xff1f; 生命周期就是一个对象从产生到消亡的一个周期过程&#xff0c;总体来说是一个这样的 展示一下生命周期的一点点的小案例吧~ 1、利用全局变量 #以下是一个python展示生命周期的方法过程 personCount 0 class Person:def _…