线程间通信
线程的通信可以被定义为:
线程通信就是当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺。
为什么需要线程通信
线程是操作系统调度的最小单位,有自己的栈空间,可以按照既定的代码逐步的执行,但是如果每个线程间都孤立的运行,那就会造资源浪费。所以在现实中,我们需要这些线程间可以按照指定的规则共同完成一件任务,所以这些线程之间就需要互相协调,这个过程被称为线程的通信。
线程通信的方式
线程通信主要可以分为三种方式,分别为共享内存、消息传递和管道流。每种方式有不同的方法来实现
- 共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信。
volatile共享内存
- 消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信。
wait
/notify
等待通知方式
join
方式
- 管道流
管道输入/输出流的形式
共享内存
共享内存这种方式比较常见,我们经常会设置一个共享变量。然后多个线程去操作同一个共享变量。从而达到线程通讯的目的。
例如
:我们使用多个线程去执行页面抓取任务,我们可以使用一个共享变量count来记录任务完成的数量。每当一个线程完成抓取任务,会在原来的count上执行加1操作。这样每个线程都可以通过获取这个count变量来获得当前任务的完成情况。当然必须要考虑的是共享变量的同步问题
,这也共享内存容易出错的原因所在。
如果线程A要和线程B通信,则需要经过以下步骤:
①线程A把本地内存A更新过的共享变量刷新到主内存中
②线程B到内存中去读取线程A之前已更新过的共享变量。
这保证了线程间的通信必须经过主内存。下面引出我们要学习的关键字volatile
volatile
有一个关键的特性:保证内存可见性,即多个线程访问内存中的同一个被volatile
关键字修饰的变量时,当某一个线程修改完该变量后,需要先将这个最新修改的值写回到主内存,从而保证下一个读取该变量的线程取得的就是主内存中该数据的最新值,这样就保证线程之间的透明性,便于线程通信。
代码实现:
A、B线程分别对值进行运算,加到50结束
public class VolatileTest {
private static volatile int flag = 0;
public static void main(String[] args) {
// 线程A
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
if(flag <= 50) {
synchronized (VolatileTest.class) {
System.out.println("线程A" + flag);
flag ++;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else break;
}
}
}).start();;
// 线程B
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
if(flag <= 50) {
synchronized (VolatileTest.class) {
System.out.println("线程B" + flag);
flag ++;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else break;
}
}
}).start();;
}
}
测试结果:
这种通讯模型中,不同的线程之间是没有直接联系的。都是通过共享变量这个中间人来进行交互。而这个中间人必要情况下还需被保护在临界区内(加锁或同步)。由此可见,一旦共享变量变得多起来,并且涉及到多种不同线程对象的交互,这种管理会变得非常复杂,极容易出现死锁等问题。
消息传递
消息传递方式采取的是线程之间的直接通信,不同的线程之间通过显式的发送消息来达到交互目的。
wait/notify等待通知方式
从字面上理解,等待通知机制就是将处于等待状态的线程将由其它线程发出通知后重新获取CPU资源,继续执行之前没有执行完的任务。最典型的例子生产者--消费者模式
有一个产品队列,生产者想要在队列中添加产品,消费者需要从队列中取出产品,如果队列为空,消费者应该等待生产者添加产品后才进行消费,队列为满时,生产者需要等待消费者消费一部分产品后才能继续生产。队列可以认为是java模型里的临界资源,生产者和消费者认为是不同的线程,它们需要交替的占用临界资源来进行各自方法的执行,所以就需要线程间通信。
生产者--消费者模型
主要为了方便复用和解耦,java语言实现线程之间的通信协作的方式是等待/通知机制
等待/通知机制提供了三个方法用于线程间的通信:
- wait()方法:中断方法的执行,使线程等待
- notify()方法:唤醒处于等待的某一个线程,使其他结束等待
- notifyAll()方法:唤醒所有处于等待的线程,使它们结束等待
线程间的通信需要对象Object来完成,对象中的wait()、notify()、notifyAll()方法就如同开关信号,用来完成等待方和通知方的交互。
生产者与消费者
容器类:
public class Queue {
private int n;
/*
* 用于判断容器中是否有值
* 若有值,则消费,生产者等待,反之则生产,消费者等待,
*/
boolean flag = false;
public synchronized int get() {
// 无值,则消费者等待
if(!flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费:" + n);
flag = false; // 容器里没有数据
notifyAll();
return n;
}
public synchronized void set(int n) {
// 有值,则生产者等待
if(flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产:" + n);
this.n = n;
flag = true; // 容器里有数据
notifyAll();
}
}
生产者线程类:
public class Producer implements Runnable{
Queue queue;
public Producer(Queue queue) {
this.queue = queue;
}
@Override
public void run() {
int i = 0;
while(true) {
queue.set(i++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
消费者线程类:
public class Consumer implements Runnable {
Queue queue;
public Consumer(Queue queue) {
this.queue = queue;
}
@Override
public void run() {
while(true) {
queue.get();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
测试类:
public static void main(String[] args) {
Queue queue = new Queue();
new Thread(new Producer(queue)).start();
new Thread(new Consumer(queue)).start();
}
使用wait()、notify()和notifyAll()需要注意以下细节
:
- 使用wait()、notify()和notifyAll()需要先调用对象加锁
- 调用wait()方法后,线程状态由Running变成Waiting,并将当前线程放置到对象的等待队列
- notify()和notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()和notifyAll()的线程释放锁之后等待线程才有机会从wait()返回
- notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部转移到同步队列,被移到的线程状态由Waiting变为Blocked。
- 从wait()方法返回的前提是获得调用对象的锁
其实等待通知机制有有一个经典的范式,该范式可以分为两部分,分别是等待方(消费者)和通知方(生产者)
- 等待方
synchronized(对象){
while(条件不满足){
对象.wait()
}
对应的处理逻辑
}
- 通知方
synchronized(对象){
改变条件
对象.notifyAll
}
join方式
在很多应用场景中存在这样一种情况,主线程创建并启动子线程后,如果子线程要进行很耗时的计算,那么主线程将比子线程先结束,但是主线程需要子线程的计算的结果来进行自己下一步的计算,这时主线程就需要等待子线程,java中提供可join()
方法解决这个问题。
join()
方法的作用是:在当前线程A调用线程B的join()
方法后,会让当前线程A阻塞,直到线程B的逻辑执行完成,A线程才会解除阻塞,然后继续执行自己的业务逻辑,这样做可以节省计算机中资源。
举例:
现在一家公司进行年终销售额清算,总公司在进行数据的汇总,其他分公司分别上报分公司的销售额,如果其中一家或者多家公司在总公司汇总的时候数据没有得到,那么汇总数据就会与实际不匹配。
public class JoinTest2 {
static int sum = 0; // 汇总金额
static int phone = 0; // 手机分公司金额
static int car = 0; // 汽车分公司金额
static int kongtiao = 0; // 空调分公司金额
public static void main(String[] args) {
// 空调分公司
Thread kt = new Thread(new Runnable() {
@Override
public void run() {
kongtiao = 2000;
System.out.println("空调分公司销售额为:" + kongtiao + "万");
}});
// 手机分公司
Thread ph = new Thread(new Runnable() {
@Override
public void run() {
phone = 7000;
System.out.println("手机分公司销售额为:" + phone + "万");
}});
// 汽车分公司
Thread c = new Thread(new Runnable() {
@Override
public void run() {
car = 800;
System.out.println("汽车分公司销售额为:" + car + "万");
}});
kt.start();
ph.start();
c.start();
sum = kongtiao + phone + car;
System.out.println("今年销售总额是:" + sum + "万");
}
}
总公司数据都已经出来了,其他公司才把销售额数据做出来,这岂不是乱套了。
这时就需要使用join()方法(当然,也可以使用强大的并发工具包,那都是后话
)
join()方法定义在Thread类中,JDK中给出的定义就是等待该线程终止,那么可以在子线程中使用join()方法,实现主线程等待子线程执行完后在执行主线程的效果。
public class JoinTest2 {
static int sum = 0;
static int phone = 0;
static int car = 0;
static int kongtiao = 0;
public static void main(String[] args) {
Thread kt = new Thread(new Runnable() {
@Override
public void run() {
kongtiao = 2000;
System.out.println("空调分公司销售额为:" + kongtiao + "万");
}});
Thread ph = new Thread(new Runnable() {
@Override
public void run() {
phone = 7000;
System.out.println("手机分公司销售额为:" + phone + "万");
}});
Thread c = new Thread(new Runnable() {
@Override
public void run() {
car = 800;
System.out.println("汽车分公司销售额为:" + car + "万");
}});
kt.start();
ph.start();
c.start();
try {
kt.join();
ph.join();
c.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
sum = kongtiao + phone + car;
System.out.println("今年销售总额是:" + sum + "万");
}
}
这就不会出错了。
接着查看join()方法的实现。
public final synchronized void join(final long millis)
throws InterruptedException {
if (millis > 0) {
if (isAlive()) {
final long startTime = System.nanoTime();
long delay = millis;
do {
wait(delay);
} while (isAlive() && (delay = millis -
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
}
} else if (millis == 0) { // // join()方法不传参数时,millis为0进入该分支
while (isAlive()) { // 判断当前的线程是否处于活动状态
wait(0);
}
} else {
throw new IllegalArgumentException("timeout value is negative");
}
}
其实对于join()方法,也是使用了final和synchronized修饰的方法,同时也是调用了wait()方法,所以,join()方法的实现原理是使用wait()方法实现的,它达到的效果其实和使用synchronized类似,区别就是synchronized使用的是对象监听器。
while (isAlive()) { // 判断线程是否处于活动状态,也就是如果配角线程还活着,主角线程就要等,我去,到底谁是主角!!!
wait(0);
}
同样可以发现,join()方法还可以传参,表示只等待指定的时间,超过时间后无论是否执行结束都会让出资源,进入等待状态。
if (millis > 0) {
if (isAlive()) {
final long startTime = System.nanoTime();
long delay = millis;
do {
wait(delay);
} while (isAlive() && (delay = millis -
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
}
}
管道输入/输出流
管道流是是一种使用比较少的线程间通信方式,管道输入/输出流和普通文件输入/输出流或者网络输出/输出流不同之处在于,它主要用于线程之间的数据传输,传输的媒介为管道。
管道输入/输出流主要包括4种具体的实现:PipedOutputStrean、PipedInputStrean、PipedReader和PipedWriter,前两种面向字节,后两种面向字符。
java的管道的输入和输出实际上使用的是一个循环缓冲数组来实现的,默认为1024,输入流从这个数组中读取数据,输出流从这个数组中写入数据,当这个缓冲数组已满的时候,输出流所在的线程就会被阻塞,当向这个缓冲数组为空时,输入流所在的线程就会被阻塞。
buffer:缓冲数组,默认为1024
out:从缓冲数组中读数据
in:从缓冲数组中写数据
public class TestPip {
public static void main(String[] args) throws IOException {
PipedWriter writer = new PipedWriter();
PipedReader reader = new PipedReader();
//使用connect方法将输入流和输出流连接起来
writer.connect(reader);
Thread printThread = new Thread(new Print(reader) , "PrintThread");
//启动线程printThread
printThread.start();
int receive = 0;
try{
//读取输入的内容
while((receive = System.in.read()) != -1){
writer.write(receive);
}
}finally {
writer.close();
}
}
private static class Print implements Runnable {
private PipedReader reader;
public Print(PipedReader reader) {
this.reader = reader;
}
@Override
public void run() {
int receive = 0;
try{
while ((receive = reader.read()) != -1){
//字符转换
System.out.print((char) receive);
}
}catch (IOException e) {
System.out.print(e);
}
}
}
}
注意:
对于Piped类型的流,必须先进性绑定,也就是调用connect()方法,如果没有将输入/输出流绑定起来,对于该流的访问将抛出异常。