显式锁(二)Lock接口与显示锁介绍
一、显式锁简介
显式锁,这个叫法是相对于隐式锁synchronized而言的,加锁和解锁都要用户显式地控制。显示锁Lock是在Java5中添加到jdk的,同synchronized一样,这也是一种协调共享对象访问的机制。但是它不是用来替代内置锁的,而是一种可选择的高级功能。
1、Lock接口提供了synchronized关键字不具备的主要特性:
- 尝试非阻塞获取锁:当前线程尝试获取锁,如果这一时刻,锁没有被其他线程占有,那么成功获取锁并返回。
- 能被中断地获取锁:当线程正在等待获取锁,则这个线程能够 响应中断,即当中断来了,线程不会阻塞等待获取锁,抛出中断异常。
- 超时获取锁:在指定的截止时间前获取锁,如果截止时间到了仍旧无法获取锁,则返回;
关于Lock与synchronized的区别,请参考我的上一篇博文。
2、两种显式锁
JDK中提供了两种显式锁,即Lock的实现方式有两种:ReentrentLock(重入锁)、ReentrantReadWriteLock.ReadLock 和 ReentrantReadWriteLock.WriteLock(这两个锁是由其父类ReentrantReadWriteLock 控制使用,可视为一体,称为读写锁)。具体的Lock接口的继承结构,可参考下图:
二、Lock接口的API
方法名称 | 描述 |
---|---|
void lock( ) | 阻塞地获取锁,直到获取到锁才返回,而且是不可中断的。 |
void lockInterruptibly( ) throws InterruptedException | 可中断地获取锁,与lock()方法的不同之处,在于该方法在阻塞等待锁的过程中会 响应中断。 |
boolean tryLock( ) | 尝试非阻塞地获取锁,即调用该方法后,立刻返回,成功获取锁,返回true,失败则返回false。 |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 可中断的超时获取锁,在以下3种情况下会返回: 1. 当前线程在指定时间内获得了锁; 2. 当前线程在指定时间内被中断; 3. 指定时间结束(超时结束),返回false; |
void unlock( ) | 释放锁。 |
Condition newCondition() | 等待通知组件,当前线程只有获得了锁,才能调用该组件的wait()方法,调用后,线程将会释放锁 |
三、ReentrentLock重入锁 详解
ReentrentLock 是Lock接口的常用的实现类,是一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。更加灵活(实现了显示锁的特性)。下面将由ReentrentLock 来介绍体验显示锁的特性:
1、可中断锁与不可中断锁 - - lockInterruptibly( )、lock( )
Lock接口不仅提供了不可中断锁(synchronized是不可中断的),还有可中断锁。在某些应用场景下,可中断锁的用处很大:当检测到线程等待锁的时间过长,不能继续等待,需要进行下一步操作;或者某个任务已经完成了,则中断其他等待锁来完成这个任务的线程。
显式锁的加锁和解锁都是由用户来操作,所以用户一旦忘记释放锁了,很可能就会造成线程讥饿。正确的用是使用 try-finally 确保锁能被正确释放。
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
如果是可中断方式获取锁 lockInterruptibly,则要 try-finally 要处于 捕获中断异常的 try-catch 块间,或者在方法上抛出中断异常。
public void method() throws InterruptedException {//抛出中断异常
lock.lockInterruptibly();
try {
//方法体.....
}
finally {
lock.unlock();
}
}
public void m() {
try {
lock.lockInterruptibly();
try{
// ... method body
}finally{
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
中断锁的例子:
//静态变量
static Lock lock =new ReentrantLock();
public static void main(String[] args) {
Thread A = new Thread("A"){
@Override
public void run() {
//不可中断锁,在等待获取锁的过程,忽略中断
lock.lock();
try {
System.out.println("线程"+getName()+"成功获取锁");
} finally {
lock.unlock();
}
}
};
Thread B = new Thread("B"){
@Override
public void run() {
try {
//可中断锁,在等待获取锁的过程中,如果有中断到来,将会停止获取锁,并抛出中断异常
lock.lockInterruptibly();
try{//
System.out.println("线程"+getName()+"成功获取锁");
}finally{
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//mian线程保持着锁时,再启动A、B线程,确保中断A、B线程时,A、B线程在等待获取锁
lock.lock();
try{
A.start();
B.start();
System.out.println("中断A、B线程");
A.interrupt();
B.interrupt();
}finally{
lock.unlock();
}
}
运行结果:
2、非阻塞获取锁 - - tryLock( )
调用此方法后,无论是否成功获取锁,都将立刻返回,成功获取锁,返回true,否则,返回false;
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Thread A = new Thread("A"){
@Override
public void run() {
if(lock.tryLock()){//尝试非阻塞获取锁
try{
System.out.println(getName()+"成功获取锁");
}finally {//释放锁
lock.unlock();
}
}else{
System.out.println(getName()+"获取锁失败!");
}
}
};
if(lock.tryLock()){//main线程成功获取锁后,启动线程A
try{
A.start();
System.out.println(Thread.currentThread().getName()+"启动线程A");
//sleep可以保持锁,模拟main线程还要运行1秒
TimeUnit.SECONDS.sleep(1);
}finally {
lock.unlock();
}
}else{
System.out.println("程序结束!");
}
}
运行结果:
main启动线程A
A获取锁失败!
3、超时获取锁 - - tryLock(long time, TimeUnit unit)
与tryLock( )相比,除了不是立刻返回,而是超时等待外,tryLock(long time, TimeUnit unit)还是可以被中断的。
改造一下上面的例子,将tryLock()换成 tryLock(1,TimeUnit.SECONDS):
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Thread A = new Thread("A"){
@Override
public void run() {
try {
if(lock.tryLock(1,TimeUnit.SECONDS)){//超时等待获取锁
try{
System.out.println(getName()+"成功获取锁");
}finally {
lock.unlock();
}
}else{
System.out.println(getName()+"获取锁失败!");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
if(lock.tryLock()){//main线程成功获取锁后,启动线程A
try{
A.start();
System.out.println(Thread.currentThread().getName()+"启动线程A");
//sleep可以保持锁,模拟main线程还要运行1秒
TimeUnit.SECONDS.sleep(1);
}finally {
lock.unlock();
}
}else{
System.out.println("程序结束!");
}
}
运行结果:
main启动线程A
A成功获取锁
上面的3小点是 显式锁的区别与隐式锁 synchronize的特性,接下来的几点,则是ReentrantLock的方法讲解,不是 显式锁的特性;
4、可重入的锁
可重入的锁:是指线程持有了某个锁,便可以进入任意的该锁同步着的代码块。
不可重入的锁:线程进入任何一个同步的代码块都必须获取锁,即使这些代码块是同一个锁;
使用可重入锁,可以很大程度地避免死锁,所以不可重入锁的应用场景很少,JDK提供的锁(synchronize、ReentrentLock、ReentrantReadWriteLock)都是可重入的锁。当线程获取重入锁时,先判断线程是不是已经持有该锁,如果是,那么重入计数器加一,否则去获取该锁。ReentrentLock中,提供了锁被线程重入的次数的方法 - - getHoldCount()。
//静态变量
static ReentrantLock lock = new ReentrantLock();
static int num = 5;
public static void main(String[] args)
Thread B = new Thread("B"){
@Override
public void run() {
lock.lock();
try{
int aa = 5*5;
//countNumber里面也要获取同步锁,而且与当前线程所拥有的锁是同一个
countNumber(aa); //可重入,意味着不需要再次去等待获取锁
System.out.println("num的值是:"+num);
}finally {
lock.unlock();
}
}
};
B.start();
}
public static void countNumber(int a){ //包含同步代码块
lock.lock(); //如果是重入,则重入计数器加一
try{
num+=a;
System.out.println("锁lock被当前线程重入的次数:"+lock.getHoldCount());
}finally {
lock.unlock();//如果是重入,则重入计数器减一
}
}
运行结果:
锁lock被当前线程重入的次数:2
num的值是:30
5、公平锁 和 非公平锁
ReentrantLock 支持公平锁,可以在构造方法中传入参数设置,默认为非公平锁;
ReentrantLock(boolean fair): 创建一个具有给定公平策略的 ReentrantLock。此类的构造方法接受一个可选的公平 参数。当设置为 true 时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程,即先来先获取锁。否则此锁将无法保证任何特定访问顺序。
关于公平锁的注意以下三点:
- 与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。
- 公平锁不能保证线程调度的公平性。公平锁保证的是获取锁的顺序。
- 未定时的 tryLock 方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。
public class MyTest{
//设置成公平锁模式
static ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) {
Thread threadA = new Thread(new MyRunable4(),"threadA");
Thread threadB = new Thread(new MyRunable4(),"threadB");
Thread threadC = new Thread(new MyRunable4(),"threadC");
Thread threadD = new Thread(new MyRunable4(),"threadD");
//启动四个线程
threadA.start();
threadB.start();
threadC.start();
threadD.start();
}
public static void countNumber(){
//
lock.lock();
try{
System.out.println("线程锁"+Thread.currentThread().currentThread().getName()+"成功获取了锁!");
}finally{
lock.unlock();
}
}
}
class MyRunable4 implements Runnable{
@Override
public void run() {
while(true){
MyTest.countNumber();
}
}
}
运行结果:
下面的结果尽管不是按照启动的顺序来执行(这是因为调用start( )方法后是进入就绪队列,公平锁无法保证线程的调度,因此4个线程谁被先调度就先去获取锁),但是却是一直按照特定的顺序来执行的(C->B->A->D);
线程锁threadC成功获取了锁!
线程锁threadB成功获取了锁!
线程锁threadA成功获取了锁!
线程锁threadD成功获取了锁!
线程锁threadC成功获取了锁!
线程锁threadB成功获取了锁!
线程锁threadA成功获取了锁!
线程锁threadD成功获取了锁!
线程锁threadC成功获取了锁!
线程锁threadB成功获取了锁!
线程锁threadA成功获取了锁!
线程锁threadD成功获取了锁!
线程锁threadC成功获取了锁!
线程锁threadB成功获取了锁!
线程锁threadA成功获取了锁!
.......
6、ReentrentLock的其他API方法
ReentrentLock 除了实现Lock接口外,还提供了对检测和监视可能很有用的方法,包括三个protected方法。
boolean hasQueuedThread(Thread thread):
查询给定线程是否正在等待获取此锁。注意,因为随时可能发生取消,所以返回 true 并不保证此线程将获取此锁。此方法主要用于监视系统状态。
boolean hasQueuedThreads( ):
查询是否有些线程正在等待获取此锁。注意,因为随时可能发生取消,所以返回 true 并不保证有其他线程将获取此锁。此方法主要用于监视系统状态。
boolean hasWaiters(Condition condition):
查询是否有些线程正在等待与此锁有关的给定条件。注意,因为随时可能发生超时和中断,所以返回 true 并不保证将来某个 signal 将唤醒线程。此方法主要用于监视系统状态。
boolean isLocked( ):
查询此锁是否由任意线程保持。此方法用于监视系统状态,不用于同步控制。
boolean isFair( ): 如果此锁的公平设置为 true,则返回 true。
boolean isHeldByCurrentThread( ): 查询当前线程是否保持此锁。
int getHoldCount( ): 查询当前线程保持此锁的次数。
int getQueueLength( ):
返回正等待获取此锁的线程估计数。该值仅是估计的数字,因为在此方法遍历内部数据结构的同时,线程的数目可能动态地变化。此方法用于监视系统状态,不用于同步控制。
int getWaitQueueLength(Condition condition):
返回等待与此锁相关的给定条件的线程估计数。注意,因为随时可能发生超时和中断,所以只能将估计值作为实际等待线程数的上边界。此方法用于监视系统状态,不用于同步控制。
三个protected方法,提供给用户在继承ReentrentLock时,拥有更多的监控方法:
protected Thread getOwner( ):
返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。当此方法被不是拥有者的线程调用,返回值反映当前锁状态的最大近似值。例如,拥有者可以暂时为 null,也就是说有些线程试图获取该锁,但还没有实现。此方法用于加快子类的构造速度,提供更多的锁监视设施。
protected Collection
返回一个 collection,它包含可能正等待获取此锁的线程。因为在构造此结果的同时实际的线程 set 可能动态地变化,所以返回的 collection 仅是尽力的估计值。所返回 collection 中的元素没有特定的顺序。此方法用于加快子类的构造速度,以提供更多的监视设施。
protected Collection
返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。因为在构造此结果的同时实际的线程 set 可能动态地变化,所以返回 collection 的元素只是尽力的估计值。所返回 collection 中的元素没有特定的顺序。此方法用于加快子类的构造速度,提供更多的条件监视设施。
重点介绍以下两个方法:
1、boolean isHeldByCurrentThread( ): 查询当前线程是否保持此锁。
与内置监视器锁的 Thread.holdsLock(java.lang.Object) 方法类似,此方法通常用于调试和测试。例如,只在保持某个锁时才应调用的方法可以声明如下:
class X {
ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
//在保持某个锁的条件下才进入,
assert lock.isHeldByCurrentThread();
// ... method body
}
}
还可以用此方法来确保某个重入锁是否以非重入方式使用的,例如:
class X {
ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
assert !lock.isHeldByCurrentThread();
lock.lock();
try {
// ... method body
} finally {
lock.unlock();
}
}
}
2、public int getHoldCount( ):查询当前线程保持此锁的次数。
对于与解除锁操作不匹配的每个锁操作,线程都会保持一个锁。
保持计数信息通常只用于测试和调试。例如,如果不应该使用已经保持的锁进入代码的某一部分,则可以声明如下:
class X {
ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
assert lock.getHoldCount() == 0;
lock.lock();
try {
// ... method body
} finally {
lock.unlock();
}
}
}