可重入读写锁 ReentrantReadWriteLock
1. 前言
从本节开始,我们学习新一章内容 —— 并发锁。
在 “Java 并发原理入门教程” 中,介绍了锁相关概念和原理知识,本章各小节内容不再过多解释概念,重点为大家介绍具体锁工具的 API 和使用方法。
本节带领大家认识第一个常用的 Java 并发锁工具之 ReentrantReadWriteLock。
本节先简单介绍 ReentrantReadWriteLock 的基本概念,然后介绍关键的编程方法,最后通过一个编程例子为大家展示 ReentrantReadWriteLock 工具类的用法。
下面我们正式开始介绍吧。
2. 概念解释
ReadWriteLock 是一个接口类型,翻译为 “读写锁”。多个线程同时读某个资源时,为了满足并发量,不应该加锁处理,但是如果有一个线程写这个资源,就不应该再有其它线程对该资源进行读操作或者写操作。
实现了此接口的类提供了 “读读能共存、读写不能共存、写写不能共存” 的控制逻辑。当同一个资源被大量线程读取,但仅有少数线程修改时,使用 ReadWriteLock 可大大提高并发效率。比如对一个电商网站的商品,回复评论(写)是不频繁的,但是浏览(读)是非常频繁的,这种情况使用 ReadWriteLock 工具类做并发控制非常适合。总之适合读多写少的场景。
ReentrantLock 翻译为 “可重入锁”。“可重入” 是什么意思呢?就是指一个线程可以多次获取该锁,Java 中 在语言语法层提供的 syncrinized 是最常见的用于并发控制的关键字,其构成的锁也是可重入的。可重入锁在一定程度可以避免死锁的发生。
更多关于锁的原理,可阅读 “Java 并发原理入门教程”。
ReentrantReadWriteLock 类实现了 “读写锁” 和 “可重入锁” 的双重功能。其本身不提供加锁服务,只负责提供读锁和写锁。在介绍关键编程方法之前,我们先看一张图整体了解关键方法的使用方式。
下面我们学习其关键的编程方法。
3.ReentrantReadWriteLock 的编程方法
构造方法 ReentrantReadWriteLock () 和 ReentrantReadWriteLock (boolean fair)
有两个构造方法,在构造对象时,可选择是否构造为公平锁模式。什么是公平锁呢?公平锁指的是获取所的线程按照申请锁的线程获取,而非乱序随机获取到锁权限。
writeLock () 和 readLock ()
这两个方法分别用于获取写锁和获取读锁,对读操作的逻辑使用读锁对象加锁,对写操作的逻辑使用写锁对象加锁,如此可以做到 “读读能共存、读写不能共存、写写不能共存”,在实现线程安全的情况下,提高并发效率。
isFair()
获取锁是否具有公平性,此方法返回 boolean 值。
getWriteHoldCount () 、 getReadHoldCount () 和 getReadLockCount ()
getWriteHoldCount () 方法用于获取当前线程的写锁计数,getReadHoldCount () 方法用于获取当前线程的读锁计数,getReadLockCount () 方法用于获取总的读锁计数。所有方法均返回 int 值。
4.ReentrantReadWriteLock.ReadLock 的编程方法
此类是一个静态内部类,封装在 ReentrantReadWriteLock 中,此类不对外提供构造方法,由 ReentrantReadWriteLock.readLock () 方法获取此类对象。
lock () 和 lockInterruptibly ()
两个方法都用于加读锁,第二个方法可被中断。
tryLock () 和 tryLock (long timeout, TimeUnit unit)
两个方法都返回 boolean 值,用于尝试加读锁,第一个方法在尝试不成功时立刻返回,第二个方法可在指定时间内尝试加读锁。这两个方法提供了加锁的柔性,提供了更多操作空间。
unlock()
释放读锁。
5.ReentrantReadWriteLock.WriteLock 的编程方法
此类是一个静态内部类,封装在 ReentrantReadWriteLock 中,此类不对外提供构造方法,由 ReentrantReadWriteLock.writeLock () 方法获取此类对象。
lock () 和 lockInterruptibly ()
两个方法都用于加写锁,第二个方法可被中断。
tryLock () 和 tryLock (long timeout, TimeUnit unit)
两个方法都返回 boolean 值,用于尝试加写锁,第一个方法在尝试不成功时立刻返回,第二个方法可在指定时间内尝试加写锁。这两个方法提供了加锁的柔性,提供了更多操作空间。
unlock()
释放写锁。
6. 编程案例
上面介绍了核心编程方法,我们举一个编程案例,实际体会一下 ReentrantReadWriteLock 的用法。
import lombok.SneakyThrows;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
// 创建读写锁对象
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 获取读锁对象
private final Lock readlock = readWriteLock.readLock();
// 获取写锁对象
private final Lock writelock = readWriteLock.writeLock();
// 待控制的资源
private int account = 0;
private static ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();
public static void main(String[] args) {
new Thread(new Runnable() {
@SneakyThrows
public void run() {
while(true) {
Thread.sleep(1000);
int tmp = readWriteLockTest.get();
System.out.println("读操作:" + tmp);
}
}
}).start();
new Thread(new Runnable() {
@SneakyThrows
public void run() {
while(true) {
Thread.sleep(2000);
readWriteLockTest.add(10);
}
}
}).start();
}
public void add(int value) {
// 加写锁
writelock.lock();
try {
account += 1;
} finally {
// 释放写锁
writelock.unlock();
}
}
public int get() {
// 加读锁
readlock.lock();
try {
return account;
} finally {
// 释放读锁
readlock.unlock();
}
}
}
运行上面代码一段时间后结果如下:
读操作:0
读操作:1
读操作:1
读操作:2
读操作:2
注意在使用时,获取锁的操作 lock () 应该放在 try 之前,而释放锁的操作 unlock () 需要放在 finally 中,可确保锁释放。
7. 小结
本节解释了 ReentrantReadWriteLock 的基本概念和应用场合,且通过一个简单的例子展示了其用法,更多关于此工具类的概念和原理介绍,可阅读 “Java 并发原理入门教程” 。希望大家在学习过程中,多思考勤练习,早日掌握之。