一、concurrent包的设计
要了解Java为我们提供的基于Lock接口(以及相关实现类)实现的锁功能,我们首先要看一下整个concurrent包下的设计。具体设计如下所示:
【concurrent包的设计】
在上图中,我们大致可以看出courrent包下的整体结构。整个包大致分为了三层。
- 高层:Lock、同步器、阻塞队列等。
- 中层:AQS(AbstractQueuedSynchronizer)、非阻塞数据结构、原子变量类(java.util.concurrent.atomic包中的类)。
- 底层:volatile变量的读/写、CAS操作。
其中每个层中的依赖关系也很明显,AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),都是基于底层实现,而高层类又依赖中层这些基础类
特别需要注意的是于Lock接口(以及相关实现类)相关的锁功能在整个高层中起着非常重要的重要。虽然没有直接在图中表述Lock接口在高层中的关系,但是在高层中我们所罗列的同步器、阻塞队列、并发容器等,或多或少都依赖或使用其Lock接口(以及相关实现类)实现的锁功能。
二、Lock接口(以及相关实现类)UML类图
总所周知锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁就能够防止多个线程同时访问共享资源(但是有些锁可以允许多线程并发的访问共享资源,比如我们后期将会讲解的读写锁)
在Lock接口出现之前,Java程序是靠synchronized关键字来实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口以及相关实现类,来实现锁的功能。
在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性,只是代码写法上有点区别,一个表现为API层面的互斥锁(lock()和unlock()方法配合try/finally语句块来完成),另一个表现为原生语法层面的互斥锁。
不过,相比synchronized,ReentrantLock增加了一些高级功能,主要有以下3项:
- 等待可中断
- 指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
- 公平锁
- 指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。
- synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
- 锁绑定多个条件
- 指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。
关于 Lock接口(以及相关实现类)的UML类图,具体如下所示:
【Lock接口(以及相关实现类)UML类图】
我们来简单分析下
- Lock接口
- 实现类: ReentrantLock
- 实现类: WriteLock
- 实现类: ReadLock
Lock的实现类普遍持有AQS的实现类,实质上lock的实现主要依赖AQS实现调度控制(主要实现并发阻塞),依赖Condition实现信号量控制(主要实现等待通知),依赖LockSupport实现底层控制
- AbstractOwnableSynchronizer 抽象类
- 实现类: AbstractQueuedLongSynchronizer
- 实现类: AbstractQueuedLongSynchronizer
AbstractOwnableSynchronizer的实现类普遍持有 Condition的实现类,作为信号量控制
我们在下文进行逐一的分析,现在先给一个简单示例:
class Test2 {
private static volatile int condition = 0;
private static Lock lock = new ReentrantLock();
private static Condition lockCondition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
while (!(condition == 1)) {
lockCondition.await();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
System.out.println("a executed by condition");
}
});
A.start();
Thread.sleep(2000);
condition = 1;
lock.lock();
try {
lockCondition.signal();
} finally {
lock.unlock();
}
}
}
可以看到通过 lock.newCondition() 可以获得到 lock 对应的一个Condition对象lockCondition ,lockCondition的await()、signal()方法分别对应之前的Object的wait()和notify()方法。整体上和Object的等待通知是类似的。
三、Lock接口
方法名称 | 描述 |
---|---|
void lock() | 获取锁. 成功则向下运行,失败则阻塞 |
void lockInterruptibly() throws InterruptedException | 可中断地获取锁,在当前线程获取锁的过程中可以响应中断信号 |
boolean tryLock() | 尝试非阻塞获取锁,调用方法后立即返回,成功返回true,失败返回false |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 在超时时间内获取锁,到达超时时间将返回false,也可以响应中断 |
void unlock(); | 释放锁 |
Condition newCondition(); | 获取等待通知组件实现信号控制,等待通知组件实现类似于Object.wait()方法的功能 |
从Lock提供的接口可以看出来,显示锁至少比synchronized多了以下功能:
- 可中断获取锁:使用synchronized关键字获取锁的时候,如果线程没有获取到被阻塞了,那么这个时候该线程是不响应中断(interrupt)的,而使用Lock.lockInterruptibly()获取锁时被中断,线程将抛出中断异常。
- 可非阻塞获取锁:使用sync关键字获取锁时,如果没有成功获取,只有被阻塞,而使用Lock.tryLock()获取锁时,如果没有获取成功也不会阻塞而是直接返回false。
- 可限定获取锁的超时时间:使用Lock.tryLock(long time, TimeUnit unit)。
- 其实显示锁还有其他的优势,比如同一锁对象上可以有多个等待通知队列(相当于Object.wait()),我们后面会讲。
其实除了更多的功能,显示锁还有一个很大的优势:synchronized的同步是jvm底层实现的,借助物理设备的指令控制,对一般程序员来说程序遇到出乎意料的行为的时候,除了查官方文档几乎没有别的办法;而显示锁除了个别操作用了底层的Unsafe类之外,几乎都是用java语言实现的
当然显示锁也不是完美的,否则java就不会保留着synchronized关键字了,显示锁的缺点主要有两个:
- 使用比较复杂,这点之前提到了,需要手动加锁,解锁,而且还必须保证在异常状态下也要能够解锁。而synchronized的使用就简单多了。
- 效率较低,synchronized关键字毕竟是jvm底层实现的,因此用了很多优化措施来优化速度(偏向锁、轻量锁等),而显示锁的效率相对低一些。
因此当需要进行同步时,优先考虑使用synchronized关键字,只有synchronized关键字不能满足需求时,才考虑使用显示锁。