读写锁
先说说独占锁 、 共享锁 、 互斥锁
简述
-
独占锁(写锁):只能被一个线程所持有。例如:ReentrantLock 和 Synchronized 都是独占锁。当一个线程获得独占锁后,其他任何尝试获取该锁的线程都会被阻塞,直到这把锁被释放。独占锁主要用于需要独占某个资源的情况。比如:当一个线程正在进行写操作时,就需要确保没有其他线程能够同时进行读或者写操作
-
共享锁(读锁):可以被多个线程所持有。当一个线程获得共享锁后,其他线程也可以获得共享锁,不会造成阻塞。这种锁主要用于需要允许多个线程同时访问某个资源的情况。比如:当多个线程需要读取同一份数据时,它们可以同时获得共享锁进行读取
-
互斥锁:在某一个时刻只允许一个线程去访问资源,它会在任何情况下阻止其他线程对资源的访问。互斥锁主要用于严格控制并发的情况。比如:当一个线程在进行敏感操作时,需要确保没有其他线程能干扰。
为什么会有 读锁 和 写锁
读锁(共享锁)允许多个线程同时读取共享资源,而写锁(独占锁)只允许一个线程写入共享资源。
在并发的场景下,如果我们使用独占锁 进行资源访问控制,每次都只能有一个线程进行访问,当存在读写分离的需求,在读的时候需要并发的访问,读锁并不会造成数据不一致的问题,可以多个线程共享读,此时在只有独占锁的情况下就会导致并发访问效率低下。
所以为了满足并发量,读取共享资源应该可以同时进行,但是如果一个线程想去写共享资源,就需要确保没有其他线程可以对该资源同时进行读或者写操作。
像之前我们使用的 ReentrantLock 或者 Synchronized 都是独占锁,而系统的大多数场景都是读多写少,使用独占锁必然导致效率低下,因此就有了读写锁 ReentrantReadWriteLock
ReentrantReadWriteLock 简述
ReentrantReadWriteLock
位于 java.util.concurrent.locks
包下。它实现了ReadWriteLock接口,提供了一种可重入的读写锁。这种锁允许多个线程同时读取共享资源,而在写操作时只允许一个线程进行操作。这种锁可以提高并发性能和吞吐量,适用于读操作频繁而写操作较少的场景。
ReentrantReadWriteLock
有两个内部类: ReadLock
和 WriteLock
- ReadLock:读锁(共享锁),允许多个线程同时读取共享资源
- WriteLock:写锁(独占锁),只允许一个线程写入字眼
ReentrantReadWriteLock 具有以下特点:
-
可重入性:ReentranReadWriteLock 支持重入,即读线程获取读锁之后能够再次获取读锁;写线程获取写锁之后能够再次获取写锁,同时也可以获取读锁。这种特性可以避免死锁
-
公平性:ReentrantReadWriteLock 支持公平和非公平两种模式。在公平模式下,锁的获取按照先后顺序进行,先请求锁的线程先获取锁,后请求锁的线程等待先获取锁的线程释放锁之后再获取。在非公平模式下,锁的获取不保证按照先后顺序进行,可能出现后请求锁的线程先获取锁的情况。
-
分离读锁和写锁:ReentranReadWriteLock 分离了读锁和写锁,通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升:同一时间可以允许多个线程同时访问,在写线程获取到锁时,所有读线程和线程都会被阻塞
-
支持状态管理:ReentranReadWriteLock 支持状态管理,可以通过 getState() 方法获取当前状态,也可以通过 setState() 方法设置当前状态。泽中特性可以用于实现复杂的业务逻辑
ReentrantReadWriteLock 使用
不加锁案例:实现一个读写缓存的操作,看看没有有加锁的时候,会出现什么情况
代码:
package com.avgrado.demo.thread;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @ClassName ReadWriteLockDemo
* @Description TODO
* @Author 半晨烟宇
*/
class MyCache{
volatile Map cacheMap = new HashMap<>();
/**
* 定义写入资源方法
*/
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName()+"\t 正在写入");
try {
//模拟网络拥堵延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
cacheMap.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t 写入完成");
}
/**
* 定义读取资源方法
*/
public void get(String key){
System.out.println(Thread.currentThread().getName()+"\t 正在读取");
try {
//模拟查询缓慢,延迟0.5秒
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = cacheMap.get(key);
System.out.println(Thread.currentThread().getName()+"\t 读取完成:"+value);
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache cache = new MyCache();
//模拟5个线程写
for(int i =1;i<=5;i++){
final int tempInt = i;
new Thread(()->{
cache.put(tempInt+"",tempInt);
},String.valueOf(i)+"号线程").start();
}
//模拟5个线程读
for(int j=1;j<=5;j++){
final int tmpInt = j;
new Thread(()->{
cache.get(tmpInt+"");
},String.valueOf(j)+"号线程").start();
}
}
}
执行结果:
从结果看出:我们可以看到,在写入的时候,写操作都全部完成就被其它线程打断了,这就造成了,还没写完,其它线程又开始读,这样就造成结果不一致
解决办法
上面的代码是没有加锁的,这样就会造成线程在进行写入操作的时候,被其它线程频繁打断,从而不具备原子性。针对这种情况,使用读写锁ReentranReadWriteLock解决,保证写操作是独占,读操作共享
代码:
package com.avgrado.demo.thread;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class MyCache{
volatile Map map = new HashMap<>();
final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* 定义写入资源方法
*/
public void put(String key, Object value) {
// 创建一个写锁
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 写锁 释放
readWriteLock.writeLock().unlock();
}
}
/**
* 定义读取资源方法
*/
public void get(String key) {
// 读锁
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 读锁释放
readWriteLock.readLock().unlock();
}
}
}
/**
* @ClassName ReadWriteLockDemo
* @Description TODO
* @Author gongchen
* @Date 2023-10-30 11:22
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache cache = new MyCache();
//模拟5个线程写
for(int i =1;i<=5;i++){
final int tempInt = i;
new Thread(()->{
cache.put(tempInt+"",tempInt);
},String.valueOf(i)+"号线程").start();
}
//模拟5个线程读
for(int j=1;j<=5;j++){
final int tmpInt = j;
new Thread(()->{
cache.get(tmpInt+"");
},String.valueOf(j)+"号线程").start();
}
}
}
执行结果:
从运行结果我们可以看出,写入操作是一个一个线程进行执行的,并且中间不会被打断,而读操作的时候,是同时5个线程进入,然后并发读取操作