zoukankan      html  css  js  c++  java
  • 关于ReadWriteLock

    一.ReadWriteLock是什么

    ReadWriteLock是一个接口,提供了readLock和writeLock两种锁的操作机制,一个是只读的锁,一个是写锁。

    读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的(排他的)。 每次只能有一个写线程,但是可以有多个线程并发地读数据。

    所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。

    理论上,读写锁比互斥锁允许对于共享数据更大程度的并发。

    与互斥锁相比,读写锁是否能够提高性能取决于读写数据的频率、读取和写入操作的持续时间、以及读线程和写线程之间的竞争。

    synchronized和ReentrantLock实现的锁是排他锁,所谓排他锁就是同一时刻只允许一个线程访问共享资源,但是在平时场景中,通常会碰到对于共享资源读多写少的场景。

    对于读场景,每次只允许一个线程访问共享资源,显然这种情况使用排他锁效率就比较低下,那么该如何优化呢?

    这个时候读写锁就应运而生了,读写锁是一种通用技术,并不是Java特有的。从名字来看,读写锁拥有两把锁,读锁和写锁。

    读写锁的特点是:同一时刻允许多个线程对共享资源进行读操作;同一时刻只允许一个线程对共享资源进行写操作;

    当进行写操作时,同一时刻其他线程的读操作会被阻塞;当进行读操作时,同一时刻所有线程的写操作会被阻塞。

    对于读锁而言,由于同一时刻可以允许多个线程访问共享资源,进行读操作,因此称它为共享锁;而对于写锁而言,同一时刻只允许一个线程访问共享资源,进行写操作,因此称它为排他锁。

    在Java中通过ReadWriteLock来实现读写锁。ReadWriteLock是一个接口,ReentrantReadWriteLock是ReadWriteLock接口的具体实现类。

    在ReentrantReadWriteLock中定义了两个内部类ReadLock、WriteLock,分别来实现读锁和写锁。

    ReentrantReadWriteLock底层是通过AQS来实现锁的获取与释放的,因此ReentrantReadWriteLock内部还定义了一个继承了AQS类的同步组件Sync,

    同时ReentrantReadWriteLock还支持公平与非公平性,因此它内部还定义了两个内部类FairSync、NonfairSync,它们继承了Sync。

    二.ReadWriteLock能做什么

    说到Java并发编程,经常常用的肯定是Synchronized,但是Synchronized存在明显的一个性能问题就是读与读之间互斥,

    简言之就是,编程想要实现的最好效果是,可以做到读和读互不影响,读和写互斥,写和写互斥,提高读写的效率,如何实现呢?

    对象的方法中一旦加入synchronized修饰,则任何时刻只能有一个线程访问synchronized修饰的方法。

    假设有个数据对象拥有写方法与读方法,多线程环境中要想保证数据的安全,需对该对象的读写方法都要加入 synchronized同步块。

    这样任何线程在写入时,其它线程无法读取与改变数据;如果有线程在读取时,其他线程也无法读取或写入。

    这种方式在写入操作远大于读操作时,问题不大,而当读取远远大于写入时,会造成性能瓶颈,因为此种情况下读取操作是可以同时进行的,而加锁操作限制了数据的并发读取。  

    ReadWriteLock解决了这个问题,当写操作时,其他线程无法读取或写入数据,而当读操作时,其它线程无法写入数据,但却可以读取数据 。

    三.ReadWriteLock原理

    在并发场景中用于解决线程安全的问题,几乎会高频率的使用到独占式锁,通常使用java提供的关键字synchronized或者concurrents包中实现了Lock接口的ReentrantLock。

    它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。而在一些业务场景中,大部分只是读数据,写数据很少,

    如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。

    针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。

    读写所允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。

    (1)公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;

    (2)重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;

    (3)锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁

    Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。

    两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象。

    读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,我们只要上好相应的锁即可。

    如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!

    读写锁接口:ReadWriteLock,它的具体实现类为:ReentrantReadWriteLock

    在多线程的环境下,对同一份数据进行读写,会涉及到线程安全的问题。

    比如在一个线程读取数据的时候,另外一个线程在写数据,而导致前后数据的不一致性;一个线程在写数据的时候,另一个线程也在写,同样也会导致线程前后看到的数据的不一致性。

    这时候可以在读写方法中加入互斥锁,任何时候只能允许一个线程的一个读或写操作,而不允许其他线程的读或写操作,这样是可以解决这样以上的问题,但是效率却大打折扣了。

    因为在真实的业务场景中,一份数据,读取数据的操作次数通常高于写入数据的操作,而线程与线程间的读读操作是不涉及到线程安全的问题,没有必要加入互斥锁,只要在读-写,写-写期间上锁就行了。

    对于以上这种情况,读写锁是最好的解决方案!其中它的实现类:ReentrantReadWriteLock--顾名思义是可重入的读写锁,允许多个读线程获得ReadLock,但只允许一个写线程获得WriteLock。

    读写锁的机制:

    (1)"读-读" 不互斥——共存

    (2)"读-写" 互斥——不能共存

    (3)"写-写" 互斥——不能共存

    四.ReadWriteLock使用

    使用示例:

    public class TestReadWriteLock {
    
        public static void main(String[] args){
            ReadWriteLockDemo rwd = new ReadWriteLockDemo();
            //启动100个读线程
            for (int i = 0; i < 100; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        rwd.get();
                    }
                }).start();
            }
            //写线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    rwd.set((int)(Math.random()*101));
                }
            },"Write").start();
        }
    }
    
    class ReadWriteLockDemo{
        //模拟共享资源--Number
        private int number = 0;
        // 实际实现类--ReentrantReadWriteLock,默认非公平模式
        private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    
        //
        public void get(){
            //使用读锁
            readWriteLock.readLock().lock();
            try {
                System.out.println(Thread.currentThread().getName()+" : "+number);
            }finally {
                readWriteLock.readLock().unlock();
            }
        }
        //
        public void set(int number){
            readWriteLock.writeLock().lock();
            try {
                this.number = number;
                System.out.println(Thread.currentThread().getName()+" : "+number);
            }finally {
                readWriteLock.writeLock().unlock();
            }
        }
    }

    测试结果如下图:

     

     首先启动读线程,此时number为0;然后某个时刻写线程修改了共享资源number数据,读线程再次读取最新值!

    四.ReentrantReadWriteLock使用

    ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁。

    线程进入读锁的前提条件:

    (1)没有其他线程的写锁;

    (2)没有写请求,或者有写请求但调用线程和持有锁的线程是同一个线程;

    进入写锁的前提条件:

    (1)没有其他线程的读锁

    (2)没有其他线程的写锁

    需要提前了解的概念:

    锁降级:从写锁变成读锁;   

    锁升级:从读锁变成写锁。   

    读锁是可以被多线程共享的,写锁是单线程独占的。也就是说写锁的并发限制比读锁高,这可能就是升级/降级名称的来源。   

    如下代码会产生死锁,因为同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock是不支持的。

    实际生产环境中的缓存案例:

    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    public class CacheDemo {
        /**
         * 缓存器,这里假设需要存储1000左右个缓存对象,按照默认的负载因子0.75,则容量=750,大概估计每一个节点链表长度为5个
         * 那么数组长度大概为:150,又有雨设置map大小一般为2的指数,则最近的数字为:128
         */
        private Map<String, Object> map = new HashMap<>(128);
        private ReadWriteLock rwl = new ReentrantReadWriteLock();
        public static void main(String[] args) {
    
        }
    public Object get(String id){ Object value = null; rwl.readLock().lock();//首先开启读锁,从缓存中去取 try{ if(map.get(id) == null){ //如果缓存中没有释放读锁,上写锁 rwl.readLock().unlock(); rwl.writeLock().lock(); try{ if(value == null){ //防止多写线程重复查询赋值 value = "redis-value"; //此时可以去数据库中查找,这里简单的模拟一下 } rwl.readLock().lock(); //加读锁降级写锁,不明白的可以查看上面锁降级的原理与保持读取数据原子性的讲解 }finally{ rwl.writeLock().unlock(); //释放写锁 } } }finally{ rwl.readLock().unlock(); //最后释放读锁 } return value; } }

    五.总结

    (1)Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性;

    (2)ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字;

    (3)ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的;

    (4)ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。

  • 相关阅读:
    Docker login报错一例
    Ubuntu 18.04设置dns
    docker日志引擎说明
    Docker简介与安装配置
    使用traefik作为kubernetes的ingress
    Axiom3D:Ogre动画基本流程与骨骼动画
    Axiom3D:Ogre中Mesh文件格式分析(一)
    Axiom3D:数据绑定基本流程
    3D引擎Axiom的选择与学习.
    初试PyOpenGL四 (Python+OpenGL)GPU粒子系统与基本碰撞
  • 原文地址:https://www.cnblogs.com/ZJOE80/p/12881589.html
Copyright © 2011-2022 走看看