关于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读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。

原文地址:https://www.cnblogs.com/ZJOE80/p/12881589.html