记录生活中的点点滴滴

0%

同步控制

同步控制是并发程序必不可少的重要手段。关键字 synchronized 就是一种最简单的控制方法,在这里,我们将看到更加丰富多彩的多线程控制方法。

重入锁

重入锁可以完全代替关键字 synchronized 。在 JDK 5.0的早期版本中,重入锁的性能远远优于关键字 synchronized ,但自从 JDK 6.0开始,JDK 在关键字 synchronized 上做了大量的优化,使得两者性能差距并不大。

基本使用

我们看下面的例子:猜猜最后的执行结果是多少?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//重入锁的基本使用
public class ReenterLock implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;

@Override
public void run() {
for (int j = 0; j < 100000; j++) {
i++;
}
}

public static void main(String[] args) throws InterruptedException {
ReenterLock lock = new ReenterLock();
Thread t1 = new Thread(lock);
Thread t2 = new Thread(lock);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}

最后结果基本差不多每次都是 120000 左右,因为i为共享变量,我们没有对其进行相关设置,所以两个线程并发时会出错,答案不会是我们所期望的 200000。

我们可以加关键字 synchronized 来解决,也可以通过重入锁的方式解决,只需要把 i++; 变成下面的代码:

1
2
3
4
5
6
lock.lock();
try {
i++;
} finally {
lock.unlock();
}

执行完毕后,运行结果就是我们所期望的 200000。

中断响应

对于关键字 synchronized 来说,如果一个线程正在等待,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。而使用重入锁,则提供了另外一种可能,那就是线程可以被中断。

我们举一个生活中的例子,小明和小红约定在操场见面,但是小红没到,小明就会一直等待。但是如果使用重入锁,小明就可以被告知无须等待,就可以回去了。

我们看下面这个例子:

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
public class ReentrantLockDemo extends Thread{
public static ReentrantLock l1 = new ReentrantLock();
public static ReentrantLock l2 = new ReentrantLock();
int lock;
public ReentrantLockDemo(int lock){
this.lock = lock;
}

@Override
public void run() {
try {
//如果lock为1,则先占用了l1的锁,再占l2的锁
if(lock==1){
l1.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+"已获得l1");
Thread.sleep(1000);
l2.lockInterruptibly();
}
//如果lock为2,则先占用了l2的锁,再占l1的锁
if(lock==2){
l2.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+"已获得l2");
Thread.sleep(1000);
l1.lockInterruptibly();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
if(l1.isHeldByCurrentThread())
l1.unlock();
if(l2.isHeldByCurrentThread())
l2.unlock();
System.out.println(Thread.currentThread().getName()+"线程退出");
}
}

public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo r1 = new ReentrantLockDemo(1);
ReentrantLockDemo r2 = new ReentrantLockDemo(2);
r1.start();
r2.start();
}
}

执行代码,直接死锁,卡在那里:

我们接下来在main方法添加两行代码:

1
2
Thread.sleep(2000);
r2.interrupt();

再次运行结果如下:

因为我们给了线程 r2的一个中断信号,所以r2会抛出 java.lang.InterruptedException 异常,然后释放锁。这样线程 r1 就可以获得两把锁,然后运行完毕。

锁申请等待限时

除了等待外部通知以外,要避免死锁还有另外一种方法,那就是限时等待。

