前言
代码中,针对加锁的区块,多线程执行调用时,会根据先获取到锁先执行,其他线程必须等待其成功释放锁后才能继续使用资源。
整体而言虽然保证了数据的完整性,但是对于效率来说,会有所降低。针对此问题,在JUC包下存在一个读写锁类ReadWriteLock
。
简介
ReadWriteLock
在java中是一个接口
。有且仅有一个官方定义的子类java.util.concurrent.locks.ReentrantReadWriteLock
。
从JDK 1.8
开发文档中的介绍得知,针对读锁
操作,允许多个线程同时执行;针对写操作,只允许线程依次执行。
下面看一个栗子。
案例
无锁情况下
多线程操作一个缓存类,同时
执行添加
、获取
操作,代码案例如下所示:
package demo5_2;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class NoLockTest {
public static void main(String[] args) throws InterruptedException {
// 缓存只能单例,让多个线程同时操作一个资源,才会有并发安全问题
MyNoLockCache myNoLockCache = new MyNoLockCache();
// 1、开启多个线程进行写操作
for (int i = 1; i <= 20 ; i++) {
// 临时变量
final int temp = i;
new Thread(()->{
myNoLockCache.set(String.valueOf(temp),"66666");
},String.valueOf(i)).start();
}
TimeUnit.SECONDS.sleep(4);
// 2、多个线程读操作
for (int i = 1; i <= 20 ; i++) {
// 临时变量
final int temp = i;
new Thread(()->{
myNoLockCache.get(String.valueOf(temp));
},String.valueOf(i)).start();
}
}
/**
* 没有锁的时候,设置总会被插队,导致日志打印不整齐;<br />
*/
}
/**
* 自定义缓存类,实现数据的 保存 和 获取操作
*/
class MyNoLockCache{
// 由于需要保存数据,此处采取集合的方式存储
private Map<String,Object> maps = new HashMap<>();
public void set(String key,Object value){
System.out.println("当前为 "+key+" 进行数据存储");
maps.put(key,value);
System.out.println("当前为 "+key+" 数据保存 OK");
}
public void get(String key){
System.out.println(key+" 获取数据");
maps.get(key);
System.out.println(key+" 获取数据 OK");
}
}
其中运行日志如下所示:
没有加锁的情况下,添加数据至缓存中,总会出现其他线程插队的现象。
使用 ReentrantLock 加锁
为了保证写入数据操作执行时,其他线程不会对其进行干扰操作,此时需要在set
和get
方法中添加锁,保证顺序执行。
package demo5_2;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static void main(String[] args) throws InterruptedException {
// 缓存只能单例,让多个线程同时操作一个资源,才会有并发安全问题
MyLockCache myLockCache = new MyLockCache();
// 1、开启多个线程进行写操作
for (int i = 1; i <= 30 ; i++) {
// 临时变量
final int temp = i;
new Thread(()->{
myLockCache.set(String.valueOf(temp),"66666");
},String.valueOf(i)).start();
}
// 这里的时间只是为了测试,时间越长只是将读和写分开。
// 测试读、写操作交替执行导致的问题现象可以频闭此延迟!
TimeUnit.SECONDS.sleep(4);
// 2、多个线程读操作
for (int i = 1; i <= 30 ; i++) {
// 临时变量
final int temp = i;
new Thread(()->{
myLockCache.get(String.valueOf(temp));
},String.valueOf(i)).start();
}
}
}
/**
* 自定义缓存类,实现数据的 保存 和 获取操作
*/
class MyLockCache{
// 由于需要保存数据,此处采取集合的方式存储
private Map<String,Object> maps = new HashMap<>();
// 生成对象资源的时候 就创建一把锁
private Lock lock = new ReentrantLock();
public void set(String key,Object value){
this.lock.lock();
try {
System.out.println("当前为 "+key+" 进行数据存储");
maps.put(key,value);
System.out.println("当前为 "+key+" 数据保存 OK");
}finally {
this.lock.unlock();
}
}
public void get(String key){
// 当读操作不加锁,会在写操作中插队!
this.lock.lock();
try {
System.out.println(key+" 获取数据");
Object o = maps.get(key);
System.out.println(key+" 获取数据 OK = "+o);
}finally {
this.lock.unlock();
}
}
}
执行后,控制台日志打印信息如下所示:
读写操作都能打印添加(获取)数据和添加(获取)ok,
但是,读操作也进行了加锁,当只是读操作执行,此时并不需要进行加锁,加了锁反而影响了执行效率。
[疑问:]
到这里可能有人会问:
既然读操作加了锁影响了效率,那读操作就不加锁嘛。
但是结合之前解释的八锁效应
,读操作不加锁和写操作加锁,那么读操作会插队至写操作!
ReentrantReadWriteLock 读写锁
为了保证数据写入顺序性,必须要求在以下状态下加锁和无锁:
- 读 - 读 操作:
读操作,只是从Map集合中获取数据,有就返回数据信息,无数据则返回null。并无并发问题,所以不需要加锁
- 读 - 写 操作:
考虑到多个线程同时执行,读操作无锁,但写操作必须加锁保证数据操作安全行;结合来看就必须保证同时进行读写
操作需要加锁
。 - 写 - 写 操作
写和写操作之间,必须保证每个线程操作执行时的安全问题,需要加锁
。
使用 ReentrantReadWriteLock
就能完美解决上述问题。
看下面栗子:
package demo5_2;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
public static void main(String[] args) throws InterruptedException {
// 缓存只能单例,让多个线程同时操作一个资源,才会有并发安全问题
MyRWLock myRWLock = new MyRWLock();
// 1、开启多个线程进行写操作
for (int i = 1; i <= 10 ; i++) {
// 临时变量
final int temp = i;
new Thread(()->{
myRWLock.set(String.valueOf(temp),"66666");
},String.valueOf(i)).start();
}
// 时间的长短 取决于测试 写-读 还是 单读,可以分别设定不同时间测试效果
TimeUnit.SECONDS.sleep(2);
// 2、多个线程读操作
for (int i = 1; i <= 10 ; i++) {
// 临时变量
final int temp = i;
new Thread(()->{
myRWLock.get(String.valueOf(temp));
},String.valueOf(i)).start();
}
}
}
class MyRWLock{
// 由于需要保存数据,此处采取集合的方式存储
private Map<String,Object> maps = new HashMap<>();
// ReentrantReadWriteLock 创建读写锁
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock() ;
public void set(String key,Object value){
// 创建写锁
this.readWriteLock.writeLock().lock();
try {
System.out.println("当前为 "+key+" 进行数据存储");
maps.put(key,value);
System.out.println("当前为 "+key+" 数据保存 OK");
}finally {
this.readWriteLock.writeLock().unlock();
}
}
public void get(String key){
// 创建读锁
this.readWriteLock.readLock().lock();
try {
System.out.println(key+" 获取数据");
maps.get(key);
System.out.println(key+" 获取数据 OK");
}finally {
this.readWriteLock.readLock().unlock();
}
}
}
[发现:]
1、当设置延迟时间
保证写操作和读操作分开执行时。日志如下:
写操作会有顺序(写和写ok)一一对应!
读操作则不会对应!
读写锁,写-写会加锁、读-读不会有锁。
2、取消延迟,测试写-读
即使出现写操作中插队有读操作,但日志
操作执行
和执行ok
是一一对应
的,说明在写-读
操作中存在锁!