什么是 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
2
3
4
5
6
7
8
public interface Lock {
void lock(); //加锁
void lockInterruptibly() throws InterruptedException; // 支持中断某线程获取锁的过程
boolean tryLock(); //有返回值,可以知道加锁成功或者失败
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //一直尝试加锁,直到放弃抛出异常
void unlock(); //解锁
Condition newCondition(); //钥匙,一把锁可以配一把或者多把钥匙
}

下面来逐个讲述 Lock 接口中每个方法的使用

1、lock

lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。

因此一般来说,使用 Lock 必须在try{} catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用 Lock 来进行同步的话,是以下面这种形式去使用的:

1
2
3
4
5
6
7
8
9
10
Lock lock = ...;
lock.lock();
try{
// 处理任务
}catch(Exception ex){

}finally{
// 释放锁
lock.unlock();
}

2、lockInterruptibly

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。

也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程 A 获取到了锁,而线程 B 只有在等待,那么对线程 B 调用threadB.interrupt()方法能够中断线程 B 的等待过程。(详情可以看ReentrantLock方案三)

由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在 try 块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException

因此 lockInterruptibly()一般的使用形式如下:

1
2
3
4
5
6
7
8
9
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}

注意:当一个线程获取了锁之后,是不会被 interrupt()方法中断的。因为本身在前面的文章中讲过单独调用 interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

因此当通过 lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。

而用 synchronized 修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

3、tryLock

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回 true,如果获取失败(即锁已被其他线程获取),则返回 false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit) 方法和 tryLock() 方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回 false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回 true。

所以,一般情况下通过 tryLock 来获取锁时是这样使用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
Lock lock = ...;
if(lock.tryLock()) {
try{
// 处理任务
}catch(Exception ex){

}finally{
// 释放锁
lock.unlock();
}
}else {
//如果不能获取锁,则直接做其他事情
}

4、newCondition

关键字 synchronized 与 wait() / notify() 这两个方法一起使用可以实现 等待/通知模式

Lock 锁的newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式。

notify() 通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知, Condition 比较常用的两个方法:

  • await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行。

  • signal()用于唤醒一个等待的线程。

注意:在调用 Conditionawait() / signal()方法前,也需要线程持有相关的 Lock 锁,调用await()后线程会释放这个锁,在singal()调用后会从当前Condition对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。

ReentrantLock

ReentrantLock,意思是可重入锁

ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更多的方法。

首先看一个错误案例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Insert {

public void add() {
// 局部变量,每次调用方法都会new一个新的
Lock lock = new ReentrantLock();
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "正在进行写操作");
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
}
}

测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {
Insert insert = new Insert();
new Thread(() -> {
insert.add();
}, "线程1").start();


new Thread(() -> {
insert.add();
}, "线程2").start();
}
}

得到了错误结果

问题:第二个线程怎么会在第一个线程释放锁之前得到了锁?

原因: 在 insert 方法中的 lock 变量是局部变量,每个线程执行该方法时都会保存一个副本,那么理所当然每个线程执行到 lock.lock()处获取的是不同的锁,所以就不会发生冲突。

解决方案:

方案一: lock 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Insert {
/**
* 设置为全局变量
*/
Lock lock = new ReentrantLock();

public void add() {

try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "正在进行写操作");
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
}
}

再次进行上面的测试,结果

方案二: tryLock 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Insert {
/**
* 设置为全局变量
*/
Lock lock = new ReentrantLock();

public void add() {

// tryLock()方法是有返回值的,它表示用来尝试获取锁,
// 如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false
if (lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + "正在进行写操作");
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
} else {
System.out.println(Thread.currentThread().getName() + "获取锁失败");
}
}
}

方案三: lockInterruptibly 实现

首先重写 run()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Run implements Runnable {

Lock lock = new ReentrantLock();

@Override
public void run() {
try {
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "正在进行写操作");
Thread.sleep(3000);
} catch (Exception e) {
System.out.println(Thread.currentThread().getName() + " interrupted");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
}
}

测试方法,线程 1 先运行,线程 2 运行再调用interrupt()方法会终止,抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) {

Thread t1 = new Thread(new Run(),"线程1");
Thread t2 = new Thread(new Run(),"线程2");

t1.start();

t2.start();
t2.interrupt();
}
}

Condition

Conditionjava.util.concurrent.locks 包中的一个接口

可以翻译成 条件对象,其作用是线程先等待,当外部满足某一条件时,在通过条件对象唤醒等待的线程。

Condition 可以看做是 Obejct 类的wait()notify()notifyAll()方法的替代品,与 Lock 配合使用。

从整体上来看Obejct 类的wait()notify()notifyAll()方法是与对象监视器配合完成线程间的等待/通知机制,而 Condition 是与 Lock 配合完成等待通知机制,前者是 java 底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同:

  1. Condition 能够支持不响应中断,而通过使用 Object 方式不支持;
  2. Condition 能够支持多个等待队列(new 多个 Condition 对象),而 Object 方式只能支持一个;
  3. Condition 能够支持超时时间的设置,而 Object 不支持
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 条件对象
*/
public interface Condition {

/**
* 当前线程进入等待状态直到被通知(signal)或中断,当前线程进入后台运行状态且从await()方法返回
* 其他线程调用该Condition的signal或者signalAll方法,而当前线程被选中唤醒
* 1、其他线程(interrupt)中断当前线程
* 2、如果当前等待线程从await方法返回,那么表明当前线程已经获取了Condition对象的锁
*/
void await() throws InterruptedException;

/**
* 当前线程进入等待状态直到被通知,对中断不响应
*/
void awaitUninterruptibly();

/**
* 当前线程进入等待状态直到被通知、中断或超时。返回值表示剩余时间,
* 如果在nanosTimeout纳秒之前被唤醒,那么返回值就是nanosTimeout-实际耗时
* 返回值<=0说明超时
*/
long awaitNanos(long nanosTimeout) throws InterruptedException;

/**
* 当前线程进入等待状态直到被通知、中断或超时,如果没有到指定时间被通知返回true,否则返回false
*/
boolean await(long time, TimeUnit unit) throws InterruptedException;

/**
* 带超时的await(指定截止时间)
*/
boolean awaitUntil(Date deadline) throws InterruptedException;

/**
* 唤醒一个等待在Condition上的线程,该线程从等待方法返回之前必须获得与Condition相关联的锁
*/
void signal();

/**
* 唤醒所有线程
*/
void signalAll();
}