:::success Java 提供了加锁机制抑制多线程访问共享变量。当一个线程访问对象时首先要获取对象的锁,获取到并通过加锁声明对对象的独占访问,访问完之后再释放对象的锁。由于对象上有锁,其它线程无法访问,只能阻塞等待当前线程释放锁再去获取。 :::

隐式锁

JVM 层面实现了一种隐式锁,这种锁相当于是对象的隐式监视器 Monitor。为什么称为隐式锁呢?Java 通过 synchronized 关键字声明对一个对象进行独占访问,这种锁不需要我们手动加锁和解锁,当线程访问对象时 JVM 自动加锁,并在访问结束或者抛出异常时自动解锁。
synchronized 使用方式如下:

  • 声明持有指定对象的锁 ```java final Object object = new Object();

public void increment() { synchronized (object) { count++; } }

  1. - 声明持有当前对象的锁
  2. ```java
  3. // 对当前对象加锁
  4. public void increment() {
  5. synchronized (this) {
  6. count++;
  7. }
  8. }
  9. // 同步方法,也是对当前对象加锁
  10. public synchronized void decrement() {
  11. count--;
  12. }

隐式锁是可重入的锁(套娃),意思就是同一个线程可以对一个对象多次加锁,每次加锁,对象的监视器 Monitor 就会加一,解锁时就会减一,直到为 0 时表示当前对象没有被线程独占。
隐式锁释放与下一次获取同一把锁具有 happens - before 关系。

显式锁

JDK 5 在 java.util.concurrent.locks 包中新增了程序实现的显式锁。显式锁提供了以下方法:

  • lock(),获取锁,获取不到则阻塞等待
  • unlock(),释放锁
  • lockInterruptibly(),获取锁,获取不到则阻塞等待,但是可以被中断而退出阻塞等待
  • tryLock(),尝试获取锁,如果获取不到则立马返回 false,如果获取到则返回 true 并自动加锁,可以进行后续操作
  • tryLock(long time, TimeUnit unit),在指定时间内尝试获取锁,可被中断退出
  • newCondition(),声明线程协调的条件

相比于隐式锁,显式锁可以更灵活的加锁解锁,并且支持尝试获取锁,如果获取不到可以退出获取锁的尝试。

ReentrantLock 类

ReentrantLock 相当于隐式锁 synchronized,正如它的名字一样,是可重入的锁。它提供了两个构造方法:

  • ReentrantLock(),默认非公平可重入锁
  • ReentrantLock(boolean fair) ,指定公平策略的可重入锁

公平模式下线程先进先出,减少饥饿,但是会减少吞吐量。isFair() 方法可以判断是否是公平锁。

  1. // 普通获取锁
  2. public void increment() {
  3. final ReentrantLock lock = new ReentrantLock();
  4. lock.lock();
  5. try {
  6. count++;
  7. } finally {
  8. lock.unlock();
  9. }
  10. }
  11. // 尝试获取锁
  12. public void increment() {
  13. final ReentrantLock lock = new ReentrantLock();
  14. if (lock.tryLock()) {
  15. try {
  16. count++;
  17. } finally {
  18. lock.unlock();
  19. }
  20. }
  21. }

ReentrantLock 类还提供了两个方法:

  • getHoldCount(),获取当前线程持有该锁的数量
  • isHeldByCurrentThread(),判断当前线程是否持有该锁

通过以上两个方法可以根据 ReentrantLock 实现不可重入的锁,即判断是否已经持有该锁:

  1. public void increment() {
  2. final ReentrantLock lock = new ReentrantLock();
  3. assert lock.isHeldByCurrentThread();
  4. //assert lock.getHoldCount() != 0;
  5. lock.lock();
  6. try {
  7. count++;
  8. } finally {
  9. lock.unlock();
  10. }
  11. }

:::success 互斥锁
隐式锁和 ReentrantLock 锁都是互斥锁(排他锁),每个锁只允许一个线程访问。 :::

ReentrantReadWriteLock 类

由于互斥锁只允许一个线程访问,在某些情况下,多个线程可以并发读取往往能提升性能。ReentrantReadWriteLock 类提供了一对关联的锁:ReadLock读锁和 WriteLock 写锁。读锁用于并发只读操作,写锁允许排他写入。
公平策略
ReentrantReadWriteLock 类提供的构造函数有两个:

  • ReentrantReadWriteLock(),默认非公平的读写锁
  • ReentrantReadWriteLock(boolean fair),指定公平策略的读写锁