我们看下面的例子:

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
public class ReentrantLockDemo2 implements Runnable{
public static void main(String[] args) {
ReentrantLockDemo2 runnable = new ReentrantLockDemo2();
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
}
public static ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
try {
if(lock.tryLock(5,TimeUnit.SECONDS)){
System.out.println(Thread.currentThread().getName()+"get successs");
Thread.sleep(2000);
}else {
System.out.println(Thread.currentThread().getName()+"get failed");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}

在这里,tryLock() 方法接受两个参数,一个表示时长,一个表示单位。这里设置为5秒,表示线程在这个锁请求中最多等待5秒,如果超过5秒还没得到锁。就会返回false。如果成功获得锁,则返回true。

当然,这个方法也可以不带参数,这种情况下,当前线程会尝试获得锁,如果成功立即返回true,失败立即返回false,不会等待,也不会死锁。

程序运行结果如下:

公平锁

大多数情况下,锁的申请是非公平的。

如下面程序:

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 ReentrantLockDemo3 implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"获得了锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}

public static void main(String[] args) {
ReentrantLockDemo3 runnable = new ReentrantLockDemo3();
Thread r1 = new Thread(runnable);
Thread r2 = new Thread(runnable);
r1.start();
r2.start();
}
}

执行结果:

线程1会一直获得锁,而线程0却一直没有获得锁。为什么?

因为根据系统的调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的,但是无公平性可言。

我们把 public static ReentrantLock lock = new ReentrantLock(); 括号内添加一个参数 true

再次执行,结果如下:

这样就表示锁是公平的。公平锁看起来很优美,但是要实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能却非常低下。因此,在默认情况下,锁是非公平的。

重入锁的好搭档:Condition

我们如果理解 Object.wait() 方法和 Object.notify() 方法,就很容易理解 Condition对象了。

它与 wait 方法和 notify 方法的作用是大致相同的。但是 wait 和 notify 方法是与 synchronized 关键字合作使用的,而 Condition 是与重入锁相关联的。通过lock接口的 Condition newCondition() 方法可以生成一个与当前重入锁绑定的Condition实例。利用 Condition对象,我们就可以让线程在合适的时间等待,或者在特定的时刻得到通知。

Condition的基本方法如下:

  • await() 方法会使当前线程等待,同时释放当前锁,当其他线程使用 signal() 方法或 signalAll() 方法时。线程会重新获得锁并继续执行。或者线程被中断时,也能跳出等待。这和 Object.wait() 方法相似。
  • awaitUninterruptibly() 方法与 await() 方法基本相同,但是它不会在等待过程中响应中断。
  • signal() 方法用于唤醒一个在等待中的线程,signalAll() 方法会唤醒所有在等待中的线程。这和 Object.notify() 方法很相似。

下面我们通过重入锁和 Condition来再完成生产者消费者问题:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//重入锁的好搭档:Condition的使用
public class ConditionDemo {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Customer customer = new Customer(clerk);
producer.start();
customer.start();
}
}

