Java中的并发编程很多都是以队列同步器AbstractQueuedSynchronizer为基础的, 例如ReentrantLock,CountDownLatch等。
下面介绍其构成以及相应的实现。
构成
private volatile int state;
AbstractQueuedSynchronizer中通过一个int类型的成员变量state来表示同步状态,该变量使用volatile来修饰,保证多线程之间的可见性。
以及一个内置的队列来完成资源排队。
其中对于state有如下几个操作方法:
- getState() 获取同步状态
- setState() 设置同步状态
- compareAndSetState() 使用CAS设置同步状态,该方法能保证状态设置的原子性
具体原理
同步器依赖内部的一个双向队列来完成同步状态的管理。该队列遵循先入先出的原则。
当线程获取同步状态失败时,同步器会执行以下步骤:
- 构造一个节点Node, 该节点包含当前线程、当前线程的等待状态、前驱节点引用、后驱节点引用等信息
- 将该节点加入到队列当中(加入到队列的尾结点)
- 阻塞该线程
当释放同步状态时,只会将首节点的线程唤醒, 使其尝试获取同步状态。
同步器中包含了指向头结点和尾结点的应用head, tail, 这两个节点作用分别如下:
- 首节点是获取同步状态成功的节点, 首节点的线程在释放同步状态时, 会唤醒后继节点, 当后继节点成功获取到同步状态时,就会将自己设置为首节点, 以此类推。
- 因为新加入的节点都会放在尾节点, 所以为了线程安全,提供了一个利用CAS设置尾结点的方法compareAndSetTail()。
同步状态获取
同步状态获取的方式分为两种:
- 独占式获取同步状态, 在同一时刻只能有一个线程能获取到同步状态,其他线程都将会被阻塞,例如文件的写操作。 对应
acquire()
方法
- 共享式获取同步状态, 在同一时刻允许有多个线程获取到同步状态,例如文件的读操作。对应
acquireShared()
方法
下图是同步器独占式获取同步状态的流程图。
ReentrantLock
可重入锁。支持一个线程对资源的重复加锁。
同时, 该锁还支持是否公平的获取锁。注意,synchronized支持的锁也是可重入的,因为它会在对象头存储获取锁的线程ID。
几个问题:
- ReentrantLock是如何实现可重入的呢?
首先ReentrantLock组合了同步器AbstractQueuedSynchronizer来实现锁的功能。其中state在ReentrantLock用来表示线程获取锁的次数,当线程获取锁时:
需要判断当前线程是否是获取锁的线程, 如果是则将state值加1, 并返回获取锁成功的标识, 这样就能保证可重入。
- 锁是如何释放的?
答案是仍然使用state来进行释放。 当调用unlock()
释放锁时, 同步器会将state的值减1, 只有当state的值为0时, 才能返回锁释放成功的标识。
- 公平锁时如何实现的?
ReentrantLock默认是非公平锁, 在获取锁时,只要使用CAS设置同步状态成功, 那么就表示该线程就获取锁成功。
对于公平锁,在获取同步状态时, 还需要判断同步队列中当前节点是否有前驱节点, 如果有, 则说明有更早的线程在等待获取锁, 则当前线程需要加入到等待队列的尾结点, 等待之前所有线程获取节点成功并释放之后才能尝试获取锁。
ReentrantReadWriteLock
可重入的读写锁。维护了一个读锁和一个写锁。其中读锁是共享锁, 同一时刻可以有多个线程拥有,写锁是排他锁, 同一时刻只能有一个写锁。
其也是组合AbstractQueuedSynchronizer来实现的。具体如何实现的呢?
关键也是state变量的设计, 将state变量按位拆成2部分, 高16位表示读状态, 低16位表示写状态, 在进行读锁和写锁的操作时,和ReentrantLock类似,对这两个状态进行加减运算。
写锁获取流程:
- 如果当前线程已经获取了写锁, 则增加写状态,并返回获取写锁成功的标识。此处保证了可重入
- 如果有其他线程获取了读锁或写锁,则当前线程进入等待状态,等待其他线程的写锁和读锁全部释放完毕, 再去尝试获取。此处保证了排他性。
读锁获取流程:
- 如果有其他线程获取了写锁, 则当前线程进入等待状态。
- 如果没有其他线程获取了写锁,读锁总是能保证成功获取。特别的,如果当前线程已经获取了读锁, 则增加读状态(保证可重入性),并返回获取读锁成功的标识。
CountDownLatch
某个任务的执行,需要等到多个线程都执行完毕之后才可以进行。 该场景可以使用CountDownLatch来实现。
其也是组合AbstractQueuedSynchronizer来实现的。关键是state的设计。state被用来表示需要等待的线程个数。
在CountDownLatch中,主要涉及到如下三个方法:
- 构造函数中count参数, 指定需要等待的线程个数, 也就是state变量
-
countDown()
方法, 每调用一次,state减1
-
await()
方法, 等待, 只有当state等于0时, 才会从该方法处返回, 否则会一直阻塞。
注意, countDown()
方法最好写在finally中, 防止发生死锁。因为如果在调用countDown()
之前程序发生了异常, 导致该方法没有执行,那么state变量就永远不可能为0, 此时调用await()
方法的线程则会永远阻塞在该处。
或者使用await(long timeout, TimeUnit unit)
, 加入超时参数, 当达到超时时间自动返回。
参考资料:
为0, 此时调用await()
方法的线程则会永远阻塞在该处。
或者使用await(long timeout, TimeUnit unit)
, 加入超时参数, 当达到超时时间自动返回。
参考资料:
- 《Java并发编程的艺术》