简介
本文将简单介绍Lock
接口和AbstractQueuedSynchronizer
同步器,并基于它们两个实现一个取名为互斥锁的同步组件
Lock接口
在Java SE 5之后并发包中提供了Lock
接口来实现同步组件,当然也能够实现锁,因为锁是一种特定的同步组件。它实现的锁能够提供和Synchronized
的类似的同步功能并且拥有更多的灵活性。这些灵活性主要表现在一下三个方面:
- 可以尝试非阻塞地获取锁:
tryLock()
相关方法没有获取到锁能够理解返回而不是阻塞等待 - 能被中断地获取锁:获取到锁的线程能够响应中断,中断时中断异常抛出并且锁释放
- 超时获取锁:如果指定时间内没有获取到锁则返回
同步器
队列同步器AbstractQueuedSynchronizer
(AQS,简称同步器)是用来构建同步组件的基础框架,它使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的设计是基于模板方法模式的。定义同步器时,我们可以通过继承AQS来实现。AQS是一个抽象类,其中我们需要关注的方法方法可大致分为3类:
- 访问或修改同步状态的3个方法,分别是:
getState()
:获取当前同步状态setState(int newState)
: 设置当前同步状态compareAndSetState(int expect, int update)
: 使用CAS原子地设置当前状态
- 可以在子类中重写的方法;
- 定义好的模板方法,这些模板方法会调用2中重写好的方法,它们基本也可以分为三类:
- 独占式获取与释放同步状态
- 共享式获取与释放同步状态
- 查询同步队列中的等待线程情况
自定义同步组件
有了上面两方面的基础知识,我们就可以自定义同步组件了。
自定义同步组件就是写一个Lock
接口的实现类,该实现基本都是通过聚合一个同步器的子类来完成线程访问控制的。而同步器的子类推荐被定义为同步组件的静态内部类。
下面自定义了一个“独占锁”的同步组件,其目的是同一个时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待。代码如下:
package site.pengcheng.concurrent.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @author pengchengbai
* @description
* @date 2020/5/24 11:35 上午
*/
public class Mutex implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 当状态为0的时候获取锁
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放锁,将状态设置为0
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
Condition newCondition() {
return new ConditionObject();
}
}
/**
* 下面的方法表示将同步组件的操作代理给自定义同步器Sync
*/
private final Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
// 返回一个Condition,每个condition都包含了一个condition队列
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
通过这个示例可以看出,当用户使用独占锁这个同步组件的时候,并不会直接和内部的同步器打交道,而是使用Mutex提供的方法(Lock中定义方法)。之前也说到,AQS是用来构建同步组件的基础框架,它们这种“合作”方式可以大大降低实现一个自定义同步组件的门槛。
总结
同步器是实现同步组件的关键。通过聚合同步器实现同步组件的时候,内部利用了同步器实现具体语义,对使用者隔离了实现细节。可以这样理解,同步组件(比如锁)是面向使用者的,它定义了使用者与其交互的接口,屏蔽了具体实现;同步器是面向同步组件的,它屏蔽了同步状态管理、线程排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。