在面试和实际业务需求中,会有一些情况,需要在A线程执行结束后再执行B线程,或者线程按照一定规则有序执行。
举个例子,如何让两个线程交替输出1 2 3 4...10,也就是A线程输出13579,B线程输出246810。
首先可以想到利用锁的机制,对同一个资源进行加锁,在适当的情况释放锁,通过Object对象的notifyAll/notify和wait方法实现。官方对这些方法的说明如下:
void notifyAll()
Wakes up all threads that are waiting on this object’s monitor.
译:唤醒在此对象监视器上等待的所有线程
void wait( )
Causes the current thread to wait until another thread invokes the notify() method or the notifyAll( ) method for this object.
译:导致当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法
这些方法都是Object的而非Thread的,更多带有参数的说明可以查看具体方法说明,由上可知:
wait( ),notify( ),notifyAll( )都不属于Thread类,而是属于Object基础类,也就是每个对象都有wait( ),notify( ),notifyAll( ) 的功能,因为每个对象都有锁,锁是每个对象的基础,当然操作锁的方法也是最基础了。
很容易的就可以想到可以利用此机制实现交替输出,首先维护一个共享的参数i和一个锁对象,线程A判断参数i,如果是奇数就输出,并且加一,然后释放锁,线程B判断偶数,如果是偶数就输出,并且加一,然后释放锁。简单的代码如下:
1package com.allen.design.thread;
2
3public class ThreadOrder {
4 public static Object object = new Object();
5 public static Integer i = 1;
6
7 public static void main(String[] args) {
8 new Thread(new ThreadA(),"线程A").start();
9 new Thread(new ThreadB(),"线程B").start();
10 }
11}
12
13class ThreadA implements Runnable {
14
15 @Override
16 public void run() {
17 synchronized (ThreadOrder.object) {
18 while (true) {
19 if(ThreadOrder.i>=10){
20 break;
21 }
22 if (ThreadOrder.i % 2 == 0) {
23 try {
24 ThreadOrder.object.wait();
25 } catch (InterruptedException e) {
26 e.printStackTrace();
27 }
28 }
29 System.out.println(Thread.currentThread().getName()+":"+ThreadOrder.i);
30 ThreadOrder.i++;
31 ThreadOrder.object.notifyAll();
32 }
33 }
34 }
35}
36
37class ThreadB implements Runnable {
38 @Override
39 public void run() {
40 synchronized (ThreadOrder.object) {
41 while (true) {
42 if(ThreadOrder.i>=10){
43 break;
44 }
45 if (ThreadOrder.i % 2 == 1) {
46 try {
47 ThreadOrder.object.wait();
48 } catch (InterruptedException e) {
49 e.printStackTrace();
50 }
51 }
52 System.out.println(Thread.currentThread().getName()+":"+ThreadOrder.i);
53 ThreadOrder.i++;
54 ThreadOrder.object.notifyAll();
55 }
56 }
57
58 }
59}
上述方式同样适用于生产者消费者问题的解决,输出结果如下:

但是这个概念是较为难以理解的,例如线程A在何时被唤醒,为什么会被唤醒,什么时候又会释放锁,释放后为什么线程B就可以得到。
JUC(JAVA并发包)给我们提供另外一种锁的实现方式,相对容易理解一些,JUC包中顶层有Lock接口,结构如下

Lock接口有如下方法:

ReentrantLock(可重入锁)主要依赖Sync实现了Lock接口,锁的性能大为提升,其关键在于基础工具Sync,而Sync继承了AbstractQueuedSynchronizer(AQS),AQS是JUC包实现同步的基础工具。AQS中定义了一个volatile int state变量作为共享资源,如果线程获取资源失败,则进入同步FIFO队列中等待;如果成功获取资源就执行临界区代码。执行完释放资源时,会通知同步队列中的等待线程来获取资源后出队并执行。
使用可重入锁实现AB线程按顺序输出的代码如下:
1package com.allen.design.thread;
2
3import java.util.concurrent.TimeUnit;
4import java.util.concurrent.locks.Lock;
5import java.util.concurrent.locks.ReentrantLock;
6
7public class ThreadOrderCyclic {
8 public static Integer i = 1;
9 public static void main(String[] args) {
10 ReentrantLock lock = new ReentrantLock();
11 new Thread(new ThreadC(lock), "线程A").start();
12 new Thread(new ThreadD(lock), "线程B").start();
13
14 }
15
16}
17
18class ThreadC implements Runnable{
19 private Lock lock;
20 ThreadC(Lock lock){
21 this.lock = lock;
22 }
23 @Override
24 public void run() {
25 while (true){
26 if(ThreadOrderCyclic.i>=10){
27 break;
28 }
29 try {
30 lock.tryLock(1, TimeUnit.SECONDS);
31 } catch (InterruptedException e) {
32 e.printStackTrace();
33 }
34
35 if(ThreadOrderCyclic.i%2==1){
36 System.out.println(Thread.currentThread().getName() + ":" + ThreadOrderCyclic.i);
37 ThreadOrderCyclic.i++;
38 }
39 lock.unlock();
40 }
41 }
42}
43class ThreadD implements Runnable{
44 private Lock lock;
45 ThreadD(Lock lock){
46 this.lock = lock;
47 }
48 @Override
49 public void run() {
50 while (true){
51 if(ThreadOrderCyclic.i>=10){
52 break;
53 }
54 try {
55 lock.tryLock(1, TimeUnit.SECONDS);
56 } catch (InterruptedException e) {
57 e.printStackTrace();
58 }
59 if(ThreadOrderCyclic.i%2==0){
60 System.out.println(Thread.currentThread().getName() + ":" + ThreadOrderCyclic.i);
61 ThreadOrderCyclic.i++;
62 }
63 lock.unlock();
64 }
65 }
66}
输出与使用synchronized锁方式相同,但是可重入锁的阅读性上高了很多,并且提高了锁的性能。
并发包中还有更多锁与线程的工具类,例如时间维度信号量CountDownLathc,信号维度信号量Semaphore,可循环使用的信号量CyclicBarrier,以及线程池,线程本地变量等,可以在确保性能和安全的前提下,大大降低并发编程的难度,后面将会逐一介绍。




