有一些关于锁的面试题:
- 你知道 Java 里面有哪些锁?
- 读写锁的饥饿问题是什么?
- 有没有比读写锁更快的锁?
- StampedLock知道嘛?(邮戳锁/票据锁)
- ReentrantReadWriteLock 有锁降级机制?
ReentrantReadWriteLock
与 ReentrantLock 相比,ReentrantLock 实现了 Lock 接口,ReentrantReadWriteLock 实现了 ReadWriteLock 接口。对于ReentrantLock 它的 读读也是一个线程访问,浪费资源。ReentrantReadWriteLock 可以实现读读共享!
读写锁定义为:一个资源能被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程(读写互斥,读读共享)
class MyRescource { // 资源类模拟缓存Map<String, String> map = new HashMap<>();// ========= ReentrantLock 等价于 ======== SynchronizedLock lock = new ReentrantLock();// ========= ReentrantReadWritLock =======读读共享ReadWriteLock readWriteLock = new ReentrantReadWriteLock();public void write(String key, String value) {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t正在写入。。。。");map.put(key,value);try {TimeUnit.MILLISECONDS.sleep(1000);System.out.println(Thread.currentThread().getName()+"\t完成写入。。。。");}catch (InterruptedException e){e.printStackTrace();}}finally {lock.unlock();}}public void readWriteWrite(String key, String value) {readWriteLock.writeLock().lock();try{System.out.println(Thread.currentThread().getName()+"\t正在写入。。。。");map.put(key,value);try {TimeUnit.MILLISECONDS.sleep(1000);System.out.println(Thread.currentThread().getName()+"\t完成写入。。。。");}catch (InterruptedException e){e.printStackTrace();}}finally {readWriteLock.writeLock().unlock();}}public void read(String key) {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t正在读入。。。。");String s = map.get(key);try {// 1. 暂停500毫秒// 2. 暂停2000毫秒,显式读锁没有完成之前,写锁无法获取锁TimeUnit.MILLISECONDS.sleep(2000);System.out.println(Thread.currentThread().getName()+"\t完成读入。。。。\t"+s);}catch (InterruptedException e){e.printStackTrace();}}finally {lock.unlock();}}public void readWriteRead(String key) {readWriteLock.readLock().lock();try{System.out.println(Thread.currentThread().getName()+"\t正在读入。。。。");String s = map.get(key);try {// 1. 暂停500毫秒// 2. 暂停2000毫秒,显式读锁没有完成之前,写锁无法获取锁TimeUnit.MILLISECONDS.sleep(2000);System.out.println(Thread.currentThread().getName()+"\t完成读入。。。。\t"+s);}catch (InterruptedException e){e.printStackTrace();}}finally {readWriteLock.readLock().unlock();}}
}public class ReentrantReadWriteLockDemo {public static void main(String[] args) {MyRescource myRescource = new MyRescource();for (int i = 0; i < 10; i++) {int finalI = i;new Thread(()->{// myRescource.write("key"+String.valueOf(finalI),"value"+String.valueOf(finalI));myRescource.readWriteWrite("key"+String.valueOf(finalI),"value"+String.valueOf(finalI));},String.valueOf(i)).start();}for (int i = 0; i < 10; i++) {int finalI = i;new Thread(()->{// myRescource.read("key"+String.valueOf(finalI));myRescource.readWriteRead("key"+String.valueOf(finalI));},String.valueOf(i)).start();}try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}// 显式读锁没有完成之前,写锁无法获取锁for (int i = 0; i < 3; i++) {int finalI = i;new Thread(()->{// myRescource.write("key"+String.valueOf(finalI),"value"+String.valueOf(finalI));myRescource.readWriteWrite("key"+String.valueOf(finalI),"value"+String.valueOf(finalI));},"新读写锁"+String.valueOf(i)).start();}}
}
锁降级
ReentrantReadWriteLock锁降级:将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样),锁的严苛程度变强叫做升级,反之叫做降级。
重进入:该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁。
锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁
写锁的降级,降级成为了读锁
- 如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁。
- 规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序。
- 如果释放了写锁,那么就完全转换为读锁。
保证数据的一致性
/**
所降级遵循了获取写锁,在获取读锁,再释放写锁的次序,写锁能够降级为读锁。因为写的优先级比读高
如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁
读没有完成时候写锁无法获得锁,只有在读锁读完才可以
*/
public void lockLevelDown(){ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();writeLock.lock();System.out.println("写入");readLock.lock();System.out.println("读入");writeLock.unlock();readLock.unlock();readLock.lock();System.out.println("读入");writeLock.lock();System.out.println("写入");readLock.unlock();writeLock.unlock();
}
/**
写入
读入
读入
*/
分析StampedLock,会发现它改进之处在于:
读的过程中也允许获取写锁介入(相当牛B,读和写两个操作也让你“共享”(注意引亏)),这样会守致戎们实的数掂趴可能个一以所以,需要额外的方法来判断读的过程中是否有写入,这是一种乐观的读锁。
显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。
为什么要锁降级?
锁降级的必要性1:
锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁, 假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
部分人读完上述话可能有些疑惑,针对上面黑体字那句话,为什么无法感知线程T的数据更新?我当前线程再次获取读锁的时候不是可以察觉到数据在主存中的变化吗? 我参考了一些资料,对该 “”数据可见性“” 有了另一种理解,理解是 当前线程为了保证数据的可见性,这是指线程自己更改了数据,自己应该要察觉到数据的变化,如果没有读锁,更改完数据之后线程T获取到了写锁并更改了数据,则当前线程读到的数据是线程T更改的,并不是自己更改的,当前线程并不知道是线程T修改了自己要读的(原来自己改的)数据,所以可能导致当前线程在执行后续代码的时候结果出错,这时就导致了数据的不可见,即当前线程并无法察觉到自己修改的值!
锁降级的必要性2:
为了提高程序执行性能,可能存在一个事务线程不希望自己的操作被别的线程中断,而这个事务操作可能分成多部分操作更新不同的数据(或表)甚至非常耗时。如果长时间用写锁独占,显然对于某些高响应的应用是不允许的,所以在完成部分写操作后,退而使用读锁降级,来允许响应其他进程的读操作。只有当全部事务完成后才真正释放锁。
StampedLock
邮戳锁、版本锁是比读写锁更快的锁。
StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中读写锁ReentrantReadWriteLock的优化。
stamp(戳记,long 类型):代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。
锁饥饿问题:ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写。
如何缓解锁饥饿问题:
- 使用“公平”策略可以一定程度上缓解这个问题
new ReentrantReadWriteLock(true)
- 但是“公平"策略是以牺牲系统吞吐量为代价
StampedLock 类的乐观读锁:对于短的制度代码段,使用乐观模式通常可以减少争用并提高吞吐量
ReentrantReadWriteLock
允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,
读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多,
原因就是在于ReentrantReadWriteLock支持读并发,读读可以共享
StampedLock横空出世
ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,
所以,在获取乐观读锁后,还需要对结果进行校验。
StampedLock的特点
所有获得锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致
StampedLock 是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
StampedLock 有三种访问模式:
- Reading(读模式悲观):功能和ReentrantReadWriteLock 的读锁类似
- Writeing(写模式):功能和ReentrantReadWriteLock 的写锁类似
- Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式——读的过程中也允许获取写锁介入
邮戳读写锁的传统版本
public class StampedLockDemo {static int number = 37;static StampedLock stampedLock = new StampedLock();public void write(){long stamp = stampedLock.writeLock();System.out.println(Thread.currentThread().getName()+"\t 写线程准备修改。");try{number = number + 13;} finally {stampedLock.unlockWrite(stamp);}System.out.println(Thread.currentThread().getName()+"\t 写线程结束修改。");}public void read(){long stamp = stampedLock.readLock();System.out.println(Thread.currentThread().getName()+"\t 悲观读线程准备修改。..");try{for (int i = 0; i < 4; i++) {TimeUnit.SECONDS.sleep(1);System.out.println("读线程正在读取中");}int result = number;System.out.println(Thread.currentThread().getName()+"\t 悲观读线程结束修改。result = "+result);System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统读写互斥");} catch (InterruptedException e) {e.printStackTrace();} finally {stampedLock.unlockRead(stamp);}}public static void main(String[] args) {StampedLockDemo demo = new StampedLockDemo();new Thread(()->{demo.read();},"读线程").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->{demo.write();},"写线程").start();}}
/**
读线程 悲观读线程准备修改。..
读线程正在读取中
读线程正在读取中
读线程正在读取中
读线程正在读取中
读线程 悲观读线程结束修改。result = 37
写线程没有修改成功,读锁时候写锁无法介入,传统读写互斥
写线程 写线程准备修改。
写线程 写线程结束修改。
*/
邮戳锁乐观版本
public class StampedLockDemo {static int number = 37;static StampedLock stampedLock = new StampedLock();public void write(){long stamp = stampedLock.writeLock();System.out.println(Thread.currentThread().getName()+"\t 写线程准备修改。");try{number = number + 13;} finally {stampedLock.unlockWrite(stamp);}System.out.println(Thread.currentThread().getName()+"\t 写线程结束修改。");}public void read(){long stamp = stampedLock.readLock();System.out.println(Thread.currentThread().getName()+"\t 悲观读线程准备修改。..");try{for (int i = 0; i < 4; i++) {TimeUnit.SECONDS.sleep(1);System.out.println("读线程正在读取中");}int result = number;System.out.println(Thread.currentThread().getName()+"\t 悲观读线程结束修改。result = "+result);System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统读写互斥");} catch (InterruptedException e) {e.printStackTrace();} finally {stampedLock.unlockRead(stamp);}}public void tryOptimisticRead(){long stamp = stampedLock.tryOptimisticRead();int result = number;System.out.println("4秒前stampedLock.validate方法值 (true无修改,false有修改)\t"+stampedLock.validate(stamp));for (int i = 0; i < 4; i++) {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"\t正在读取"+i+"秒,后stampedLock.validate方法值\t"+stampedLock.validate(stamp));}if (!stampedLock.validate(stamp)){System.out.println("有人修改------有写操作");stamp = stampedLock.readLock();try{System.out.println("从乐观读 升级为 悲观读");result = number;System.out.println("从新悲观读后result="+result);}finally {stampedLock.unlockRead(stamp);}}System.out.println(Thread.currentThread().getName()+"\t最后的值是"+result);}public static void main(String[] args) {StampedLockDemo demo = new StampedLockDemo();new Thread(()->{System.out.println(Thread.currentThread().getName()+"进入");demo.tryOptimisticRead();},"乐观读线程").start();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->{System.out.println(Thread.currentThread().getName()+"进入");demo.write();},"写线程").start();}
}
/**
乐观读线程进入
4秒前stampedLock.validate方法值 (true无修改,false有修改) true
乐观读线程 正在读取0秒,后stampedLock.validate方法值 true
写线程进入
写线程 写线程准备修改。
写线程 写线程结束修改。
乐观读线程 正在读取1秒,后stampedLock.validate方法值 false
乐观读线程 正在读取2秒,后stampedLock.validate方法值 false
乐观读线程 正在读取3秒,后stampedLock.validate方法值 false
有人修改------有写操作
从乐观读 升级为 悲观读
从新悲观读后result=50
乐观读线程 最后的值是50
*/
邮戳锁的缺点:
- StampedLock 不支持重入,没有 Re 开头
- StampedLock 的悲观读锁和写作都不支持条件变量
- 使用StampedLock一定不要调用中断操作