在JDK1.5之前,在协调共享对象的访问时可使用的机制只有synchronized和volatile,这就是我们所熟知的内置锁;在JDK1.5中,著名并发编程大师Doug Lea使用Java编写了一个并发编程框架(java.util.concurrent.* => JUC),提供了更多并发编程的高级手段。例如可以通过显示加锁以保证多线程编程的可靠性,这就是大名鼎鼎的 显示锁。本系列文章将介绍一下 java.util.concurrent.locks 下的模块及其实现原理。
内置锁和显示锁
- 在JDK1.5之前,为了保证对共享变量的同步访问,需要对共享变量加锁,以避免出现线程安全问题,这时我们使用的是synchronized,可以对代码块和方法加锁,这就是我们所熟知的内置锁,也可以称为隐式锁。Java中的内置锁被称为对象监视器(monitor),每个继承自java.lang.Object的对象都有一个对象监视器。内置锁是JDK基于JVM的一个重要实现。如下代码:
//源码
public class Synchronized {
public static void main(String[] args) {
synchronized (Synchronized.class){
}
m();
}
public static synchronized void m(){
}
}
//对应的Java字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class Synchronized
2: dup
3: astore_1
4: monitorenter //插入同步代码块的开始位置
5: aload_1
6: monitorexit //插入同步代码块的结束位置
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: invokestatic #3 // Method m:()V
18: return
public static synchronized void m();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED //方法同步使用ACC_SYNCHRONIZED标记
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 14: 0
}
- JDK1.5之后新增的显示锁是一种更为高级的功能,作为内置锁不适用时的选择。Lock完全是由Java写成的,在Java这个层面无关于JVM实现。显示锁在JDK1.6之前效率远高于synchronized,即使JDK1.6对synchronized做了优化(偏向锁,轻量级锁等),其效率还是略高于synchronized。虽然它缺少了synchronized隐式获取锁和释放锁的便捷性,但是却拥有了锁获取与释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
lock 的体系结构
下图展示的是 java.util.concurrent.locks 下几个重要的接口和类的继承关系:
简单介绍与分析如下:
-
AbstractOwnableSynchronizer 是一个抽象的同步器类(关于同步器后文会有详细介绍),此类是创建锁和相关同步器的基础,可以由线程以独占方式拥有同步器
-
AbstractQueuedLongSynchronizer 继承于 AbstractOwnableSynchronizer,是一个抽象的队列同步器,以long来维护同步状态的一个 AbstractQueuedSynchronizer 版本,此类的结构、属性和方法与 AbstractQueuedSynchronizer 相同,只是所有与状态相关的参数和结果都定义为long,当创建需要64位状态的多级别锁时将会用到该同步器
-
AbstractQueuedSynchronizer 继承于 AbstractOwnableSynchronizer,是一个以 int 值来维护同步状态的同步器。该同步器是用来构建锁或者其他同步组建的基础框架,它使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作
-
LockSupport 定义了一组公共的静态方法,是构建同步组建的基础工具。AbstractQueuedSynchronizer 和 AbstractQueuedLongSynchronizer 实现同步需要其支持
-
Condition 接口提供了类似于Object的监视器方法,可以与Lock配合实现等待/通知机制。
-
Lock 接口定义了锁获取与释放的基本操作
-
ReadWriteLock 接口定义了获取读锁和写锁的方法
-
ReentrantLock 实现了Lock接口,其操作都有委派给了内部类 Sync,Sync继承了同步器类 AbstractQueuedSynchronizer
-
ReentrantReadWriteLock 实现了 ReadWriteLock 接口,其操作都有委派给了内部类 Sync,Sync继承了同步器类 AbstractQueuedSynchronizer
Lock 接口简介
上面聊了一下Lock的体系结构,大体清楚了Java并发包中的锁是什么,现在就具体说说Lock接口给我们提供了哪些API,以及我们应该如何使用这些API
下面就是Lock接口中的API
public interface Lock {
// 获取锁,调用该方法时当前线程将会获取锁,当获得锁之后将会从该方法返回
void lock();
// 可中断的获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程
void lockInterruptibly() throws InterruptedException;
// 尝试非阻塞的获取锁,调用该方法后立即返回,如果能够获取则返回true,否则返回false
boolean tryLock();
// 超时的获取锁,当线程出现以下3中情况时返回:
// 1. 当前线程在超时时间内获得锁
// 2. 当前线程在超时时间内被中断
// 3. 超时时间结束,返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 获取等待通知的组建,该组建和当前的锁绑定,当前线程只有获得了锁
// 才能调用该组件的wait()方法,调用后,当前线程将释放锁
Condition newCondition();
}
Lock的简单用法如下
public class TestLock {
private ReentrantLock lock = new ReentrantLock();
public void test() {
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
}
ReentrantLock是可重入锁,实现了Lock接口,后续文章会提到。这里需要注意两点:
-
一定要在finally中释放锁,否则后续可能会出现很严重的问题。
-
不要将获取锁的过程写在try块中,因为假如在获取锁时发生了异常,异常抛出的同时也会导致锁无法释放
Lock接口具备而synchronized没有的特性如下:
特性 | 描述 |
---|---|
尝试非阻塞的获取锁 | 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁 |
能被中断的获取锁 | 获取锁的线程能够响应中断:当获取到锁的线程被中断时,中断异常将会抛出,同时锁被释放 |
超时获取锁 | 在指定的截止时间之前获取锁,如果到了截止时间仍无法获取锁,则返回 |
参考
《并发编程的艺术》
《并发编程实战》