Java 并发包 Concurrent 的包结构共可分为五个部分:
– 原子类
– 锁
– collection并发集合框架
– excutor线程池
– 同步工具
本文介绍 Lock 接口和其与 synchronized 关键字的对比。
Lock接口
尽管 synchronized 在语法上已经足够简单了,在 JDK5 之前只能借助此实现,但是由于是独占锁,性能却不高,因此 JDK5 以后就开始借助于 JNI 来完成更高级的锁实现。
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。
Lock 实现提供了使用 synchronized 方法和语句所没有的其他功能,包括提供了一个非块结构的获取锁尝试 (tryLock())、一个获取可中断锁的尝试 (lockInterruptibly()) 和一个获取超时失效锁的尝试 (tryLock(long, TimeUnit))。
void lock()
获取锁。
如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。
void lockInterruptibly()
如果当前线程未被中断,则获取锁。
如果锁可用,则获取锁,并立即返回。
如果锁不可用,出于线程调度目的,将禁用当前线程,并且在发生以下两种情况之一以前,该线程将一直处于休眠状态:
锁由当前线程获得;
或者其他某个线程中断当前线程,并且支持对锁获取的中断。
boolean tryLock()
仅在调用时锁为空闲状态才获取该锁。
如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false。
boolean tryLock(long time, TimeUnit unit)
如果锁在给定的等待时间内空闲,并且当前线程未被 中断,则获取锁。
如果锁可用,则此方法将立即返回值 true。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在发生以下三种情况之一前,该线程将一直处于休眠状态:
锁由当前线程获得;或者
其他某个线程中断当前线程,并且支持对锁获取的中断;或者
已超过指定的等待时间
如果获得了锁,则返回值 true。
如果当前线程:
在进入此方法时已经设置了该线程的中断状态;或者
在获取锁时被中断,并且支持对锁获取的中断,
则将抛出 InterruptedException,并会清除当前线程的已中断状态。
如果超过了指定的等待时间,则将返回值 false。如果 time 小于等于 0,该方法将完全不等待。
void unlock()
释放锁。
Lock 实现通常对哪个线程可以释放锁施加了限制(通常只有锁的保持者可以释放它),如果违背了这个限制,可能会抛出(未经检查的)异常。该 Lock 实现必须对所有限制和异常类型进行记录。
Condition newCondition()
返回绑定到此 Lock 实例的新 Condition 实例。
在等待条件前,锁必须由当前线程保持。调用 Condition.await() 将在等待前以原子方式释放锁,并在等待返回前重新获取锁。
synchronized和Lock
可以比较一下 synchronized 关键字和 lock 的性能:
(JDK6以后对 synchronized 进行了很多优化,而 Lock 没有太多优化空间,二者其实开销是差不多的。在更新的版本中内置锁只会比显式锁性能更好。)
一个简单的例子:
public class TestLock {
private static int a = 0;
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
final int threadSize = 100000;
Thread[] ts = new Thread[threadSize];
for (int i = 0; i < threadSize; i++) {
ts[i] = new Thread() {
public void run() {
synchronized (obj) {
a++;
}
}
};
}
long start = System.currentTimeMillis();
for(Thread t:ts) {
t.start();
}
for(Thread t:ts) {
t.join();
}
long end = System.currentTimeMillis();
System.out.println(a);
System.out.println(end-start);
}
}
运行结果:
100000
6562
改为使用 Lock 接口实现的 ReentrantLock:
public class TestLock2 {
private static int a = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
final int threadSize = 100000;
Thread[] ts = new Thread[threadSize];
for (int i = 0; i < threadSize; i++) {
ts[i] = new Thread() {
public void run() {
lock.lock();
try{
a++;
}finally{
lock.unlock();
}
}
};
}
long start = System.currentTimeMillis();
for(Thread t:ts) {
t.start();
}
for(Thread t:ts) {
t.join();
}
long end = System.currentTimeMillis();
System.out.println(a);
System.out.println(end-start);
}
}
运行结果:
100000
6431
根据多次运行的结果,发现在 JDK8 中两种锁的开销其实是差不多的。
其实是由于这两种锁都是独占锁,JDK5 以前内置锁性能低的原因是它没做任何优化,直接使用系统的互斥体来获取锁。显式锁除了 CAS 的时候利用的是本地代码以外,其它的部分都是 Java 代码实现的,在后续版本的 Java 中,显式锁不太可能会比内置锁好,只会更差。使用显式锁的唯一理由是要利用它更多的功能。