Java中的Lock接口
什么是 Lock
在 Java 多线程编程中,我们经常使用 synchronized 关键字来实现同步,控制多线程对变量的访问,来避免并发问题。
Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock 提供了比 synchronized 更多的功能。
Lock 不是 Java 中的关键字,而是 java.util.concurrent.locks
包中的一个接口,因此只能在代码块中进行使用,不能用来修饰方法。
Lock 与的 Synchronized 区别
- Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内置特性。Lock 是一个类,通过这个类可以实现同步访问;
- Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
Lock 接口
1 | public interface Lock { |
下面来逐个讲述 Lock 接口中每个方法的使用
1、lock
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。
因此一般来说,使用 Lock 必须在try{} catch{}
块中进行,并且将释放锁的操作放在finally
块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用 Lock 来进行同步的话,是以下面这种形式去使用的:
1 | Lock lock = ...; |
2、lockInterruptibly
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。
也就使说,当两个线程同时通过lock.lockInterruptibly()
想获取某个锁时,假若此时线程 A 获取到了锁,而线程 B 只有在等待,那么对线程 B 调用threadB.interrupt()
方法能够中断线程 B 的等待过程。(详情可以看ReentrantLock方案三
)
由于lockInterruptibly()
的声明中抛出了异常,所以lock.lockInterruptibly()
必须放在 try 块中或者在调用lockInterruptibly()
的方法外声明抛出InterruptedException
。
因此 lockInterruptibly()一般的使用形式如下:
1 | public void method() throws InterruptedException { |
注意:当一个线程获取了锁之后,是不会被 interrupt()方法中断的。因为本身在前面的文章中讲过单独调用 interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
因此当通过 lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
而用 synchronized 修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
3、tryLock
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回 true,如果获取失败(即锁已被其他线程获取),则返回 false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)
方法和 tryLock()
方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回 false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回 true。
所以,一般情况下通过 tryLock 来获取锁时是这样使用的:
1 | Lock lock = ...; |
4、newCondition
关键字 synchronized 与 wait()
/ notify()
这两个方法一起使用可以实现 等待/通知模式
Lock 锁的newContition()
方法返回 Condition 对象,Condition 类也可以实现等待/通知模式。
用 notify()
通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知, Condition 比较常用的两个方法:
await()
会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行。signal()
用于唤醒一个等待的线程。
注意:在调用 Condition
的 await()
/ signal()
方法前,也需要线程持有相关的 Lock 锁,调用await()
后线程会释放这个锁,在singal()
调用后会从当前Condition
对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。
ReentrantLock
ReentrantLock,意思是可重入锁
ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更多的方法。
首先看一个错误案例。
1 | public class Insert { |
测试方法
1 | public class Test { |
得到了错误结果
问题:第二个线程怎么会在第一个线程释放锁之前得到了锁?
原因: 在 insert 方法中的 lock 变量是局部变量,每个线程执行该方法时都会保存一个副本,那么理所当然每个线程执行到 lock.lock()处获取的是不同的锁,所以就不会发生冲突。
解决方案:
方案一: lock 实现
1 | public class Insert { |
再次进行上面的测试,结果
方案二: tryLock 实现
1 | public class Insert { |
方案三: lockInterruptibly 实现
首先重写 run()方法
1 | public class Run implements Runnable { |
测试方法,线程 1 先运行,线程 2 运行再调用interrupt()
方法会终止,抛出异常
1 | public class Test { |
Condition
Condition
是 java.util.concurrent.locks
包中的一个接口
可以翻译成 条件对象,其作用是线程先等待,当外部满足某一条件时,在通过条件对象唤醒等待的线程。
Condition 可以看做是 Obejct 类的wait()
、notify()
、notifyAll()
方法的替代品,与 Lock 配合使用。
从整体上来看Obejct 类的wait()
、notify()
、notifyAll()
方法是与对象监视器配合完成线程间的等待/通知机制,而 Condition 是与 Lock 配合完成等待通知机制,前者是 java 底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同:
- Condition 能够支持不响应中断,而通过使用 Object 方式不支持;
- Condition 能够支持多个等待队列(new 多个 Condition 对象),而 Object 方式只能支持一个;
- Condition 能够支持超时时间的设置,而 Object 不支持
1 | /** |