独占锁:指该锁一次只能被一个线程所持有。对 ReentrantLock 和 Synchronized 而言都是独占锁
共享锁:指该锁可被多个线程所持有。
ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥。

通俗理解:机场大屏幕,每个人都能看到,这就是读共享;但是在在机场通知旅客飞机延误的只能是一个人,如果一群广播员播放,将会乱套,这就是独占写锁。同时在播报的时候,屏幕信息就不能更改,这就是互斥。 即 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。但是如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写。

一、代码展示

正面例子

共享缓存

  1. public class MyCache {
  2. /**
  3. * volatile关键字可以查看前面的包
  4. * 管缓存的,必须用volatile,保证可见性和禁止指令重排,一个线程对其进行了修改,必须让其它线程知道
  5. */
  6. private volatile Map<String, Object> map = new HashMap<>();
  7. /**
  8. * 可以对比加了读写锁和没加读写锁的区别
  9. */
  10. private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  11. /**
  12. * 写操作:原子+独占,整个写过程必须完整的统一体,不能被分割和打断
  13. *
  14. * @param key
  15. * @param value
  16. */
  17. public void put(String key, Object value) {
  18. readWriteLock.writeLock().lock();
  19. try {
  20. System.out.println(Thread.currentThread().getName() + " 正在写入:" + "key:" + key + ",value:" + value);
  21. // 暂定一会模拟网络延迟
  22. try {
  23. TimeUnit.MILLISECONDS.sleep(300);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. map.put(key, value);
  28. System.out.println(Thread.currentThread().getName() + " 写入完成:" + "key:" + key + ",value:" + value);
  29. } catch (Exception e) {
  30. e.printStackTrace();
  31. } finally {
  32. readWriteLock.writeLock().unlock();
  33. }
  34. }
  35. public void get(String key) {
  36. readWriteLock.readLock().lock();
  37. try {
  38. System.out.println(Thread.currentThread().getName() + " 正在读取:" + "key:" + key);
  39. // 暂定一会模拟网络延迟
  40. try {
  41. TimeUnit.MILLISECONDS.sleep(300);
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. }
  45. Object result = map.get(key);
  46. System.out.println(Thread.currentThread().getName() + " 读取完成:" + "key:" + key + ",result:" + result);
  47. } catch (Exception e) {
  48. e.printStackTrace();
  49. } finally {
  50. readWriteLock.readLock().unlock();
  51. }
  52. }
  53. public void clearMap() {
  54. map.clear();
  55. }
  56. }

测试

  1. public class ReadWriteLockDemo {
  2. public static void main(String[] args) {
  3. MyCache myCache = new MyCache();
  4. // MyCacheNoLock myCache = new MyCacheNoLock();
  5. for (int i = 0; i < 5; i++) {
  6. final int i1 = i;
  7. new Thread(() -> {
  8. myCache.put(i1 + "", i1 + "");
  9. }, String.valueOf(i)).start();
  10. new Thread(() -> {
  11. myCache.get(i1 + "");
  12. }, String.valueOf(i)).start();
  13. }
  14. }
  15. }

结果

  1. 0 正在写入:key:0,value:0
  2. 0 写入完成:key:0,value:0
  3. 0 正在读取:key:0
  4. 0 读取完成:key:0,result:0
  5. 1 正在写入:key:1,value:1
  6. 1 写入完成:key:1,value:1
  7. 1 正在读取:key:1
  8. 1 读取完成:key:1,result:1
  9. 2 正在写入:key:2,value:2
  10. 2 写入完成:key:2,value:2
  11. 2 正在读取:key:2
  12. 2 读取完成:key:2,result:2
  13. 3 正在写入:key:3,value:3
  14. 3 写入完成:key:3,value:3
  15. 3 正在读取:key:3
  16. 3 读取完成:key:3,result:3
  17. 4 正在写入:key:4,value:4
  18. 4 写入完成:key:4,value:4
  19. 4 正在读取:key:4
  20. 4 读取完成:key:4,result:4

可以很明显的看到,先写后读的,这样可以保证每次读到的数据都是最新的。展现了读写锁的,读(共享),写(独占),读写(互斥)

反面例子

以下代码没有加读写锁

  1. public class MyCacheNoLock {
  2. /**
  3. * volatile关键字可以查看前面的包
  4. * 管缓存的,必须用volatile,保证可见性和禁止指令重排,一个线程对其进行了修改,必须让其它线程知道
  5. */
  6. private volatile Map<String, Object> map = new HashMap<>();
  7. /**
  8. * 写操作:原子+独占,整个写过程必须完整的统一体,不能被分割和打断
  9. *
  10. * @param key
  11. * @param value
  12. */
  13. public void put(String key, Object value) {
  14. System.out.println(Thread.currentThread().getName() + " 正在写入:" + "key:" + key + ",value:" + value);
  15. // 暂定一会模拟网络延迟
  16. try {
  17. TimeUnit.MILLISECONDS.sleep(300);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. map.put(key, value);
  22. System.out.println(Thread.currentThread().getName() + " 写入完成:" + "key:" + key + ",value:" + value);
  23. }
  24. public void get(String key) {
  25. System.out.println(Thread.currentThread().getName() + " 正在读取:" + "key:" + key);
  26. // 暂定一会模拟网络延迟
  27. try {
  28. TimeUnit.MILLISECONDS.sleep(300);
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. Object result = map.get(key);
  33. System.out.println(Thread.currentThread().getName() + " 读取完成:" + "key:" + key + ",result:" + result);
  34. }
  35. public void clearMap() {
  36. map.clear();
  37. }
  38. }

结果

  1. 0 正在写入:key:0,value:0
  2. 2 正在写入:key:2,value:2
  3. 1 正在写入:key:1,value:1
  4. 4 正在写入:key:4,value:4
  5. 3 正在写入:key:3,value:3
  6. 0 正在读取:key:0
  7. 1 正在读取:key:1
  8. 2 正在读取:key:2
  9. 3 正在读取:key:3
  10. 4 正在读取:key:4
  11. 3 写入完成:key:3,value:3
  12. 2 写入完成:key:2,value:2
  13. 4 写入完成:key:4,value:4
  14. 1 写入完成:key:1,value:1
  15. 0 写入完成:key:0,value:0
  16. 0 读取完成:key:0,result:null // 没有读取到
  17. 1 读取完成:key:1,result:1
  18. 3 读取完成:key:3,result:null
  19. 2 读取完成:key:2,result:null
  20. 4 读取完成:key:4,result:4

可以看到,还没有写入完成,就开始读取了,导致一些数据没有读取到。

二、总结

  • 读-读共存
    读-写不能共存
    写-写不能共存
    写操作:原子+独占,整个写过程必须完整的统一体,不能被分割和打断