高级并发编程系列十一(读写锁案例)

140 阅读6分钟

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