读写锁 ReentrantReadWriteLock

ReentrantReadWriteLock 介绍

ReentrantReadWriteLock 是 Java 中的一个读写锁实现,它包含了两种锁:读锁和写锁。读锁是共享锁,多个线程可以同时获取读锁进行读操作,但是写锁是排他锁,只有一个线程可以获取写锁进行写操作。ReentrantReadWriteLock 提供了比单一锁更高效的并发性能,特别适用于读多写少的场景。

JDK 提供了 ReentrantReadWriteLock 读写锁,使用它可以加快效率,在某些不需要操作实例变量的方法中,完全可以使用读写锁 ReemtrantReadWriteLock 来提升该方法的运行速度。

定义:读写锁表示有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排他锁。

定义解读:也就是多个读锁之间不互斥,读锁与写锁互斥、写锁与写锁互斥。在没有线程 Thread 进行写入操作时,进行读取操作的多个 Thread 都可以获取读锁,而进行写入操作的 Thread 只有在获取写锁后才能进行写入操作。即多个 Thread 可以同时进行读取操作,但是同一时刻只允许一个 Thread 进行写入操作。

ReentrantReadWriteLock 的主要特点包括:

  1. 读写分离:读操作可以并发进行,写操作是排他的。
  2. 可重入性:读锁和写锁都支持重入。
  3. 公平性:可以选择是否公平地获取读写锁。
  4. 锁降级:可以将写锁降级为读锁,但不能将读锁升级为写锁。
  5. 条件变量:支持 Condition 条件变量。

ReentrantReadWriteLock 的主要方法包括:

  1. readLock():获取读锁。
  2. writeLock():获取写锁。
  3. readLock().lock():获取读锁。
  4. writeLock().lock():获取写锁。
  5. readLock().unlock():释放读锁。
  6. writeLock().unlock():释放写锁。

ReentrantReadWriteLock 的特点

性质 1 :可重入性。

ReentrantReadWriteLock 与 ReentrantLock 以及 synchronized 一样,都是可重入性锁,这里不会再多加赘述所得可重入性质,之前已经做过详细的讲解。

性质 2 :读写分离。

我们知道,对于一个数据,不管是几个线程同时读都不会出现任何问题,但是写就不一样了,几个线程对同一个数据进行更改就可能会出现数据不一致的问题,因此想出了一个方法就是对数据加锁,这时候出现了一个问题:

线程写数据的时候加锁是为了确保数据的准确性,但是线程读数据的时候再加锁就会大大降低效率,这时候怎么办呢?那就对写数据和读数据分开,加上两把不同的锁,不仅保证了正确性,还能提高效率。

性质 3 :可以锁降级,写锁降级为读锁。

线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。

 

性质 4 :不可锁升级。

线程获取读锁是不能直接升级为写入锁的。需要释放所有读取锁,才可获取写锁。

 

复制代码
public class LockExample3 {

    private final Map<String, Data> map = new TreeMap<>();

    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private final Lock readLock = lock.readLock();

    private final Lock writeLock = lock.writeLock();

    public Data get(String key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public Set<String> getAllKeys() {
        readLock.lock();
        try {
            return map.keySet();
        } finally {
            readLock.unlock();
        }
    }

    public Data put(String key, Data value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            readLock.unlock();
        }
    }

    class Data {

    }
}
复制代码

ReentrantReadWriteLock 读锁共享

我们之前说过,ReentrantReadWriteLock 之所以优秀,是因为读锁与写锁是分离的,当所有的线程都为读操作时,不会造成线程之间的互相阻塞,提升了效率,那么接下来,我们通过代码实例进行学习。

场景设计:

  • 创建三个线程,线程名称分别为 t1,t2,t3,线程实现方式自行选择;
  • 三个线程同时运行获取读锁,读锁成功后打印线程名和获取结果,并沉睡 2000 毫秒,便于观察其他线程是否可共享读锁;
  • finally 模块中释放锁并打印线程名和释放结果;
  • 运行程序,观察结果。

结果预期:三条线程能同时获取锁,因为读锁共享。

复制代码
public class DemoTest {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();// 读写锁
    private int i;
    public String readI() {
        try {
            lock.readLock().lock();// 占用读锁
            System.out.println("threadName -> " + Thread.currentThread().getName() + " 占用读锁,i->" + i);
            Thread.sleep(2000);
        } catch (InterruptedException e) {

        } finally {
            System.out.println("threadName -> " + Thread.currentThread().getName() + " 释放读锁,i->" + i);
            lock.readLock().unlock();// 释放读锁
        }
        return i + "";
    }

    public static void main(String[] args) {
        final DemoTest demo1 = new DemoTest();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                demo1.readI();
            }
        };
        new Thread(runnable, "t1"). start();
        new Thread(runnable, "t2"). start();
        new Thread(runnable, "t3"). start();
    }
}
复制代码

ReentrantReadWriteLock 读写互斥

当共享变量有写操作时,必须要对资源进行加锁,此时如果一个线程正在进行读操作,那么写操作的线程需要等待。同理,如果一个线程正在写操作,读操作的线程需要等待。

场景设计:细节操作不详细阐述,看示例代码即可。

  • 创建两个线程,线程名称分别为 t1,t2;
  • 线程 t1 进行读操作,获取到读锁之后,沉睡 5000 毫秒;
  • 线程 t2 进行写操作;
  • 开启 t1,1000 毫秒后开启 t2 线程;
  • 运行程序,观察结果。

结果预期:线程 t1 获取了读锁,在沉睡的 5000 毫秒中,线程 t2 只能等待,不能获取到锁,因为读写互斥。

复制代码
public class DemoTest {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();// 读写锁
    private int i;
    public String readI() {
        try {
            lock.readLock().lock();// 占用读锁
            System.out.println("threadName -> " + Thread.currentThread().getName() + " 占用读锁,i->" + i);
            Thread.sleep(5000);
        } catch (InterruptedException e) {
        } finally {
            System.out.println("threadName -> " + Thread.currentThread().getName() + " 释放读锁,i->" + i);
            lock.readLock().unlock();// 释放读锁
        }
        return i + "";
    }

    public void addI() {
        try {
            lock.writeLock().lock();// 占用写锁
            System.out.println("threadName -> " + Thread.currentThread().getName() + " 占用写锁,i->" + i);
            i++;
        } finally {
            System.out.println("threadName -> " + Thread.currentThread().getName() + " 释放写锁,i->" + i);
            lock.writeLock().unlock();// 释放写锁
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final DemoTest demo1 = new DemoTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                demo1.readI();
            }
        }, "t1"). start();
        Thread.sleep(1000);
        new Thread(new Runnable() {
            @Override
            public void run() {
                demo1.addI();
            }
        }, "t2"). start();
    }
}
复制代码

 

posted @   黄橙  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示