1.引子
又到了一周分享的时候,我们接着上一篇继续分享锁相关的案例。上一篇你已经知道了:可重入锁、公平锁、非公平锁。相信你都还记得,那么这一篇我们一起来看:读写锁。即你平常听说的共享锁(读锁),与排它锁(写锁) 。
在实际演示案例以前,应该争取先尝试搞清楚什么是共享锁,什么是排它锁,以及适合在什么场景下使用对吧。
我们先从字面上来理解,关于共享锁与排它锁,其实理解概念非常直观容易:所谓共享,即是你有我有大家有;所谓排它,即是有你没我,有我没你。
那么在实际应用当中,比如说今天周末,你约了几个朋友想去看电影,需要购买电影票,这个场景你一定不会陌生对吧。我们来尝试捋一下,购买电影票(在线上购买)通常你都有哪些操作:
- 打开某购买电影票应用,通常是App或者小程序
- 选择想要看的影片
- 查询影厅座位情况
- 选择喜欢的座位位置
- 锁定下单付款
好了,日常看电影购买电影票的操作已经回顾完了。现在请你转换回角色,你不是真的有机会享受电影,请牢牢记住:你是一个程序员。
老板说了,这周末要加班,因为你写的卖电影票的程序有点问题,虽然功能上卖票没什么问题,但是性能不行,Tps啊Qps根本上不去,用户体验不好。这个时候,请牢牢记住:你是一个程序员。你肯定不愿意相信这是你写的代码,对吧。
于是你要优化,你经过一番冥思苦想,梳理了一下核心关键业务场景,对于购买电影票来说,有两个核心操作:
- 查询影厅座位情况
- 选择锁定座位
因为是开放业务场景,一定存在多人同时并行查询座位、选择锁定座位操作。为了避免同一个座位,被两个人或者更多人重复选择购买的情况发生,你需要加锁保证线程安全,于是你的代码可能是这么写的:
public class Movies{
// 查询座位
public List query(){
// 加锁
lock.lock();
try{
// 查询影厅座位
......
}finally{
// 释放锁
lock.unlock();
}
}
public boolean buy(){
// 加锁
lock.lock();
try{
// 选择锁定座位
......
}finally{
// 释放锁
lock.unlock();
}
}
}
上面的代码初步看没有什么问题,查询操作与购买操作,都加了锁。可以保证不会出现同一个座位,被多个人同时购买,发生重复购买的问题。
但是你仔细想会发现,好像又有那么点问题:
- 如果有三个人A、B、C
- 其中A、B只是查询一下座位的情况,不准备购买电影票
- 只有C是真正想要看电影,想要给你钱的那一个人
这时候会有什么问题呢?A先来查询电影票,加锁;B来查询电影票,拿不到锁,阻塞等待;C来查询电影票,拿不到锁,阻塞等待。
你有没有发现,这个时候你写的程序(上面那段代码):要求ABC必须是一个一个来,哪怕B只是为了查询电影票情况,也必须要排队等待,导致用户体验不好,于是B用户心想下次换个App,这个App太烂了。不知不觉你就流失了一个用户......
于是老板要求你要加班解决这个问题,于是你也很想解决这个问题。你是这么想的:
- 在并发业务场景下,锁肯定是要锁的,程序功能的正确性永远是第一位的
- 问题是在锁的基础上,有没有一些更好的选择呢?
- 比如说:如果大家都是查询电影票,那就不要排队了,都来查询就是
- 只有确实有人要买电影票,选择座位后,再进行锁定排队
- 这样是不是既保证了程序功能的正确性,又同时提升了性能,还提升了用户体验呢
基于以上你的想法,我们确实有更好的选择,也就是我们这一篇的主角:共享锁(读锁)、排它锁(写锁)。下面我们统一叫做读锁、写锁。
读锁可以做到不互斥,大家都可以读;写锁互斥,保证程序功能的正确性;另外读写互斥,总结起来一句话:要么多读,要么一写。
好了,相信看到这里,你已经明白什么是读锁,什么是写锁,以及它们适用的业务场景。接下来我们通过juc包提供的ReentrantReadWriteLock来进行读写锁案例演示,开始吧
2.案例
2.1.案例代码
案例描述:
-
创建5个线程,3个线程进行读操作,2个线程进行写操作
-
在读操作,与写操作方法中,执行休眠100毫秒,模拟业务操作
-
预期执行结果:3个读操作线程,可以同时并行执行,不需要排队等待
-
预期执行结果:2个写操作线程,与3个读操作线程之间,相互排斥,需要排队等待
-
结论:
- 读锁不互斥
- 写锁互斥
- 读锁,与写锁互斥
-
总结:要么多读,要么一写
package com.anan.edu.common.newthread.lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写锁案例
*
* @author ThinkPad
* @version 1.0
* @date 2020/11/7 19:35
*/
public class ReadWriteLockDemo {
/**
* 创建锁对象
* 读锁:readLock
* 写锁:writeLock
*/
private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
/**
* 执行入口
* @param args
*/
public static void main(String[] args) {
// 创建5个线程:3个执行读操作,2个执行写操作
for(int i = 0; i < 5; i ++){
// 读操作
if(i <= 2){
new Thread(() ->{doRead();},"读线程:" + i).start();
}else{
// 写操作
new Thread(() ->{doWrite();},"写线程:" + i).start();
}
}
}
/**
* 读方法,允许多读,即多个线程同时读
*/
public static void doRead(){
// 加锁
readLock.lock();
try{
System.out.println("线程【" + Thread.currentThread().getName() + "】获取到读锁:readLock.正在执行读操作...");
// 休眠100毫秒,模拟任务执行
Thread.sleep(100);
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 释放锁
System.out.println("线程【" + Thread.currentThread().getName() + "】执行完毕.准备释放读锁:readLock.");
readLock.unlock();
}
}
/**
* 写方法,只允许一写,即同一时刻只能一个线程写
*/
public static void doWrite(){
// 加锁
writeLock.lock();
try{
System.out.println("线程【" + Thread.currentThread().getName() + "】获取到写锁:writeLock.正在执行写操作...");
// 休眠100毫秒,模拟任务执行
Thread.sleep(100);
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 释放锁
System.out.println("线程【" + Thread.currentThread().getName() + "】执行完毕.准备释放写锁:writeLock.");
writeLock.unlock();
}
}
}
2.2.执行结果及分析
D:\02teach\01soft\jdk8\bin\java com.anan.edu.common.newthread.lock.ReadWriteLockDemo
线程【读线程:0】获取到读锁:readLock.正在执行读操作...
线程【读线程:2】获取到读锁:readLock.正在执行读操作...
线程【读线程:1】获取到读锁:readLock.正在执行读操作...
线程【读线程:0】执行完毕.准备释放读锁:readLock.
线程【读线程:1】执行完毕.准备释放读锁:readLock.
线程【读线程:2】执行完毕.准备释放读锁:readLock.
线程【写线程:3】获取到写锁:writeLock.正在执行写操作...
线程【写线程:3】执行完毕.准备释放写锁:writeLock.
线程【写线程:4】获取到写锁:writeLock.正在执行写操作...
线程【写线程:4】执行完毕.准备释放写锁:writeLock.
Process finished with exit code 0