非公平锁模式:
读锁和写锁互相竞争,具体谁获得锁是不确定的,受重入影响。相比公平锁可以提高吞吐量。
公平锁模式:
当释放当前锁时,公平锁会为等待时间最长的一个写请求线程分配写锁,或者为等待时间比写请求长的读请求线程组分配读锁。
如果没有空闲的读写锁,那么请求获取写锁的线程会被阻塞;如果有写锁或者有等待写锁的线程,那么请求获取读锁会被阻塞。(tryLock 尝试获取锁不受此限制,还有可能不管等待的线程直接获取锁)
可重入
正如 ReentrantReadWriteLock 名字那样,读锁和写锁是可重入的,但是以下情况除外:

  • 写锁未释放不允许非重入读,即不允许其它线程读
  • 写锁未释放允许重入读,即允许当前线程读,写锁可降级为读锁。但反之不能(内存一致性)

    1. class CachedData {
    2. Object data;
    3. boolean cacheValid;
    4. final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    5. void processCachedData() {
    6. rwl.readLock().lock();
    7. if (!cacheValid) {
    8. // Must release read lock before acquiring write lock(获取写锁先释放读锁)
    9. rwl.readLock().unlock();
    10. rwl.writeLock().lock();
    11. try {
    12. // Recheck state because another thread might have
    13. // acquired write lock and changed state before we did.(重新检查状态,防止其它线程先于我们获取写锁之前修改)
    14. if (!cacheValid) {
    15. data = ...
    16. cacheValid = true;
    17. }
    18. // Downgrade by acquiring read lock before releasing write lock(锁降级:获取读锁,这时并不释放写锁)
    19. rwl.readLock().lock();
    20. } finally {
    21. rwl.writeLock().unlock(); // Unlock write, still hold read(获取到读锁后可以释放写锁)
    22. }
    23. }
    24. try {
    25. use(data);
    26. } finally {
    27. rwl.readLock().unlock();
    28. }
    29. }
    30. }

    可中断
    读写锁在获取锁的时候也是可中断的。
    读写锁适用于前期大量写并很少修改,后期大量读的场景,比如字典。示例代码如下:

    1. class RWDictionary {
    2. private final Map<String, Data> m = new TreeMap<>();
    3. private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    4. private final Lock r = rwl.readLock();
    5. private final Lock w = rwl.writeLock();
    6. public Data get(String key) {
    7. r.lock();
    8. try {
    9. return m.get(key);
    10. } finally {
    11. r.unlock();
    12. }
    13. }
    14. public List<String> allKeys() {
    15. r.lock();
    16. try {
    17. return new ArrayList<>(m.keySet());
    18. } finally {
    19. r.unlock();
    20. }
    21. }
    22. public Data put(String key, Data value) {
    23. w.lock();
    24. try {
    25. return m.put(key, value);
    26. } finally {
    27. w.unlock();
    28. }
    29. }
    30. public void clear() {
    31. w.lock();
    32. try {
    33. m.clear();
    34. } finally {
    35. w.unlock();
    36. }
    37. }
    38. }

    StampedLock 类

    Java 8 新增了 StampedLock 类,提供了乐观读的功能。 :::success 乐观锁和悲观锁
    乐观锁假定读的过程中大概率不会写入,可以获取写锁;而悲观锁在读的过程中禁止写,必须释放读锁才能获取写锁。 ::: StampedLock 类提供了如下方法:

  • writeLock(),获取独占形式的排他写锁,释放用 unlockWrite(long stamp)

  • readLock(),获取非排他的读锁,释放用 unlockRead(long stamp)
  • tryOptimisticRead(),获取带有验证标记的乐观锁
  • validate(long stamp),验证读取是否已被修改
  • tryConvertToWriteLock(long stamp),如果锁定状态与给定的标记相匹配,则自动执行以下操作之一:如果图章表示持有写锁,则将其返回;如果有读锁,则如果写锁可用,则释放读锁并返回写标记;如果乐观阅读,则仅在立即可用时才返回写戳记。在所有其他情况下,此方法均返回零。
  • tryConvertToReadLock(long stamp),如果锁定状态与给定的标记相匹配,则自动执行以下操作之一:如果图章表示持有写锁,请释放它并获得读锁;如果有读锁,则返回它;如果是乐观读取,则获取读取锁定,并且仅在立即可用时才返回读取戳记。在所有其他情况下,此方法返回零。
  • tryConvertToOptimisticRead(long stamp),如果锁状态与给定的戳匹配,则从原子上讲,如果戳表示持有锁,则将其释放并返回观察戳。如果乐观阅读,则在验证后返回。在所有其他情况下,此方法都返回零,因此可用作 tryUnlock 的形式。

    1. class Point {
    2. private double x, y;
    3. private final StampedLock sl = new StampedLock();
    4. // an exclusively locked method
    5. void move(double deltaX, double deltaY) {
    6. long stamp = sl.writeLock();
    7. try {
    8. x += deltaX;
    9. y += deltaY;
    10. } finally {
    11. sl.unlockWrite(stamp);
    12. }
    13. }
    14. // a read-only method
    15. // upgrade from optimistic read to read lock
    16. // 乐观读升级为悲观读
    17. double distanceFromOrigin() {
    18. long stamp = sl.tryOptimisticRead();
    19. try {
    20. retryHoldingLock:
    21. for (; ; stamp = sl.readLock()) {
    22. if (stamp == 0L)
    23. continue retryHoldingLock;
    24. // possibly racy reads
    25. double currentX = x;
    26. double currentY = y;
    27. // 验证是否被修改
    28. if (!sl.validate(stamp))
    29. continue retryHoldingLock;
    30. return Math.hypot(currentX, currentY);
    31. }
    32. } finally {
    33. if (StampedLock.isReadLockStamp(stamp))
    34. sl.unlockRead(stamp);
    35. }
    36. }
    37. // upgrade from optimistic read to write lock
    38. // 乐观读升级为悲观写
    39. void moveIfAtOrigin(double newX, double newY) {
    40. long stamp = sl.tryOptimisticRead();
    41. try {
    42. retryHoldingLock:
    43. for (; ; stamp = sl.writeLock()) {
    44. if (stamp == 0L)
    45. continue retryHoldingLock;
    46. // possibly racy reads
    47. double currentX = x;
    48. double currentY = y;
    49. if (!sl.validate(stamp))
    50. continue retryHoldingLock;
    51. if (currentX != 0.0 || currentY != 0.0)
    52. break;
    53. stamp = sl.tryConvertToWriteLock(stamp);
    54. if (stamp == 0L)
    55. continue retryHoldingLock;
    56. // exclusive access
    57. x = newX;
    58. y = newY;
    59. return;
    60. }
    61. } finally {
    62. if (StampedLock.isWriteLockStamp(stamp))
    63. sl.unlockWrite(stamp);
    64. }
    65. }
    66. // Upgrade read lock to write lock
    67. // 读锁升级为写锁
    68. void moveIfAtOrigin(double newX, double newY) {
    69. long stamp = sl.readLock();
    70. try {
    71. while (x == 0.0 && y == 0.0) {
    72. long ws = sl.tryConvertToWriteLock(stamp);
    73. if (ws != 0L) {
    74. stamp = ws;
    75. x = newX;
    76. y = newY;
    77. break;
    78. } else {
    79. sl.unlockRead(stamp);
    80. stamp = sl.writeLock();
    81. }
    82. }
    83. } finally {
    84. sl.unlock(stamp);
    85. }
    86. }
    87. }

    隐式锁和显式锁对比

    | | 隐式锁 | 显式锁 | | —- | —- | —- | | 实现 | JVM 实现,内置锁 | 程序实现 | | 加锁解锁 | 自动,比较方便 | 手动,且必须在 finally 块中保证释放锁 | | 范围限制 | 被获取的锁只能按相反方向释放 | 允许以任意顺序获取和释放多个锁,更灵活 | | 获取锁 | 获取不到只能阻塞等待 | 可以阻塞等待,可以通过可被中断的 lockInterruptibly()
    方法获取锁,也可以尝试获取,更灵活 | | 重入 | 可重入 | 可重入,可不重入 |

图示锁之间的关系