在上一篇文章中,对 synchronized 进行了介绍。 我们知道它是 Java 的关键字,属于 Java 的内置特性,是在 JVM 层面实现了对临界资源的同步互斥访问,但 synchronized 的粒度有些大,在处理实际问题时存在诸多局限性。比如:响应中断等。而下面要介绍的 Lock 提供了比 synchronized 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。
下面以 synchronized 与 Lock 的对比作为切入点,对 Java 中的 Lock 框架进行了学习介绍,最后给出了锁的一些相关概念。
二、 synchronized 与 Lock 的对比假设一个代码块被 synchronized 关键字修饰,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,直到占有锁的线程释放锁。事实上,占有锁的线程释放锁一般会是以下三种情况之一:
a. 占有锁的线程执行完了该代码块,然后释放对锁的占有;(正常释放锁)
b. 占有锁线程执行发生异常,此时JVM会让线程自动释放锁;(异常释放锁)
c. 占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。(主动释放锁)
我们知道,synchronized 是 Java 语言的内置特性,它可以轻松实现对临界资源的同步互斥访问。那么,为什么还会出现 Lock 呢?下面我们试着考虑以下三种情况:
(1)、情况一:在使用 synchronized 关键字的情形下,假设占有锁的线程由于要等待IO或者其他原因被阻塞了(比如调用sleep()方法),但是又没有释放锁,那么其他线程就只能一直等待,这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。
(2)、情况二:当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用 synchronized 关键字实现同步读的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。
(3)、情况三:我们可以通过 Lock 得知线程有没有成功获取到锁 (解决方案:ReentrantLock),但这个是 synchronized 无法办到的。
上面三种情形,我们都可以通过 Lock 来解决,但 synchronized 关键字却无能为力。事实上,Lock 是 java.util.concurrent.locks包 下的接口。 实现提供了比 synchronized 关键字 更灵活、更广泛、粒度更细 的锁操作,它能以更优雅的方式处理线程同步问题。也就是说,Lock 提供了比 synchronized 更多的功能。但是要注意以下几点:
1)、 synchronized 是 Java 的关键字,因此是 Java 的内置特性,是基于JVM层面实现的,其经过编译之后,会在同步块的前后分别
形成 monitorenter 和 monitorexit 两个字节码指令;而 Lock 是一个Java接口,是基于JDK层面实现的,通过这个接口可以实现
同步访问;
2)、 采用 synchronized 方式不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动
让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁 (发生异常时,不会自动释放锁),如果没有主动释放锁,就有可能
导致死锁现象。
这是很好理解的。synchronized 方式是 Java 原生支持的,开发人员在使用它来解决并发问题时,一定会方便很多,在这里,开发人员就不需要手动获取锁和释放锁,这些操作均有 Java 自身自动完成;而Lock方式是JDK层面的提供给开发人员的接口,因此开发人员在使用它来解决并发问题时,需要手动获取锁和释放锁。
三、 java.util.concurrent.locks包下常用的类与接口以下是 java.util.concurrent.locks包下主要常用的类与接口的关系:
通过查看 Lock 的源码可知,Lock 是一个接口:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException; // 可以响应中断
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 可以响应中断
void unlock();
Condition newCondition();
}
下面来逐个分析 Lock 接口中每个方法。lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的。unLock()方法是用来释放锁的。newCondition() 返回 绑定到此 Lock 的新的 Condition 实例 ,用于线程间的协作。
(1)、 lock()在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢?首先,lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。在前面已经讲到,如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
(2)、 tryLock() & tryLock(long time, TimeUnit unit)
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断(即不用等待需要访问的临界资源,立刻执行下面的代码)。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
一般情况下,通过tryLock()来获取锁时是这样使用的:
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
(3)、 lockInterruptibly()
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程 正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出 InterruptedException,但推荐使用后者,原因稍后阐述。因此,lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为interrupt()方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。因此,当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,那么只有进行等待的情况下,才可以响应中断的。与 synchronized 相比,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
总结:在使用Lock时,无论以哪种方式获取锁,习惯上最好一律将获取锁的代码放到 try…catch…,因为我们一般将锁的unlock操作放到finally子句中,如果线程没有获取到锁,在执行finally子句时,就会执行unlock操作,从而抛出 IllegalMonitorStateException,因为该线程并未获得到锁却执行了解锁操作。
2、ReentrantLockReentrantLock,即可重入锁。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面通过一些例子学习如何使用 ReentrantLock。
例 1 : Lock 的正确使用 public class Test {
private ArrayList arrayList = new ArrayList();
public static void main(String[] args) {
final Test test = new Test();
new Thread("A") {
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread("B") {
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
Lock lock = new ReentrantLock(); // 注意这个地方:lock被声明为局部变量
lock.lock();
try {
System.out.println("线程" + thread.getName() + "得到了锁...");
for (int i = 0; i
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?