面试突击:为什么要使用读写锁?它有哪些优势?

每天早上七点三十,准时推送干货

一、摘要

在上篇文章中,我们讲到ReentrantLock可以保证了只有一个线程能执行加锁的代码。

但是有些时候,这种保护显的有点过头,比如下面这个方法,它仅仅就是只读取数据,不修改数据,它实际上允许多个线程同时调用的。

public class Counter {

    private final Lock lock = new ReentrantLock();

    private int count;

    public int get() {
        // 加锁
        lock.lock();
        try {
            return count;
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

站在程序性能的角度,实际上我们想要的是这样的效果。

  • 1.读和读之间不互斥,因为只读操作不会有数据安全问题
  • 2.写和写之间互斥,避免一个写操作影响另外一个写操作,引发数据计算错误问题
  • 3.读和写之间互斥,避免读操作的时候写操作修改了内容,引发数据脏读问题

总结起来就是,允许多个线程同时读,但只要有一个线程在写,其他线程就必须排队等待。

在 JDK 中有一个读写锁ReadWriteLock,使用它就可以解决这个问题,它可以保证以下两点:

  • 1.只允许一个线程写入,其他线程既不能写入也不能读取
  • 2.没有写入时,多个线程允许同时读,可以提高程序并发性能

实际上,读写锁ReadWriteLock里面有两个锁实现,一个是读操作相关的锁,称为共享锁,当多个线程同时操作时,不会让多个线程进行排队等待,大大的提升了程序并发读的执行效率;另一个是写操作相关的锁,称为排他锁,当多个线程同时操作时,只允许一个线程写入,其他线程进入排队等待;两者进行组合操作,就可以实现上面的预期效果。

下面我们一起来看看它的基本用法!

二、ReadWriteLock 基本用法

2.1、读和读共享

读和读之间不互斥,当多个线程进行读的时候,不会让多个线程进行排队等待。

我们可以看一个简单的例子!

public class Counter {

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    private int count;

    public void read() {
        // 加读锁
        lock.readLock().lock();
        try {
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
            System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + "获得了读锁,count:" + count);
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放读锁
            lock.readLock().unlock();
        }
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.read();
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.read();
            }
        });

        threadA.start();
        threadB.start();
    }
}

看一下运行结果:

2023-10-23 16:12:28:119 当前线程:Thread-0获得了读锁,count:0
2023-10-23 16:12:28:119 当前线程:Thread-1获得了读锁,count:0

从日志时间上可以很清晰的看到,尽管加锁了,并且休眠了 5 秒,但是两个线程还是几乎同时执行try()方法里面的代码,证明了读和读之间是不互斥的,可以显著提高程序的运行效率。

2.2、写和写之间互斥

写和写之间互斥,当多个线程进行写的时候,只允许一个线程写入,其他线程进入排队等待。

我们可以看一个简单的例子!

public class Counter {

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    private int count;

    public void write() {
        // 加写锁
        lock.writeLock().lock();
        try {
            count++;
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
            System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + "获得了写锁,count:" + count);
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放写锁
            lock.writeLock().unlock();
        }
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.write();
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.write();
            }
        });

        threadA.start();
        threadB.start();
    }
}

看一下运行结果:

2023-10-23 16:29:59:103 当前线程:Thread-0获得了写锁,count:1
2023-10-23 16:30:04:108 当前线程:Thread-1获得了写锁,count:2

从日志时间上可以很清晰的看到,两个线程进行串行执行,证明了写和写之间是互斥的。

2.3、读和写之间互斥

读和写之间互斥,当多个线程交替进行读写的时候,操作上互斥,只有一个线程能进入,其他线程进入排队等待。

我们可以看一个简单的例子!

public class Counter {

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    private int count;

    public void read() {
        // 加读锁
        lock.readLock().lock();
        try {
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
            System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + "获得了读锁,count:" + count);
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放读锁
            lock.readLock().unlock();
        }
    }

    public void write() {
        // 加写锁
        lock.writeLock().lock();
        try {
            count++;
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
            System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + "获得了写锁,count:" + count);
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放写锁
            lock.writeLock().unlock();
        }
    }
}

public class MyThreadTest {

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.read();
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.write();
            }
        });

        threadA.start();
        threadB.start();
    }
}

看一下运行结果:

2023-10-23 16:36:08:786 当前线程:Thread-0获得了读锁,count:0
2023-10-23 16:36:13:791 当前线程:Thread-1获得了写锁,count:1

从日志时间上可以很清晰的看到,两个线程进行串行执行,证明了读和写之间是互斥的。

三、小结

总结下来,ReadWriteLock有以下特点:

  • 允许多个线程在没有写入时同时读取,可以提高读取效率
  • 当存在写入情况时,只允许一个线程写入,其他线程进入排队等待
  • 适合读多写少的场景

对于同一个数据,有大量线程读取,但仅有少数线程修改,使用ReadWriteLock可以显著的提升程序并发执行效率。

例如,一个论坛的帖子,浏览可以看做读取操作,是非常频繁的,而回复可以看做写入操作,它是不频繁的,这种情况就可以使用ReadWriteLock来实现。

本文主要围绕ReadWriteLock的基本使用做了一次知识总结,如果有不正之处,请多多谅解,并欢迎批评指出。

四、参考

1. https://www.cnblogs.com/xrq730/p/4855631.html

2. https://www.liaoxuefeng.com/wiki/1252599548343744/1306581002092578



相关推荐

  • ChatGPT版iPhone终面世!Sam Altman联手苹果前员工祭出苹果杀手
  • 阿里云严重故障,阿里云、钉钉、闲鱼、淘宝、语雀等都崩了...
  • Kaggle知识点:检测 LLMs文本的方法
  • ​1112.AI日报:张朝阳谈 AI - 十年之内不能低估它,两年之内不能高估它
  • 项目总延期?需求乱插队?我们应如何做好项目管理
  • [开源]基于Go+Vue实现的OpenLDAP后台管理项目,简单易用清晰美观
  • 阿里云严重故障,阿里云、钉钉、闲鱼、淘宝、语雀等都崩了...
  • 一个指令,释放 1000 行屎山代码
  • 从ID-based到LLM-based:可迁移推荐系统发展
  • EMNLP 2023 | 基于知识图谱嵌入的关系感知集成学习算法
  • 开源跨平台终端仿真软件,多协议支持,个性化配置,无缝体验
  • 再看领域微调大模型的主流基座和评测数据集:项目地址及论文指引
  • Windows 全新虚拟机发布!
  • 分享图片
  • 成都周报 | 华微电子即将上市,7家企业获得投资
  • 为什么王者荣耀不使用微服务架构?
  • 能跟「猫主子」聊天了!生成式AI带来的全面革命:最快5年内破译第一种动物语言
  • Meta高管放弃82万美元年薪,离职创业!21年拿到全美TOP 1%收入,自称原因是运气好
  • 1分钟诞生一个新GPT!3天内定制GPT大爆发,理想型男友、科研利器全网刷屏
  • 最近收了一堆大佬简历,有阿里p8,华为18,快手k4,还有各种大企业部长总监,个个百万年薪,却来竞争一个40万年薪的岗位!