概念

独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁 共享锁:指该锁可以被多个线程锁持有 对ReentrantReadWriteLock其读锁是共享,其写锁是独占 写的时候只能一个人写,但是读的时候,可以多个人同时读

为什么会有写锁和读锁

原来我们使用ReentrantLock创建锁的时候,是独占锁,也就是说一次只能一个线程访问,但是有一个读写分离场景,读的时候想同时进行,因此原来独占锁的并发性就没这么好了,因为读锁并不会造成数据不一致的问题,因此可以多个人共享读

多个线程 同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写

读-读:能共存 读-写:不能共存 写-写:不能共存

代码示例

实现一个读写缓存的操作,假设开始没有加锁的时候,会出现什么情况

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. import java.util.concurrent.TimeUnit;
  4. import java.util.concurrent.locks.Lock;
  5. /**
  6. * 资源类
  7. */
  8. class MyCache {
  9. private volatile Map<String, Object> map = new HashMap<>();
  10. /**
  11. * 定义写操作
  12. * 满足:原子 + 独占
  13. * @param key
  14. * @param value
  15. */
  16. public void put(String key, Object value) {
  17. System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
  18. try {
  19. // 模拟网络拥堵,延迟0.3秒
  20. TimeUnit.MILLISECONDS.sleep(300);
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. map.put(key, value);
  25. System.out.println(Thread.currentThread().getName() + "\t 写入完成");
  26. }
  27. public void get(String key) {
  28. System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
  29. try {
  30. // 模拟网络拥堵,延迟0.3秒
  31. TimeUnit.MILLISECONDS.sleep(300);
  32. } catch (InterruptedException e) {
  33. e.printStackTrace();
  34. }
  35. Object value = map.get(key);
  36. System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
  37. }
  38. }
  39. public class ReadWriteLockDemo {
  40. public static void main(String[] args) {
  41. MyCache myCache = new MyCache();
  42. // 线程操作资源类,5个线程写
  43. for (int i = 0; i < 5; i++) {
  44. // lambda表达式内部必须是final
  45. final int tempInt = i;
  46. new Thread(() -> {
  47. myCache.put(tempInt + "", tempInt + "");
  48. }, String.valueOf(i)).start();
  49. }
  50. // 线程操作资源类, 5个线程读
  51. for (int i = 0; i < 5; i++) {
  52. // lambda表达式内部必须是final
  53. final int tempInt = i;
  54. new Thread(() -> {
  55. myCache.get(tempInt + "");
  56. }, String.valueOf(i)).start();
  57. }
  58. }
  59. }

运行结果

1 正在写入:1 3 正在写入:3 2 正在写入:2 0 正在写入:0 4 正在写入:4 1 正在读取: 0 正在读取: 2 正在读取: 3 正在读取: 4 正在读取: 0 读取完成:null 4 写入完成 1 读取完成:null 4 读取完成:null 2 写入完成 3 写入完成 2 读取完成:null 0 写入完成 1 写入完成 3 读取完成:null

结论

多个线程 同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行, 但是,如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写,而从上面代码输出我们可以看到,在写入的时候,写操作都被其它线程打断了,这就造成了,还没写完,其它线程又开始写,这样就造成数据不一致

解决方法

上面的代码是没有加锁的,这样就会造成线程在进行写入操作的时候,被其它线程频繁打断,从而不具备原子性,这个时候,我们就需要用到读写锁来解决了

代码示例

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. import java.util.concurrent.TimeUnit;
  4. import java.util.concurrent.locks.ReentrantReadWriteLock;
  5. /**
  6. * 资源类
  7. */
  8. class MyCache {
  9. /**
  10. * 缓存中的东西,必须保持可见性,因此使用volatile修饰
  11. */
  12. private volatile Map<String, Object> map = new HashMap<>();
  13. /**
  14. * 创建一个读写锁
  15. * 它是一个读写融为一体的锁,在使用的时候,需要转换
  16. */
  17. private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
  18. /**
  19. * 定义写操作
  20. * 满足:原子 + 独占
  21. * @param key
  22. * @param value
  23. */
  24. public void put(String key, Object value) {
  25. // 创建一个写锁
  26. rwLock.writeLock().lock();
  27. try {
  28. System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
  29. try {
  30. // 模拟网络拥堵,延迟0.3秒
  31. TimeUnit.MILLISECONDS.sleep(300);
  32. } catch (InterruptedException e) {
  33. e.printStackTrace();
  34. }
  35. map.put(key, value);
  36. System.out.println(Thread.currentThread().getName() + "\t 写入完成");
  37. } catch (Exception e) {
  38. e.printStackTrace();
  39. } finally {
  40. // 写锁 释放
  41. rwLock.writeLock().unlock();
  42. }
  43. }
  44. /**
  45. * 获取
  46. * @param key
  47. */
  48. public void get(String key) {
  49. // 读锁
  50. rwLock.readLock().lock();
  51. try {
  52. System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
  53. try {
  54. // 模拟网络拥堵,延迟0.3秒
  55. TimeUnit.MILLISECONDS.sleep(300);
  56. } catch (InterruptedException e) {
  57. e.printStackTrace();
  58. }
  59. Object value = map.get(key);
  60. System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
  61. } catch (Exception e) {
  62. e.printStackTrace();
  63. } finally {
  64. // 读锁释放
  65. rwLock.readLock().unlock();
  66. }
  67. }
  68. /**
  69. * 清空缓存
  70. */
  71. public void clean() {
  72. map.clear();
  73. }
  74. }
  75. public class ReadWriteLockDemo {
  76. public static void main(String[] args) {
  77. MyCache myCache = new MyCache();
  78. // 线程操作资源类,5个线程写
  79. for (int i = 1; i <= 5; i++) {
  80. // lambda表达式内部必须是final
  81. final int tempInt = i;
  82. new Thread(() -> {
  83. myCache.put(tempInt + "", tempInt + "");
  84. }, String.valueOf(i)).start();
  85. }
  86. // 线程操作资源类, 5个线程读
  87. for (int i = 1; i <= 5; i++) {
  88. // lambda表达式内部必须是final
  89. final int tempInt = i;
  90. new Thread(() -> {
  91. myCache.get(tempInt + "");
  92. }, String.valueOf(i)).start();
  93. }
  94. }
  95. }

运行结果

1 正在写入:1 1 写入完成 2 正在写入:2 2 写入完成 3 正在写入:3 3 写入完成 4 正在写入:4 4 写入完成 5 正在写入:5 5 写入完成 1 正在读取: 3 正在读取: 2 正在读取: 5 正在读取: 4 正在读取: 3 读取完成:3 5 读取完成:5 4 读取完成:4 2 读取完成:2 1 读取完成:1

结论

从运行结果我们可以看出,写入操作是一个一个线程进行执行的,并且中间不会被打断,而读操作的时候,是同时5个线程进入,然后并发读取操作

这里的读锁和写锁的区别在于,写锁一次只能一个线程进入,执行写操作,而读锁是多个线程能够同时进入,进行读取的操作