读写锁的内部包含两把锁:一把是读(操作)锁,是一种共享锁;另一把是写(操作)锁,是一种独占锁。在没有写锁的时候,读锁可以被多个线程同时持有。写锁是具有排他性的:如果写锁被一个线程持有,其他的线程不能再持有写锁,抢占写锁会阻塞;进一步来说,如果写锁被一个线程持有,其他的线程不能再持有读锁,抢占读锁也会阻塞。
读写锁的读写操作之间的互斥原则具体如下:
- 读操作、读操作能共存,是相容的。
- 读操作、写操作不能共存,是互斥的。
- 写操作、写操作不能共存,是互斥的。
与单一的互斥锁相比,组合起来的读写锁允许对于共享数据进行更大程度的并发操作,虽然每次只能有一个写线程,但是同时可以有多个线程并发地读数据,读写锁适用于读多写少的并发情况。
代码语言:javascript复制public interface ReadWriteLock {
/**
* 返回读锁
*/
Lock readLock();
/**
* 返回写锁
*/
Lock writeLock();
}
读写锁ReentrantReadWriteLock
通过ReentrantReadWriteLock类能获取读锁和写锁,它的读锁是可以多线程共享的共享锁,而它的写锁是排他锁,在被占时不允许其他线程再抢占操作。然而其读锁和写锁之间是有关系的:同一时刻不允许读锁和写锁同时被抢占,二者之间是互斥的。
读写锁升级与降级
锁升级是指读锁升级为写锁,锁降级指的是写锁降级为读锁。在ReentrantReadWriteLock读写锁中,只支持写锁降级为读锁,而不支持读锁升级为写锁。ReentrantReadWriteLock不支持读锁的升级,主要是避免死锁,例如两个线程A和B都占了读锁并且都需要升级成写锁,A升级要求B释放读锁,B升级要求A释放读锁,二者就会由于相互等待形成死锁。
代码语言:javascript复制public class ReadWriteLockTest2{
//创建一个Map,代表共享数据
final static Map<String, String> MAP = new HashMap<String, String>();
//创建一个读写锁
final static ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
//获取读锁
final static Lock READ_LOCK = LOCK.readLock();
//获取写锁
final static Lock WRITE_LOCK = LOCK.writeLock();
//对共享数据的写操作
public static Object put(String key, String value){
WRITE_LOCK.lock();
try{
Print.tco(DateUtil.getNowTime()
" 抢占了WRITE_LOCK,开始执行write操作");
Thread.sleep(1000);
String put = MAP.put(key, value);
Print.tco( "尝试降级写锁为读锁");
//写锁降级为读锁(成功)
READ_LOCK.lock();
Print.tco( "写锁降级为读锁成功");
return put;
} catch (Exception e){
e.printStackTrace();
} finally{
READ_LOCK.unlock();
WRITE_LOCK.unlock();
}
return null;
}
//对共享数据的读操作
public static Object get(String key){
READ_LOCK.lock();
try{
Print.tco(DateUtil.getNowTime()
" 抢占了READ_LOCK,开始执行read操作");
Thread.sleep(1000);
String value = MAP.get(key);
Print.tco( "尝试升级读锁为写锁");
//读锁升级为写锁(失败)
WRITE_LOCK.lock();
Print.tco("读锁升级为写锁成功");
return value;
} catch (InterruptedException e){
e.printStackTrace();
} finally{
WRITE_LOCK.unlock();
READ_LOCK.unlock();
}
return null;
}
public static void main(String[] args){
//创建Runnable可执行实例
Runnable writeTarget = () -> put("key", "value");
Runnable readTarget = () -> get("key");
//创建1个写线程,并启动
new Thread(writeTarget, "写线程").start();
//创建1个读线程
new Thread(readTarget, "读线程").start();
}
}
StampedLock
StampedLock(印戳锁)是对ReentrantReadWriteLock读写锁的一种改进,主要的改进为:在没有写只有读的场景下,StampedLock支持不用加读锁而是直接进行读操作,最大程度提升读的效率,只有在发生过写操作之后,再加读锁才能进行读操作。StampedLock的三种模式如下:
- 悲观读锁:与ReadWriteLock读锁类似,多个线程可同时获取悲观读锁,悲观读锁是一个共享锁。
- 乐观读锁:相当于直接操作数据,不加任何锁,连读锁都不要。
- 写锁:与ReadWriteLock的写锁类似,写锁和悲观读锁是互斥的。虽然写锁与乐观读锁不会互斥,但是在数据被更新之后,之前通过乐观读锁获得的数据已经变成了脏数据。
StampedLock与ReentrantReadWriteLock语义类似,不同的是,StampedLock并没有实现ReadWriteLock接口,而是定义了自己的锁操作API。
代码语言:javascript复制public class StampedLockTest{
//创建一个Map,代表共享数据
final static Map<String, String> MAP = new HashMap<String, String>();
//创建一个印戳锁
final static StampedLock STAMPED_LOCK = new StampedLock();
//对共享数据的写操作
public static Object put(String key, String value){
long stamp = STAMPED_LOCK.writeLock(); //尝试获取写锁的印戳
try{
Print.tco(getNowTime() " 抢占了WRITE_LOCK,开始执行write操作");
Thread.sleep(1000);
String put = MAP.put(key, value);
return put;
} catch (Exception e){
e.printStackTrace();
} finally{
Print.tco(getNowTime() " 释放了WRITE_LOCK");
STAMPED_LOCK.unlockWrite(stamp); //释放写锁
}
return null;
}
//对共享数据的悲观读操作
public static Object pessimisticRead(String key){
Print.tco(getNowTime() "LOCK进入过写模式,只能悲观读");
//进入了写锁模式,只能获取悲观读锁
long stamp = STAMPED_LOCK.readLock(); //尝试获取读锁的印戳
try{
//成功获取到读锁,并重新获取最新的变量值
Print.tco(getNowTime() " 抢占了READ_LOCK");
String value = MAP.get(key);
return value;
} finally{
Print.tco(getNowTime() " 释放了READ_LOCK");
STAMPED_LOCK.unlockRead(stamp); //释放读锁
}
}
//对共享数据的乐观读操作
public static Object optimisticRead(String key){
String value = null;
//尝试进行乐观读
long stamp = STAMPED_LOCK.tryOptimisticRead();
if (0 != stamp){
Print.tco(getNowTime() "乐观读的印戳值,获取成功");
sleepSeconds(1); //模拟耗费时间1秒
value = MAP.get(key);
} else // 0 == stamp 表示当前为写锁模式 {
Print.tco(getNowTime() "乐观读的印戳值,获取失败");
//LOCK已经进入写模式,使用悲观读方法
return pessimisticRead(key);
}
//乐观读操作已经间隔了一段时间,期间可能发生写入
//所以,需要验证乐观读的印戳值是否有效,即判断LOCK是否进入过写模式
if (!STAMPED_LOCK.validate(stamp)){
//乐观读的印戳值无效,表明写锁被占用过
Print.tco(getNowTime() " 乐观读的印戳值,已经过期");
//写锁已经被抢占,进入了写锁模式,只能通过悲观读锁再一次读取最新值
return pessimisticRead(key);
} else {
//乐观读的印戳值有效,表明写锁没有被占用过
//不用加悲观读锁而直接读,减少了读锁的开销
Print.tco(getNowTime() " 乐观读的印戳值,没有过期");
return value;
}
}
public static void main(String[] args) throws InterruptedException{
//创建Runnable可执行实例
Runnable writeTarget = () -> put("key", "value");
Runnable readTarget = () -> optimisticRead("key");
//创建1个写线程,并启动
new Thread(writeTarget, "写线程").start();
//创建1个读线程
new Thread(readTarget, "读线程").start();
}
}