并发编程学习笔记(7)----线程之间的通信
(一)线程之间的通信
前面所有的线程都是独立运行的,几个线程之间只会存在竞争锁和资源的管理,但是在多线程的环境下可能会需要多个线程同时协作完成,当某个线程执行一定操作之后,需要其他线程的帮助才能执行完成,此时该线程该如何去通知其他线程执行任务,当其他线程执行时该线程又处于什么状态,当其他线程执行完成后,又该如何使当前线程执行。这就是我们今天需要讨论的问题。。
1.1 等待(wait)/通知(notify)
为了实现多线程之间的协作jdk提供了两个非常重要的接口,wait()和notify(),这两个方法不是Thread类的,而是属于Object类的,这表示每个对象都可以调用这两个方法去使线程等待或唤醒其他线程。两个方法如下:
public final native void notify();
public final native void wait(long timeout) throws InterruptedException;
当在一个对象实例上调用了wait方法之后,当前的线程就会等待在这个对象上,停止执行,直到有其他线程调用了该实力的notify()方法之后才会继续执行,此时的对象就成为了两个线程之间通信的有效手段。当一个线程调用某个实例的wait()方法后,该线程就会进入到一个等待队列中,此时的队列中或许有多个线程在等待,此时如果有其他线程调用当前实例的notify()方法,则会从等待队列中随机唤醒一个线程执行,这是一个不公平的选择。
但是当调用了notify方法之后,只能唤醒一个线程,所以Object中还有另一个方法notifyAll,该方法可以唤醒所有正在等待队列中的方法,而不是去随机选择一个,同时需要注意的是,并不是在任何地方都可以随便的调用wait和notify方法,这两个方法的必须在synchronized修饰的代码块中,因为要让它们生效,首先它们需要获取目标对象的监视器,不加synchronized会报IllegalMonitorStateException异常。它们的使用在生产者和消费者模式下会更容易理解,所以这里用了一段生产者和消费者的示例代码。
package com.wangx.thread.t7;
public class Demo {
private int num;
public final static int MAX_NUM = 10;
public synchronized void push() {
while (num >= MAX_NUM ) {
System.out.println(Thread.currentThread().getName() + "仓库已满,生产者进入等待...");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.println("生产者正在生产,当前生产数量" + num);
notify();
}
public synchronized void take() {
while (num <= 0 ) {
System.out.println(Thread.currentThread().getName() + "存货为0,消费者进入等待...");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
System.out.println("消费者正在消费,当前剩余数量为" + num);
notify();
}
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo.push();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo.push();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo.push();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo.take();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
此时的由多个线程在生产,那么必然消费速度是没有生产速度快的,所以当生产个数大于10时,进来的线程都会进入等待状态,必须等到消费者去唤醒才能看条件是否能够生产,控制台部分打印如下:
Thread-2仓库已满,生产者进入等待...
Thread-0仓库已满,生产者进入等待...
Thread-1仓库已满,生产者进入等待...
消费者正在消费,当前剩余数量为9
生产者正在生产,当前生产数量10
这就是jdk中的wait/notify的使用方式。
1.2 Condition接口实现等待与唤醒
1.2.1 Condition的使用方式和解决的问题
在说Condition之前,我们先来看这样一段代码:
package com.wangx.thread.t7;
public class Demo2 {
private int signal;
public synchronized void a () {
while (signal != 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
signal++;
System.out.println("a");
notifyAll();
}
public synchronized void b () {
while (signal != 1) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
signal++;
System.out.println("b");
notifyAll();
}
public synchronized void c () {
while (signal != 2) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("c");
signal = 0;
notifyAll();
}
public static void main(String[] args) {
Demo2 demo2 = new Demo2();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo2.a();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo2.b();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo2.c();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
这段代码实现的功能是想三个输入按a-b-c这样的顺序输出的,所以我们需要加条件来控制等待,然后使用notifyAll()来唤醒线程,但是这样做的问题就是notify唤醒线程是随机的,所以可能会唤醒其他的线程来执行,这样会多出等待时间,如果单纯的使用notify还可能会造成线程阻塞的问题等,所以我们期望的就是能够唤醒我们想要指定的线程。如a方法执行时想唤醒b,b想唤醒c,c想唤醒a,这样指定的去唤醒就可以给我们节省很多资源和避免了很多的问题,因此jdk中Condition接口就为我们提供了这个问题的解决方案,使用方式为改良我们如上的代码:
package com.wangx.thread.t7;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo2 {
private int signal;
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
public synchronized void a () {
lock.lock();
while (signal != 0) {
try {
conditionA.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
signal++;
System.out.println("a");
conditionB.signal();
lock.unlock();
}
public synchronized void b () {
lock.lock();
while (signal != 1) {
try {
conditionB.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
signal++;
System.out.println("b");
conditionC.signal();
lock.unlock();
}
public synchronized void c () {
lock.lock();
while (signal != 2) {
try {
conditionC.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
signal = 0;
System.out.println("c");
conditionA.signal();
lock.unlock();
}
public static void main(String[] args) {
Demo2 demo2 = new Demo2();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo2.a();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo2.b();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
demo2.c();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
先实例化一把锁,通过锁获取三个Condition实例,使用await()方法使线程等待,signal方法分别唤醒不同的线程,简单的解决了问题,通过Condition即可简单的解决不能唤醒指定线程的问题,可以在开发中解决很大的问题。
1.2.2 Condition 源码分析
Condition属于AQS的实现的一种,观察源码可以看出Condition的实现类ConditionObject正是实现了Condtion的几种方法。
首先看await()方法:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
首先,如果当前线程中断,直接抛出异常,否则,将线程加入到等待节点。
addConditionWaiter()方法将一个线程添加到等待队列中。
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
以上代码的实现方式就是将当前线程创建节点加入到等待队列队尾,并将当前线程节点的状态设置为CONDITION状态。返回当前节点。链表的操作前面几节已经详细叙述过了,这里就不再详述了。
使用fullyRelease释放当前线程的独占锁,不管冲入几次,都要将状态释放为0,然后判断当前线程是否在同步队列中,没有则表示未被signal,则将当前线程置为等待状态直到加入到同步队列中即。if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) 判断是在被signal前中断还是在被signal后中断,如果是被signal前就被中断则抛出 InterruptedException,否则执行 Thread.currentThread().interrupt();被中断后直接退出自旋,
当线程退出自旋之后表示当前线程一定在同步队列中了,但是却不一定在同步队列队首,acquireQueued将会一直阻塞着知道当前线程位于队首,即表示当前线程获取到了锁,wait()方法退出,然线程继续执行await()之后的代码。
signal()方法:
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
判断是不是独享锁时,直接抛出异常,可以看出Condition只会配合独占锁使用。
当前队列不为空时,做唤醒操作doSignal():
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
该方法主要是由transferForSignal()尝试去唤醒当前节点,如果当前节点唤醒失败,则继续唤醒当前节点的后续节点。
transferForSignal():
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
修改当前节点状态为CONDITION,enq然后将节点加入到同步同步队列,返回的是当前节点的前驱,当前节点线程状态是等待状态,或cas修改状态为SIGNAL成功,则唤醒成功,否则表示表示该线程被中断或超时,进入结束状态,此时调用LockSupport.unpark()将当前线程设置为不可用。
总的来说Condition的实现原理就是等待队列和同步队列的交互,从等待队列到同步队列或从同步队列到等待队列之间的交互。
1.3 join
join是属于Thread的一个实例方法,它可以将两个并行执行的程序按某种顺序执行,即如果在A线程种调用B线程的join方法,此时执行是当B线程执行完毕之后,A线程才能继续执行,join方法还可以传入参数表示当前线程需要等待的时间,时间过后,被阻塞的线程也可以继续执行。