读写锁
首先了解读写锁之前,可以先来聊聊其它锁。
比如说乐观锁和悲观锁。
什么为乐观锁,什么为悲观锁呢?这个名字听起来就很有趣有没有!
如图所示:
其实有点像之前做UE4项目时用到的P4V,就是用来远程在线协同合作中小型项目。
里面就讲到了,在把项目拿出来修改的时候,必须上锁,防止别人的误操作。
- 悲观锁就是只允许一个人进行修改,在修改的时候对资源进行上锁,其它人操作不了。
- 乐观锁就是可以同时允许两个人进行操作,但是每个操作的时候都有版本号。
比如A和B同时进行操作,而A先操作完提交了,版本号从原本1.0改为了1.1,所以当B要提交的时候,发现版本号已经为1.1了,就没办法提交。
这也解释了为什么当时p4v保存的是先提交的人的一份。
好!讲完乐观悲观锁,我们来讲讲主角读写锁
读锁也被称为共享锁
写锁被称为独占锁
然后读写锁都会发生死锁。
首先来看一个正常案例:
public class Demo2 {
//资源类
static class MyCache {
//创建 map 集合
private volatile Map<String,Object> map = new HashMap<>();
//创建读写锁对象
// private ReadWriteLock rwLock = new ReentrantReadWriteLock();
//放数据
public void put(String key,Object value) {
//添加写锁
// rwLock.writeLock().lock();
try {
System.out.println("线程"+Thread.currentThread().getName()+" 正在写入操作"+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//放数据
map.put(key,value);
System.out.println("线程"+Thread.currentThread().getName()+"写完了 "+key);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放写锁
// rwLock.writeLock().unlock();
}
}
//取数据
public Object get(String key) {
//添加读锁
// rwLock.readLock().lock();
Object result = null;
try {
System.out.println("线程"+Thread.currentThread().getName()+"正在读取操作 "+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
result = map.get(key);
System.out.println("线程"+Thread.currentThread().getName()+"读完了 "+key);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放读锁
// rwLock.readLock().unlock();
}
return result;
} }
public static void main(String[] args) {
MyCache myCache=new MyCache();
for(int i=1;i<=5;i++)
{
final int num=i;
new Thread(()-> {
myCache.put(num + "", num + "");
},String.valueOf(i)).start();}
for(int i=1;i<=5;i++)
{
final int num=i;
new Thread(()-> {
myCache.get(num+" ");
},String.valueOf(i)).start();}
}
}
怎么说呢?
就是多个线程同时操作一个资源类cache里面的map集合,可以对map写入操作,也可以读取操作。
按程序后面的顺序是先写再读。
添加延时只是为了让效果更加明显,因为在创建线程去写入之后,就直接延时,这样就可以很明显看出有没有 读的进程会进去。(如果不加延时,仅靠线程的随机触发,偶然性很高)
而结果是会的。
这就是我们运行的结果,就是读操作等不及写操作完成的3秒,就已经开始执行了。
但是!但是这不是我们想要的结果,我们要的结果是得线程写入完成之后,才进行读的线程。
- 于是我们的第一个想法是两个方法同时加了synchronized。那让我们试一下,看一下运行效果如何?
这就是运行效果,可以说一部分目的达到了。
也就是说,确实不会在写的同时去进行读了。
(因为两个方法都加了锁,也就是每个执行了该方法的线程都对资源类上了锁,同时只能运行自己该线程执行,即使睡眠了,别人也抢不到,得等我醒来,这就是锁的作用)
虽然达到了一定的效果,但是还不是我们真正想要的结果。我们真正想要的是,可以同时读,但不能同时写。(而目前是不能同时读同时写)
- 于是乎我们想到了,那么如果对写的方法上锁,而对读的方法不上锁,那么不就可以同时读而不同时写了吗?
多说无益,我们直接看运行结果:
为什么会出现这种情况呢?我一时半会想不过来,并感觉知识体系遭到重创!
于是乎,我想了一下,8锁现象中好像没有一个方法加锁,一个不加的情况。
于是乎我就研究了一下。如果只有读的方法,读方法不加锁,那么运行情况是这样的:
如果只有写方法,写方法加锁,运行结果是这样的:
结合上面的情况,我做出了这样的假设(也不知道对不对)。
就是说一个方法加锁,一个方法不加锁,那么运行结果
它能确报写的操作不被其它写的操作插队,但是不能保证写的操作被另一个写的操作插队。
也就是说它只能保证锁住的方法,保证线程1写完线程2才会进来写,但是其中会有读的线程进来它就管不了了。
或许是这样吧,锁只能管自己的方法。
- 但是这种效果还不是我们要达到的效果,我们要的效果是,能同时读,但不能同时写,且写的同时读不能进来捣乱。
- 而要达成这种效果就只能用读写锁了。
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
创建一个ReadWriteLock现象,然后在写的方法添加写锁
rwLock.writeLock().lock();
在读的地方添加读锁
rwLock.readLock().lock();
最终就能达到想要的结果:
读写锁:
可以共享,提升性能。同时多人进行读操作。
但是也不是没有缺点:
- 比如说锁饥饿,一直读,没有写操作。
最后讲一讲锁降级的事:
• 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发
现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
• 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写
锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
原因: 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把
获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写
锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释
放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
确实,因为写锁可能同时很多线程在占用,同时读;而写锁只能一线程占用,必须写完!
所以锁降级是这样的:
这样是可以正常执行的,先获取写锁,再获取读锁,然后释放写锁,再释放读锁。
(同样可以执行读的操作)
但是如果反过来
这样只能读,但是不能写!
- 读锁不能升级为写锁!
- 写锁可以升级为读锁!
以上内容参考自b站视频https://www.bilibili.com/video/BV1Kw411Z7dF?p=32&spm_id_from=pageDriver;