章节索引 :

读写锁 ReentrantReadWriteLock

1. 前言

本节内容主要是对 Java 读写锁 ReentrantReadWriteLock 进行讲解,本节内容几乎全部为重点知识,需要学习者对 ReentrantReadWriteLock 进行理解和掌握。本节内容的知识点如下:

  • ReentrantReadWriteLock 简单介绍,对 ReentrantReadWriteLock 进行一个总体的概括;
  • ReentrantReadWriteLock 的类结构,从 Java 层面了解 ReentrantReadWriteLock;
  • ReentrantReadWriteLock 的特点,相比于上两点知识,该知识点可视为重点;
  • ReentrantReadWriteLock 读锁共享的性质验证,为本节核心内容之一;
  • ReentrantReadWriteLock 读写互斥的性质验证,为本节核心内容之一。

ReentrantReadWriteLock 在 Java 的锁当中也占据着十分重要的地位,在并发编程中使用频率也是非常的高,一定要对本节内容进行细致的学习和掌握。

2. ReentrantReadWriteLock 介绍

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

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

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

3. ReentrantReadWriteLock 的类结构

ReentrantReadWriteLock 是接口 ReadWriteLock 的子类实现,通过 JDK 的代码可以看出这一实现关系。

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable{}

我们再来看下接口 ReadWriteLock,该接口只定义了两个方法:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

通过调用相应方法获取读锁或写锁,可以如同使用 Lock 接口一样使用。

4. ReentrantReadWriteLock 的特点

性质 1 :可重入性。

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

性质 2 :读写分离。

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

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

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

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

图片描述

性质 4 :不可锁升级。

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

图片描述

5. 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();
    }
}

结果验证

threadName -> t1 占用读锁,i->0
threadName -> t2 占用读锁,i->0
threadName -> t3 占用读锁,i->0
threadName -> t1 释放读锁,i->0
threadName -> t3 释放读锁,i->0
threadName -> t2 释放读锁,i->0

结果分析:从结果来看,t1,t2,t3 均在同一时间获取了锁,证明了读锁共享的性质。

6. 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();
    }
}

结果验证

threadName -> t1 占用读锁,i->0
threadName -> t1 释放读锁,i->0
threadName -> t2 占用写锁,i->0
threadName -> t2 释放写锁,i->1

结果解析:验证成功,在线程 t1 沉睡的过程中,写锁 t2 线程无法获取锁,因为锁已经被读操作 t1 线程占据了。

7. 小结

本节内容只要是对读写锁 ReentrantReadWriteLock 进行的比较细致的讲解,对于本节的内容几乎通篇为重点内容。

其中核心知识点为读锁共享和读写互斥的验证,所有的知识点都是围绕这两个话题进行讲解的,有兴趣的同学可以根据实例代码进行写锁互斥的验证。唯一不同的地方就是创建两个写线程进行写锁的获取。

掌握本节知识点,有助于我们在特定的场景下对读写锁进行应用。