9.JUC线程高级-ReadWriteLock读写锁

简介: 前面我们讲解了Lock的使用,下面我们来讲解一下ReadWriteLock锁的使用,顾明思义,读写锁在读的时候,上读锁,在写的时候,上写锁,这样就很巧妙的解决synchronized的一个性能问题:读与读之间互斥。

前面我们讲解了Lock的使用,下面我们来讲解一下ReadWriteLock锁的使用,顾明思义,读写锁在读的时候,上读锁,在写的时候,上写锁,这样就很巧妙的解决synchronized的一个性能问题:读与读之间互斥

ReadWriteLock也是一个接口,原型如下:

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

该接口只有两个方法,读锁和写锁。也就是说,我们在写文件的时候,可以将读和写分开,分成2个锁来分配给线程,从而可以做到读和读互不影响读和写互斥写和写互斥,提高读写文件的效率。该接口也有一个实现类ReentrantReadWriteLock,下面我们就来学习下这个类。

我们先看一下,多线程同时读取文件时,用synchronized实现的效果,代码如下:


public class ReadAndWriteLock {
    public synchronized void get(Thread thread) {
        System.out.println("start time:"+System.currentTimeMillis());
        for(int i=0; i<5; i++){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(thread.getName() + ":正在进行读操作……");
        }
        System.out.println(thread.getName() + ":读操作完毕!");
        System.out.println("end time:"+System.currentTimeMillis());
    }
    
    public static void main(String[] args) {
        final ReadAndWriteLock lock = new ReadAndWriteLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();
    }
}

测试结果如下:

start time:1442459467623
Thread-0:正在进行读操作……
Thread-0:正在进行读操作……
Thread-0:正在进行读操作……
Thread-0:正在进行读操作……
Thread-0:正在进行读操作……
Thread-0:读操作完毕!
end time:1442459467723
start time:1442459467723
Thread-1:正在进行读操作……
Thread-1:正在进行读操作……
Thread-1:正在进行读操作……
Thread-1:正在进行读操作……
Thread-1:正在进行读操作……
Thread-1:读操作完毕!
end time:1442459467823

整个过程耗时200ms

我们可以看到,及时是在读取文件,在加了synchronized关键字之后,读与读之间,也是互斥的,也就是说,必须等待Thread-0读完之后,才会轮到Thread-1线程读,而无法做到同时读文件,这种情况在大量线程同时都需要读文件的时候,读写锁的效率,明显要高于synchronized关键字的实现。下面我们来测试一下,代码如下:

public class ReadAndWriteLock {
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public void get(Thread thread) {
        lock.readLock().lock();
        try{
            System.out.println("start time:"+System.currentTimeMillis());
            for(int i=0; i<5; i++){
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在进行读操作……");
            }
            System.out.println(thread.getName() + ":读操作完毕!");
            System.out.println("end time:"+System.currentTimeMillis());
        }finally{
            lock.readLock().unlock();
        }
    }
    
    public static void main(String[] args) {
        final ReadAndWriteLock lock = new ReadAndWriteLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.get(Thread.currentThread());
            }
        }).start();
    }
}

测试结果如下:

start time:1442460030593
start time:1442460030593
Thread-1:正在进行读操作……
Thread-0:正在进行读操作……
Thread-1:正在进行读操作……
Thread-0:正在进行读操作……
Thread-1:正在进行读操作……
Thread-0:正在进行读操作……
Thread-1:正在进行读操作……
Thread-0:正在进行读操作……
Thread-1:正在进行读操作……
Thread-0:正在进行读操作……
Thread-0:读操作完毕!
Thread-1:读操作完毕!
end time:1442460030693
end time:1442460030693

整个过程耗时:100ms

从测试结果来看,Thread-0和Thread-1是在同时读取文件。

通过两次实验的对比,我们可以看出来,读写锁的效率明显高于synchronized关键字

不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。读锁写锁互斥的。

下面我们来验证下读写锁的互斥关系,代码如下:

