极客时间《Java并发编程实战》笔记---ReadWriteLock与StampedLock

﹏ヽ暗。殇╰゛Y 2023-01-10 13:26 187阅读 0赞

文章目录

  • 实现缓存的按需加载
  • 读写锁的升级与降级
  • 比读写锁更快的锁—-StampedLock
      • StampedLock 支持的三种锁模式
      • 乐观读的实现与数据库的乐观锁非常相似
      • StampedLock的使用注意事项
      • StampedLock 的使用模板,建议在以后的使用中直接套用或参考

实现缓存的按需加载

  1. package com.codes;
  2. import java.util.Map;
  3. import java.util.concurrent.locks.Lock;
  4. import java.util.concurrent.locks.ReadWriteLock;
  5. import java.util.concurrent.locks.ReentrantReadWriteLock;
  6. class Cache<K, V> {
  7. final Map<K, V> m =
  8. new HashMap<>();
  9. final ReadWriteLock rwl =
  10. new ReentrantReadWriteLock();
  11. // 读锁
  12. final Lock r = rwl.readLock();
  13. // 写锁
  14. final Lock w = rwl.writeLock();
  15. // 读缓存
  16. V get(K key) {
  17. r.lock();
  18. try {
  19. //缓存中存在,直接返回
  20. if (m.containsKey(key)) {
  21. return m.get(key);
  22. }
  23. } finally {
  24. r.unlock();
  25. }
  26. w.lock(); //⑤
  27. try {
  28. //再次检查缓存中是否存在(重点理解!!!)
  29. if (m.containsKey(key)) {
  30. return m.get(key);
  31. }
  32. //查询数据库并写入
  33. V value = DB.getByKey(key);
  34. m.put(key, value);
  35. return m.get(key);
  36. } finally {
  37. w.unlock();
  38. }
  39. }
  40. // 写缓存
  41. V put(K key, V value) {
  42. w.lock();
  43. try {
  44. return m.put(key, value);
  45. } finally {
  46. w.unlock();
  47. }
  48. }
  49. }

再次检查缓存是否存在是因为:在高并发的场景下,有可能会有多线程竞争写锁。假设缓存是空的,没有缓存任何东西,如果此时有三个线程 T1、T2 和 T3 同时调用 get() 方法,并且参数 key 也是相同的。那么它们会同时执行到代码⑤处,但**此时只有一个线程能够获得写锁,假设是线程 T1,线程 T1 获取写锁之后查询数据库并更新缓存,最终释放写锁。此时线程 T2 和 T3 会再有一个线程能够获取写锁,假设是 T2,如果不采用再次验证的方式,此时 T2 会再次查询数据库。T2 释放写锁之后,T3 也会再次查询一次数据库。而实际上线程 T1 已经把缓存的值设置好了,T2、T3 完全没有必要再次查询数据库。**所以,再次验证的方式,能够避免高并发场景下重复查询数据的问题。

读写锁的升级与降级

升级就是先获取读锁,然后再升级为写锁,降级就是 先获取写锁,然后再降级为读锁
但是 ReadWriteLock 不支持锁的升级,只支持锁的降级,如果在获取写锁时,没有释放掉占有的读锁就会 导致写锁永久等待,最终导致相关线程都被阻塞,永远也没有机会被唤醒。

原因:由于获取写锁需要释放所有的读锁,在这里是矛盾的。

只有写锁支持条件变量,读锁是不支持条件变量的,读锁调用 newCondition() 会抛出 UnsupportedOperationException 异常。

比读写锁更快的锁—StampedLock

在读多写少的场景下,Java 在 1.8 这个版本里,提供了一种叫 StampedLock 的锁,它的性能比读写锁还要好。

StampedLock 支持的三种锁模式

ReadWriteLock 只支持 读锁和写锁两种模式,但是 StampedLock 提供了三种模式,分别是:写锁、悲观读锁乐观读。其中,写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义完全一样,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。

不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。相关的示例代码如下。

  1. final StampedLock sl =
  2. new StampedLock();
  3. // 获取/释放悲观读锁示意代码
  4. long stamp = sl.readLock();
  5. try {
  6. //省略业务相关代码
  7. } finally {
  8. sl.unlockRead(stamp);
  9. }
  10. // 获取/释放写锁示意代码
  11. long stamp = sl.writeLock();
  12. try {
  13. //省略业务相关代码
  14. } finally {
  15. sl.unlockWrite(stamp);
  16. }

那么性能好的主要原因就是:乐观读!特点:无锁操作。并且允许一个线程获取写锁,也就是说不是所有的写操作都被阻塞。

看下面这段示例代码:

  1. class Point {
  2. private int x, y;
  3. final StampedLock sl =
  4. new StampedLock();
  5. //计算到原点的距离
  6. int distanceFromOrigin() {
  7. // 乐观读
  8. long stamp =
  9. sl.tryOptimisticRead();
  10. // 读入局部变量,
  11. // 读的过程数据可能被修改
  12. int curX = x, curY = y;
  13. //判断执行读操作期间,
  14. //是否存在写操作,如果存在,
  15. //则sl.validate返回false
  16. if (!sl.validate(stamp)){
  17. // 升级为悲观读锁
  18. stamp = sl.readLock();
  19. try {
  20. curX = x;
  21. curY = y;
  22. } finally {
  23. //释放悲观读锁
  24. sl.unlockRead(stamp);
  25. }
  26. }
  27. return Math.sqrt(
  28. curX * curX + curY * curY);
  29. }
  30. }

需要注意的一点是:如果执行乐观读操作的期间,存在写操作,会把乐观读升级为悲观读锁。这个做法挺合理的,否则你就需要在一个循环里反复执行乐观读,直到执行乐观读操作的期间没有写操作(只有这样才能保证 x 和 y 的正确性和一致性),而循环读会浪费大量的 CPU。升级为悲观读锁,代码简练且不易出错,建议你在具体实践时也采用这样的方法

乐观读的实现与数据库的乐观锁非常相似

极客上讲述了一种很很很容易理解的方式,具体可自行购买查看。

对数据加上一个版本号,查询的时候把 version 字段查出来,更新的时候要利用 version 字段做验证,即可判断是否有人修改过,很妙!

MYSQL 乐观锁及其原理

StampedLock的使用注意事项

  • 不支持重入
  • StampedLock 的悲观读锁、写锁都不支持条件变量(不能与条件变量一起配合使用)
  • 使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。否则会导致CPU疯狂飙升原因:内部实现里while循环里面对中断的处理有点问题

StampedLock 的使用模板,建议在以后的使用中直接套用或参考

StampedLock 读模板:

  1. final StampedLock sl =
  2. new StampedLock();
  3. // 乐观读
  4. long stamp =
  5. sl.tryOptimisticRead();
  6. // 读入方法局部变量
  7. ......
  8. // 校验stamp
  9. if (!sl.validate(stamp)){
  10. // 升级为悲观读锁
  11. stamp = sl.readLock();
  12. try {
  13. // 读入方法局部变量
  14. .....
  15. } finally {
  16. //释放悲观读锁
  17. sl.unlockRead(stamp);
  18. }
  19. }
  20. //使用方法局部变量执行业务操作
  21. ......

StampedLock 写模板:

  1. long stamp = sl.writeLock();
  2. try {
  3. // 写共享变量
  4. ......
  5. } finally {
  6. sl.unlockWrite(stamp);
  7. }

发表评论

表情:
评论列表 (有 0 条评论,187人围观)

还没有评论,来说两句吧...

相关阅读