设计步骤
- 继承
AbstractQueuedSynchronizer
:自定义同步器需要继承 AbstractQueuedSynchronizer
,以便复用 AQS 框架提供的队列管理、线程等待与唤醒等基础功能。
- 定义同步状态:根据共享锁的特性,同步状态(
state
)可以表示可用的共享资源数量。例如,如果共享资源有 N 个,那么 state
的初始值可以设为 N。
- 重写获取共享资源方法:
tryAcquireShared(int arg)
:实现获取共享资源的逻辑。根据当前 state
判断是否有可用资源,如果有则更新 state
并返回成功,否则返回失败。例如:
protected int tryAcquireShared(int arg) {
for (;;) {
int state = getState();
int next = state - arg;
if (next < 0 || compareAndSetState(state, next)) {
return next < 0? -1 : 1;
}
}
}
- 重写释放共享资源方法:
tryReleaseShared(int arg)
:实现释放共享资源的逻辑。更新 state
并唤醒等待队列中的线程。例如:
protected boolean tryReleaseShared(int arg) {
for (;;) {
int state = getState();
int next = state + arg;
if (compareAndSetState(state, next)) {
return true;
}
}
}
关键要点
- 同步状态管理:确保
state
的更新操作是线程安全的,通常使用 compareAndSetState
方法来实现原子更新,避免竞争条件。
- 队列操作:AQS 框架的等待队列(
CLH
队列)用于管理等待获取锁的线程。要理解队列节点的创建、入队、出队等操作,确保线程按照正确的顺序等待和唤醒。
- 线程等待与唤醒机制:
LockSupport
类提供的 park
和 unpark
方法用于线程的等待和唤醒。在获取资源失败时,将线程包装成队列节点并放入等待队列,同时调用 park
方法使线程进入等待状态;在释放资源时,从等待队列中唤醒合适的线程。
性能优化策略
- 减少线程上下文切换:
- 尽量使用非阻塞的 CAS 操作:如在更新
state
时使用 compareAndSetState
,避免使用独占锁导致线程阻塞和上下文切换。
- 使用自旋:在短时间内尝试获取资源,避免直接进入阻塞状态。可以通过设置一个自旋次数,在自旋次数内不断尝试获取锁,如果获取成功则避免了线程上下文切换。
- 避免不必要的锁竞争:
- 优化锁粒度:如果共享资源可以细分,考虑使用多个锁分别保护不同部分的资源,减少锁竞争。
- 公平性选择:如果对公平性要求不高,可以选择非公平锁,非公平锁在某些情况下可以减少线程的饥饿现象,提高整体性能。因为非公平锁允许新线程在队列头部线程等待时尝试获取锁,有一定概率直接获取成功,减少排队开销。