public class ReadAndWriteLock {
   ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public static void main(String[] args) {
        final ReadAndWriteLock lock = new ReadAndWriteLock();
    // 建N个线程,同时读
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(new Runnable() {
            @Override
            public void run() {
                lock.readFile(Thread.currentThread());
            }
        });
        // 建N个线程,同时写
        ExecutorService service1 = Executors.newCachedThreadPool();
        service1.execute(new Runnable() {
            @Override
            public void run() {
                lock.writeFile(Thread.currentThread());
            }
        });
    }
    // 读操作
    public void readFile(Thread thread){
        lock.readLock().lock();
        boolean readLock = lock.isWriteLocked();
        if(!readLock){
            System.out.println("当前为读锁!");
        }
        try{
            for(int i=0; i<5; i++){
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在进行读操作……");
            }
            System.out.println(thread.getName() + ":读操作完毕!");
        }finally{
         System.out.println("释放读锁!");
            lock.readLock().unlock();
        }
    }
    // 写操作
    public void writeFile(Thread thread){
        lock.writeLock().lock();
        boolean writeLock = lock.isWriteLocked();
        if(writeLock){
            System.out.println("当前为写锁!");
        }
        try{
            for(int i=0; i<5; i++){
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在进行写操作……");
            }
            System.out.println(thread.getName() + ":写操作完毕!");
        }finally{
         System.out.println("释放写锁!");
            lock.writeLock().unlock();
        }
    }
}

测试结果如下:

// 读锁和读锁测试结果:
当前为读锁!
当前为读锁!
pool-2-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-2-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-2-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-2-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-2-thread-1:正在进行读操作……
pool-1-thread-1:读操作完毕!
pool-2-thread-1:读操作完毕!
释放读锁!
释放读锁!
// 测试结果不互斥
 
// 读锁和写锁,测试结果如下:
当前为读锁!
pool-1-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-1-thread-1:正在进行读操作……
pool-1-thread-1:读操作完毕!
释放读锁!
当前为写锁!
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:写操作完毕!
释放写锁!
// 测试结果互斥
 
// 写锁和写锁,测试结果如下:
当前为写锁!
pool-1-thread-1:正在进行写操作……
pool-1-thread-1:正在进行写操作……
pool-1-thread-1:正在进行写操作……
pool-1-thread-1:正在进行写操作……
pool-1-thread-1:正在进行写操作……
pool-1-thread-1:写操作完毕!
释放写锁!
当前为写锁!
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:正在进行写操作……
pool-2-thread-1:写操作完毕!
释放写锁!
// 测试结果互斥

关于锁的内容我们就讲到这里,下面对锁的相关概念做一个介绍:

1.可重入(Reentrant)锁

如果锁具备可重入性,则称作为可重入锁。像synchronized和 ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一 个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法 method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

看下面这段代码就明白了:

classMyClass {
    public synchronized void method1() {
        method2();
    }
    public synchronized void method2() {
    }
}

上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程 A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请 锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。  而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。

2.可中断锁

可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

3.公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。

而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。设置方法如下:

ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);

4、读写锁

前面已经介绍,这里不做赘述

目录
相关文章
|
11月前
|
缓存 安全 Java
JUC系列之《CountDownLatch:同步多线程的精准发令枪 》
CountDownLatch是Java并发编程中用于线程协调的同步工具,通过计数器实现等待机制。主线程等待多个工作线程完成任务后再继续执行,适用于资源初始化、高并发模拟等场景,具有高效、灵活、线程安全的特点,是JUC包中实用的核心组件之一。
|
11月前
|
设计模式 缓存 安全
【JUC】(6)带你了解共享模型之 享元和不可变 模型并初步带你了解并发工具 线程池Pool,文章内还有饥饿问题、设计模式之工作线程的解决于实现
JUC专栏第六篇,本文带你了解两个共享模型:享元和不可变 模型,并初步带你了解并发工具 线程池Pool,文章中还有解决饥饿问题、设计模式之工作线程的实现
686 2
|
11月前
|
Java 测试技术 API
【JUC】(1)带你重新认识进程与线程!!让你深层次了解线程运行的睡眠与打断!!
JUC是什么?你可以说它就是研究Java方面的并发过程。本篇是JUC专栏的第一章!带你了解并行与并发、线程与程序、线程的启动与休眠、打断和等待!全是干货!快快快!
1272 2
|
11月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
503 1
|
存储 缓存 安全
JUC并发—11.线程池源码分析
本文主要介绍了线程池的优势和JUC提供的线程池、ThreadPoolExecutor和Excutors创建的线程池、如何设计一个线程池、ThreadPoolExecutor线程池的执行流程、ThreadPoolExecutor的源码分析、如何合理设置线程池参数 + 定制线程池。
JUC并发—11.线程池源码分析
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
1419 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
364 6
|
Java C++
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
289 0
|
Java 关系型数据库 MySQL
【JavaEE“多线程进阶”】——各种“锁”大总结
乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
675 0

热门文章

最新文章