class Clerk{
//数量
private int count = 0;
//锁
public ReentrantLock lock = new ReentrantLock();
//Condition,完成等待与唤醒
public Condition condition = lock.newCondition();
//生产方法
public void product(){
lock.lock();
try {
if(count>=20){
try {
condition.await();//有20个就等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
condition.signal();
System.out.println("生产了第"+(++count)+"个");
}
} finally {
lock.unlock();
}
}
//消费方法
public void custome(){
lock.lock();
try {
if(count==0){
try {
condition.await();//没有就等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
condition.signal();
System.out.println("消费了第"+(count--)+"个");
}
} finally {
lock.unlock();
}
}
}
//生产者
class Producer extends Thread{
private Clerk clerk;
public Producer(Clerk clerk){
this.clerk = clerk;
}

@Override
public void run() {
while (true){
try {
Thread.sleep(500);
clerk.product();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者
class Customer extends Thread{
private Clerk clerk;
public Customer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
clerk.custome();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

下面写一下两种 wait 的区别:

  1. Object wait() 不能单独使用,必须是在 synchronized 下才能使用,
    Object wait() 必须要通过 notify() 方法进行唤醒

  2. Condition await() 必须是当前线程被排斥锁 lock 后,,获取到 Condition 后才能使用

    Condition await() 必须通过 signal() 方法进行唤醒

允许多个线程同时访问:信号量

信号量为多线程协作提供了更为强大的控制方法。从广义上说,信号量是对锁的扩展。无论是内部锁 synchronized 还是重入锁 ReentrantLock ,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。

Semaphore的模型可以概括为一个计数器、一个等待队列、三个方法。 三个方法原子性分别是init()、down()、up()

init():设置计数器的初始值。

down():将计数器的值减一,如果减了一之后,计数器的值小于0,则当前的线程被阻塞,否则继续执行。

up():将计数器的值加一,如果加了一之后,计数器的值小于等于0,则唤醒等待队列中的一个线程,并且将它移除出等待队列。(注意是小于等于0,不应该理解为大于等于0,因为大于等于0表明此时没有等待的线程,所以不会有唤醒这个操作。)

简单的理解就是 Semaphore 就是通过这三个方法来改变计数器,通过计数器的值来判断此时的线程是应该加入到等待队列中等待还是成功执行。

信号量模型也被称为PV原语,也就是downup操作最早称为P操作和V操作,有些人还称为semWaitsemSignal在JAVA中信号量模型是由 java.util.concurrent.Semaphore 的实现,并且downup对应的实现方法是acquire和release

方法 描述
Semaphore(int permits) 创建一个 Semaphore与给定数量的许可证和非公平公平设置
Semaphore(int permits, boolean fair) 创建一个 Semaphore与给定数量的许可证和给定的公平设置。
acquire() 从该信号量获取许可证,阻止直到可用
acquire(int permits) 从该信号量获取给定数量的许可证,阻止直到所有可用
tryAcquire() 尝试从该信号量获得1个许可,如果获取不到则返回false
tryAcquire(long timeout,TimeUnit unit) 规定时间内尝试从该信号量获得1个许可,如果获取不到则返回false
tryAcquire(int permits) 尝试从该信号量获得给定数量个许可,如果获取不到则返回false
release() 释放许可证
release(int permits) 释放一定数量的许可证

下面给出一个用信号量解决死锁问题的方法:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//用信号量去解决死锁问题
public class SemapDemo2 extends Thread {
//两个信号量,两个线程都尝试获取它们
public static Semaphore s1 = new Semaphore(1);
public static Semaphore s2 = new Semaphore(1);
//根据传入的参数决定执行的方法
public int p;
public SemapDemo2(int p){
this.p = p;
}
@Override
public void run() {
//若p为1
if(p==1){
try {
//p为1,则尝试先获取1信号量,再获取2信号量
if (s1.tryAcquire()) {
System.out.println(Thread.currentThread().getName()+"获取了s1信号量");
Thread.sleep(1000);
//尝试获取2信号量1s,这个时候p2在占用2信号量,所以这个线程会失败
if(s2.tryAcquire(1,TimeUnit.SECONDS)){
System.out.println(Thread.currentThread().getName()+"获取了s2信号量");
Thread.sleep(1000);
}else {
System.out.println(Thread.currentThread().getName()+"获取s2信号量失败,接下来会退出");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
s1.release();
s2.release();
}
}
//若p为2
if(p==2){
try {
//p为2,则尝试先获取2信号量,再获取1信号量
if (s2.tryAcquire()) {
System.out.println(Thread.currentThread().getName()+"获取了s2信号量");
Thread.sleep(1000);
//尝试获取1信号量2秒
if(s1.tryAcquire(2, TimeUnit.SECONDS)){
System.out.println(Thread.currentThread().getName()+"获取了s1信号量");
Thread.sleep(1000);
}else {
System.out.println(Thread.currentThread().getName()+"获取s1信号量失败,接下来会退出");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
s2.release();
s1.release();
}
}
}

public static void main(String[] args) {
SemapDemo2 t1 = new SemapDemo2(1);
SemapDemo2 t2 = new SemapDemo2(2);
t1.start();//这个线程获取信号量2会失败,接下来会释放1信号量
t2.start();//这个线程最终会获取两个信号量成功
}
}

ReadWriteLock读写锁

ReadWriteLock 是 JDK 5中提供的读写分离锁。读写分离锁可以有效帮助减少锁竞争。提升系统性能。

这个很好理解,比如有一个文件,线程1 2 3对其进行读操作,线程4 5 6对其进行写操作,如果用重入锁或内部锁,从理论上讲,所有读写线程之间都是串行操作。但是由于读线程之间是不会破坏数据的完整性的,所以这种读线程之间的等待是不合理的,所以就有了读写锁。

读写锁允许多个线程同时读,但是写写操作和读写操作之间依然是需要相互等待和持有锁的。

读写锁的访问约束情况:

非阻塞 阻塞
非阻塞 阻塞

如果在系统中,读操作的次数远远大于写操作的次数,则读写锁就可以发挥最大的功效,提升系统的性能。这里给出一个案例:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//读写锁
public class WriteReadLockDemo {
private static Lock lock = new ReentrantLock();//普通重入锁
//读写锁
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
private int value;//用一个int值模拟文件内容
//模拟读操作
public Object read(Lock lock) throws InterruptedException {
try {
lock.lock();
Thread.sleep(1000);//假设读操作需要1s时间
System.out.println(Thread.currentThread().getName()+"完成了读操作,读的内容是:"+value);
return value;
}finally {
lock.unlock();
}
}
//模拟写操作
public void write(Lock lock,int index) throws InterruptedException {
try {
lock.lock();
Thread.sleep(500);//假设写操作需要1s时间
value = index;
System.out.println(Thread.currentThread().getName()+"完成了写操作,写的内容是:"+value);
} finally {
lock.unlock();
}
}

//main方法
public static void main(String[] args) {
WriteReadLockDemo demo = new WriteReadLockDemo();
//读线程
Runnable readRunnable = new Runnable() {
@Override
public void run() {
try {
demo.read(readLock);
// demo.read(lock);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//写线程
Runnable writeRunnable = new Runnable() {
@Override
public void run() {
try {
demo.write(writeLock, new Random().nextInt(10));
// demo.write(lock, new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};

for (int i = 0; i < 2; i++) {
new Thread(writeRunnable).start();
}
for (int i = 2; i < 20; i++) {
new Thread(readRunnable).start();
}
}
}

这个程序让18个线程去执行读操作,2个线程去执行写操作。

如果用读写锁,程序大概4s左右就跑完了,但是如果把锁换成重入锁,那么程序得跑20多秒。

倒计数器:CountDownLatch

CountDownLatch 是一个非常实用的多线程控制工具类,这个工具类通常用来控制线程等待,它可以让某一个线程等待直到倒计数结束,再开始执行。

举个例子,火箭发射,只有我们把火箭的各项检查工作做完之后,火箭才能发射,那么用这个倒计数器就可以使发射线程等待所有检查线程完成之后再执行。

CountDownLatch 的构造函数接受一个整数作为参数,即当前这个计数器的计数个数:

1
public CountDownLatch(int count)

下面用一个火箭发射的例子来说明:

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
//倒计数器 CountDownLatch 的使用:例子(火箭发射)
public class CountDownLatchDemo implements Runnable{
//计数数量为10
static final CountDownLatch cdl = new CountDownLatch(10);
static final CountDownLatchDemo demo = new CountDownLatchDemo();

@Override
public void run() {
//模拟检查任务
try {
Thread.sleep(new Random().nextInt(10)*2000);
System.out.println("某个零件检查完成");
cdl.countDown();//计数器countDown
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
pool.submit(demo);
}
//等待检查
cdl.await();
//发射
System.out.println("火箭检查完毕,发射!");
pool.shutdown();
}
}

循环栅栏:CyclicBarrier

CyclicBarrier 是另外一种多线程并发控制工具。和 CountDownLatch 非常类似,它也可以实现线程间的计数等待,但它的功能比 CountDownLatch 更加复杂且强大。

CyclicBarrier 可以理解为循环栅栏。栅栏就是用来阻止线程继续执行,要求线程在栅栏外等待。循环说明这个计数器可以反复使用。比如,我们凑够10个线程之后,计数器就会归零,接着凑齐下一批10个线程,这就是循环栅栏内在的含义。

CyclicBarrierCountDownLatch 略微强大一些,它可以接受一个参数作为 barrierAction 。即当计数器一次计数完成后,系统会执行的动作。如下构造函数,parties 表示计数总数,也就是参与的线程总数。

1
public CyclicBarrier(int parties, Runnable barrierAction)

我们来写一个例子就明白了,现在有10个士兵,我们要集合这10个士兵,集合完毕后,他们再各自执行任务,全部执行完任务后,我们就可以对外宣布任务完成。代码如下:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//循环栅栏 CyclicBarrier 的使用
public class CyclicBarrierDemo {
//士兵线程
public static class Soldier implements Runnable{
private String name;
private CyclicBarrier cyclicBarrier;
public Soldier(String name,CyclicBarrier cyclicBarrier){
this.name = name;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
System.out.println(name+"报到");
//先等待所有士兵到齐
cyclicBarrier.await();
Thread.sleep(new Random().nextInt(10)*500);
System.out.println(name+"已完成任务");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
//barrierAction线程
public static class BarrierAction implements Runnable{
private int N;
private boolean flag = false;
public BarrierAction(int N){
this.N = N;
}
@Override
public void run() {
if(flag)
System.out.println(N+"个士兵任务已全部完成");
else {
//第一次集合完毕后,flag默认为false,将其改为true
System.out.println(N+"个士兵集合完毕,开始完成任务");
flag = true;
}

}
}

public static void main(String[] args) {
final int N = 10;//士兵数/线程数
//士兵线程
Thread[] soldierThread = new Thread[N];
CyclicBarrier cyclicBarrier = new CyclicBarrier(N, new BarrierAction(N));
//设置屏障
System.out.println("集合队伍");
for (int i = 0; i < N; i++) {
soldierThread[i] = new Thread(new Soldier("士兵"+i,cyclicBarrier));
soldierThread[i].start();
}
}
}

解释一下代码:我们先创建了 CyclicBarrier 实例,并将计数器设置为10,当计数器到达到指标后,执行BarrierActionrun 方法。每一个士兵都会执行各自的 run 方法,并会等待两次,全部完成后,会再一次执行 BarrierActionrun 方法,打印 10个士兵任务已全部完成

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
集合队伍
士兵1报到
士兵0报到
士兵5报到
士兵2报到
士兵6报到
士兵7报到
士兵8报到
士兵9报到
士兵4报到
士兵3报到
10个士兵集合完毕,开始完成任务
士兵3已完成任务
士兵1已完成任务
士兵7已完成任务
士兵4已完成任务
士兵5已完成任务
士兵9已完成任务
士兵2已完成任务
士兵8已完成任务
士兵0已完成任务
士兵6已完成任务
10个士兵任务已全部完成

线程挂起(suspend)和继续执行(resume)

为什么要谈这两个方法?虽然他们就像 线程的stop 方法一样,被标记为废弃方法,但是因为下面一个知识点和这个有关系,我们要先理解好 线程挂起(suspend)和继续执行(resume),明白为什么他们被不建议使用,再能继续学习下一个更厉害的。

所以这一节就谈谈 线程挂起(suspend)和继续执行(resume),为什么会废弃?

不推荐使用 suspend 方法去挂起线程是因为它在导致线程暂停的同时,并不会释放任何锁资源。此时,其他线程想要访问它占用的锁,都无法继续。知道这个线程进行了 resume 方法操作,被挂起的线程才能继续。但是,如果 resume 方法意外在 suspend 方法前就执行了,那么被挂起的线程可能很难有机会被继续执行。这时,它占有的锁还不会释放,别的线程如之奈何?还有,它的线程状态竟然也是 Runnable ,这也严重影响我们对当前线程状态的判断。

我们用代码举个例子:

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
//线程挂起(suspend)和继续执行(resume)
public class SuspendResumeDemo {
public static Object o = new Object();//锁
//两个线程
public static MyThread t1 = new MyThread("t1");
public static MyThread t2 = new MyThread("t2");
//自定义线程
public static class MyThread extends Thread{
private String name;
public MyThread(String name){
this.name = name;
}
@Override
public void run() {
synchronized (o){
System.out.println("Name: "+name);
Thread.currentThread().suspend();//挂起线程
}
}
}

public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(1000);
t2.start();
//让t1 t2继续执行
t1.resume();
t2.resume();
t1.join();
t2.join();
}
}

运行结果:

为什么会出现这种情况?

虽然主函数我们调用了 resume 方法,但是由于时间先后顺序的缘故,那个 resume 并没有生效,这就导致了线程 t2 被永远挂起,并且永远占用了对象 o 的锁。

线程阻塞工具类:LockSupport

LockSupport 是一个非常方便实用的线程阻塞工具,它可以在线程内任意位置让线程阻塞。

我们上面一节的 线程挂起(suspend)和继续执行(resume),存在线程继续执行不会生效的情况,用 LockSupport 就可以弥补这一缺陷,并且它还不会抛出 InterruptedException 异常。

上一节的程序我们用 LockSupport 来完成:

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
//线程阻塞工具类 LockSupport
public class LockSupportDemo {
public static Object o = new Object();//锁
//两个线程
public static MyThread t1 = new MyThread("t1");
public static MyThread t2 = new MyThread("t2");
//自定义线程
public static class MyThread extends Thread{
private String name;
public MyThread(String name){
this.name = name;
}
@Override
public void run() {
synchronized (o){
System.out.println("Name: "+name);
LockSupport.park();
}
}
}

public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(1000);
t2.start();
//让t1 t2继续执行
LockSupport.unpark(t1);
LockSupport.unpark(t2);
t1.join();
t2.join();
}
}

执行结果就不会出现线程 t2 被永远挂起的情况,结果如下:

为什么呢?它是这样保证的?

因为 LockSupport 类使用类似信号量的机制。它为每一个线程准备了一个许可,如果许可可用,那么 park 方法就会立即返回,并且消费这个许可(也就是说将许可变成不可用),如果许可不可用,就会阻塞,而 unpark 方法则使得一个许可变为可用(和信号量不同的是,许可不能累加,不能拥有超过一个许可)。

所以,即使 unpark 方法操作发生在 park 方法之前,它也可以使下一次 park 方法操作立即返回,这就是下面代码可以顺利结束的原因。

LockSupport.park() 方法还能支持中断影响,但是和其他接收中断的函数不一样,LockSupport.park() 不会抛出 InterruptedException 异常。它只会默默返回,但是我们可以从 Thread.interrupted() 等方法中获得中断标记。

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
//线程阻塞工具类 LockSupport 的中断
public class LockSupportIntDemo {
public static Object o = new Object();//锁
//两个线程
public static MyThread t1 = new MyThread("t1");
public static MyThread t2 = new MyThread("t2");
//自定义线程
public static class MyThread extends Thread{
private String name;
public MyThread(String name){
this.name = name;
}
@Override
public void run() {
synchronized (o){
System.out.println("Name: "+name);
LockSupport.park();
if(Thread.interrupted()){
System.out.println(name+"被中断了");
}
}
}
}

public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(1000);
t2.start();
//t1中断
t1.interrupt();

Thread.sleep(1000);
LockSupport.unpark(t2);
}
}

上面代码中断了处于 park() 状态的 t1。之后,t1可以马上响应这个中断并且返回。t1返回后在外面等待的 t2 才可以进入临界区,并最终由 LockSupport.unpark(t2); 操作使其结束运行。