在日常开发中,多线程之间通常会进行相互协作完成任务。为了支持线程间通信,JDK提供了很多很有趣的方法,其中很普遍的就是等待/通知机制。JDK中使用等待wait()方法和通知notify()/notifyAll()实现,这两个方法不是线程内部所有,而是属于Object。
等待/通知机制—第一种方式:
public final void wait() throws InterruptedException
public final native void notify();
实现:wait()方法,顾名思义,就是等待的意思,作用使当前线程在该对象上进行等待,wait()方法时Object类的方法,该方法是当前线程进入该对象的等待队列,并且停止运行,直到其它线程通知或被中断。wait()方法调用之前,线程必须持有该对象的锁,也就是说,只能与sychronized进行配合使用,如果未获取对象级别的锁而调用wait()方法,会抛出IllegalMonitorStateException异常。有等待则必然有通知,与等待wait()方法所对应的,就是notify()方法。当notify()方法被调用时,就会从该对象的等待队列中,随机选择一个线程唤醒,需要注意一点,notify方法在执行之后,并不会立即释放当前对象锁,而是等到sychronized中的语句执行完毕,才会释放对象锁,wait()会立即释放当前锁。除了notify()方法外,还有一个notifyAll()方法也可以唤醒线程,与之不同的是notify()方法只能唤醒一个线程,notifyAll()方法能够唤醒所有的线程。
我们可以用一句话来概括,wait()方法使线程停止运行,notify()使停止的线程继续运行。
为了使大家能够更好的理解,下面给出一段简单的代码:
public static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();
threadA.start();
Thread.sleep(100);
threadB.start();
}
private static class ThreadA extends Thread {
@Override
public void run() {
synchronized (lock) {
System.out.println("ThreadA wait 开始执行:" + System.currentTimeMillis());
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadA wait 结束执行:" + System.currentTimeMillis());
}
}
}
private static class ThreadB extends Thread {
@Override
public void run() {
synchronized (lock) {
System.out.println("ThreadB notify 开始执行:" + System.currentTimeMillis());
lock.notify();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadB notify 结束执行:" + System.currentTimeMillis());
}
}
}
执行结果:
上述代码,我们开启了两个线程,线程A和线程B。线程A,先是获取lock对象的对象锁,然后我们调用了wait()方法,线程A立即停止运行,并且释放了lock的对象锁。线程B,当线程A释放掉lock对象锁之后,线程B获取到锁,然后执行notify()方法,大家从结果可以看到,执行完notify()方法后,线程A并没有立即获取锁开始运行,这是因为,线程B还没有执行完成,2秒之后,线程B执行结束释放lock锁,然后线程A才继续运行。
关于wait()和sleep()方法,这两个方法都可以让线程进行等待,有一点大家注意,那就是wait()方法会立即释放目标对象的对象锁,而sleep()对象不会释放任何资源。
另外,不知道大家有没有发现,在main()方法中,我们加了一句Thread.sleep(100),这一句完全跟业务逻辑不沾边,为什么要加它呢?大家不妨去掉这一句,多执行几次,大家是不是发现,有些时候,线程B先于线程A执行,导致线程A会一直等待。这就是使用wait()和notify()的一个弊端,当notify()先于wait()方法执行,会导致线程永久等待。
在多线程中,大家可能会遇到,也是面试经常遇到的一个问题。线程之间,怎么确保一个线程先于另外一个线程执行完成?
join()小知识点:
在java中,线程提供了一个非常好用的方法join(),使用join()就可以保证线程间的有序执行。
join()部分源码:
关于join()方法,大家只需要记住它可以实现线程间有序执行并且也是依靠等待/通知机制就可以了。至于具体如何实现的,大家可以根据源码描述,就不做过多的介绍了。
上面,我们讲到了使用sychronized和wait()、notify()/notifyAll()方法实现等待/通知机制,那么在灵活的Lock里有没有更加好用的方式呢?
等待/通知机制—第二种方式:
答案是肯定的,Lock类也提供相应的机制,但是需要借助Condition类。Condition类是JDK1.5中才出现的,相对于sychronized的方式,使用它我们可以进行多路通知,也就是选择性通知,从而是线程调度变得更加的灵活。下面我们简单介绍一下Condition的相关方法:
void await() throws InterruptedException;
void signal();
await()相当于之前Object.wait()方法,都是使线程停止运行并且释放锁资源,在使用await()之前必须获得锁。若未加锁使用,则会抛出IllegalMonitorStateException。
signal()方法相当于Object.notify()方法,进行线程通知,使用前也必须获取锁。
我们对sychronized方式的代码进行修改:
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private static class ThreadA extends Thread {
@Override
public void run() {
lock.lock();
System.out.println("ThreadA await 开始执行:" + System.currentTimeMillis());
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println("ThreadA await 结束执行:" + System.currentTimeMillis());
}
}
private static class ThreadB extends Thread {
@Override
public void run() {
lock.lock();
System.out.println("ThreadB signal 开始执行:" + System.currentTimeMillis());
condition.signal();
lock.unlock();
System.out.println("ThreadB signal 结束执行:" + System.currentTimeMillis());
}
}
执行结果:
大家可以发现,使用Condition我们也能实现等待/通知机制,对于它的选择性通知是怎么理解法呢?请看:
private static Lock lock = new ReentrantLock();
private static Condition conditionA = lock.newCondition();
private static Condition conditionB = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
ConditionBDemo conditionBDemo = new ConditionBDemo();
Thread threadA = new Thread(() -> {
conditionBDemo.awaitA();
}
);
Thread threadB = new Thread(() -> {
conditionBDemo.awaitB();
});
threadA.start();
threadB.start();
Thread.sleep(2000);
conditionBDemo.signalAll();
}
public void awaitA() {
lock.lock();
System.out.println("awaitA 开始执行:" + System.currentTimeMillis());
try {
conditionA.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println("awaitA 结束执行:" + System.currentTimeMillis());
}
public void awaitB() {
lock.lock();
System.out.println("awaitB 开始执行:" + System.currentTimeMillis());
try {
conditionB.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println("awaitB 结束执行:" + System.currentTimeMillis());
}
public void signalAll() {
lock.lock();
System.out.println(" signalA 开始执行:" + System.currentTimeMillis());
try {
conditionA.signal();
} finally {
lock.unlock();
}
}
执行结果:
通过程序运行,大家发现,Condition对象一次可以创建多个,这样我们就可以唤醒指定部分的线程,有利于我们控制部分线程行为,这就是选择性唤醒逻辑。
sychronized方式,被通知的线程是由JVM随机选择的,如果我们需要进行选择性的唤醒线程,大家不妨试试Condition吧!
关于等待/通知机制,大家应该有所了解了,下面我们介绍线程通信的另外一种方式—线程控制工具。
控制工具类—CountDownLatch:
CountDownLatch是一个非常好用的多线程控制工具类。它的作用就是用来控制线程等待,让一个线程等待计时结束才开始运行,就好比一个倒计数器。
CountDownLatch的构造函数接受一个整数,即计数个数。
public CountDownLatch(int count)
比如,不知大家看过七龙珠没(龙珠粉,嘿嘿),其中有一个宝物,那就是七颗龙珠,当集齐七颗龙珠的时候(缺一不可),就可以召唤神龙,许三个愿望。我至少有三个愿望,所以我小小的满足了自己:
上面第13行,我们定义了一个CounDownLatch实例,指定计数7,表示当七个线程完成任务,在CountDownLatch上等待的线程才能继续执行。代码第32行,我们使用了countDown()方法,这个方法是用来通知计数器,有一个线程已经完成了任务,应当减一了。代码第22行,我们使用await()方法,这个方法会使当前线程进行等待,当计数完成时,才能继续执行。下面是我们的执行结果:
在日常开发中,当一个任务需等待其它任务执行完成时,才能继续执行的时候,CountDownLatch值得你拥有。
控制工具类—CyclicBarrier
CyclicBarrier是另外一种多线程控制工具类。它也是实现线程间计数等待,但相较于CountDownLatch,它更加的复杂和强大。CyclicBarrier意为循环栅栏,栅栏就相当于障碍物,阻止线程继续执行,使线程在栅栏外等待,循环意味着计数器可以多次使 用,当第一次计数完毕后,还可以进行第二次重新计数。
CyclicBarrier构造函数接受两个参数,parties代表计数总数,当计数完成就会执行barrierAction线程。
public CyclicBarrier(int parties, Runnable barrierAction)
比如:大家应该都坐过汽车,在一次坐车事件中,涉及到两个动作,第一次,载满客开车,第二次,到达目的地,乘客全部下车。
上述21行,我们定义了一个CyclicBarrier类,设定三个乘客。在32行的时候,我们使用了await()方法,让所有线程进行等待,当乘客全部到齐之后,再执行后续的方法。下面是我们的输出结果:
大家可以看到,执行完成输出了两次,第一次乘客全部上车之后,完成一次计数,会输出执行完成。第二次乘客全部下车的时候,完成一次计数,又会输出一次执行完成。当我们在具体的业务中,可以根据逻辑,在计数完成后添加不同的处理方法。
CyclicBarrier相交于CountDownLatch,计数可以多次使用,业务范围更加广大。CyclicBarrier的await()方法可能会抛出两个异常,其一就是InterruptedException,其二就是BrokenBarrierException(这是特有的),一旦遇到这个异常,表明CyclicBarrier已经损坏了,线程就会停止运行。