并发编程四:深入理解java线程

文章目录

  • 深入理解java线程
    • 线程基础知识
      • 线程的同步互斥
      • 上下文切换(Context switch)
    • 线程的生命周期
      • 操作系统层面线程生命周期
      • Java线程的生命周期
    • java线程详解
      • java线程实现方式
      • Java线程实现原理
      • Java线程的调度机制
      • Thread 常用的方法
      • Java线程的中断机制
      • Java线程间通信

深入理解java线程

线程基础知识

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中 还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的 。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。 进程就可以视为程序的一个实例。
  • 操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。

线程

  • 线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
  • 线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度 (CPU调度)执行的最小单位

进程与线程的区别

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂:
    同一台计算机的进程通信称为 IPC(Inter-process communication)
    不同计算机之间的进程通信,需要通过网络,并遵守共同的协议, 例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,例如多个线程可以访问同一个共享变量,线程更轻量,线程上下文切换成本一般上要比进程上下文切换低。

进程间通信的方式

  1. 管道(pipe)及有名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
  2. 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方 式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果 上可以说是一致的。
  3. 消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式 中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息; 对消息队列有读权限得进程则可以从消息队列中读取信息。
  4. 共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
  5. 信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
  6. 套接字(socket):这是一种更为一般得进程间通信机制,它可用于网络中不同机器之 间的进程间通信,应用非常广泛。

线程的同步互斥

线程的同步互斥

  • 线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
  • 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等 待,直到占用资源者释放该资源。

四种线程同步互斥的控制方法

  • 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访 问。(在一段时间内只允许一个线程访问的资源就称为临界资源)。
  • 互斥量:为协调共同对一个共享资源的单独访问而设计的。
  • 信号量:为控制一个具有有限数量用户资源而设计。
  • 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

上下文切换(Context switch)

上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。

进程是程序的一个执行实例。在Linux中,线程是轻量级进程,可以并行运行,并与父进程(即创建线程的进程)共享一个地址空间和其他资源。
上下文是CPU寄存器和程序计数器在任何时间点的内容。
寄存器是CPU内部的一小部分非常快的内存(相对于CPU外部较慢的RAM主内存),它通过提供对常用值的快速访问来加快计算机程序的执行。
程序计数器是一种专门的寄存器,它指示CPU在其指令序列中的位置,并保存着正在执行的指令的地址或下一条要执行的指令的地址,这取决于具体的系统。

上下文切换可以更详细地描述为内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动:

  1. 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
  2. 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
  3. 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。

并发编程四:深入理解java线程_第1张图片
上下文切换只能在内核模式下发生。内核模式是CPU的特权模式,其中只有内核运行,并提供对所有内存位置和所有其他系统资源的访问。其他程序(包括应用程序)最初在用户模式下运行,但它们可以通过系统调用运行部分内核代码。
上下文的切换是有时间损耗的,会对性能有影响。如果在程序中不断的创建线程,那么这些线程之间的上下文切换会对系统性能造成很大的影响,因此要考虑线程复用,也就是利用到线程池区创建线程。
上下文切换通常是计算密集型的。就CPU时间而言,上下文切换对系统来说是一个巨大的成本, 实际上,它可能是操作系统上成本最高的操作。因此,操作系统设计中的一个主要焦点是尽可能地避免不必要的上下文切换。与其他操作系统(包括一些其他类unix系统)相比,Linux的众多优势之一是它的上下文切换和模式切换成本极低。
对于CPU密集型任务(计算密集型)和IO密集型任务的多线程创建,使用到线程池的参数是不一样,这个后续在分析。

通过命令查看CPU上下文切换情况
linux系统可以通过命令统计CPU上下文切换数据

 #可以看到整个操作系统每1秒CPU上下文切换的统计
 vmstat 1

并发编程四:深入理解java线程_第2张图片
其中cs列就是CPU上下文切换的统计。当然,CPU上下文切换不等价于线程切换。

查看某一个线程\进程的上下文切换
使用pidstat命令
常用的参数:

-u 默认参数,显示各个进程的 CPU 统计信息
-r 显示各个进程的内存使用情况
-d 显示各个进程的 IO 使用
-w 显示各个进程的上下文切换
-p PID 指定 PID

在这里插入图片描述
其中cswch表示主动切换,nvcswch表示被动切换。如果说该进程每秒主动切换次数很多的话,代码中可能存在大量的睡眠\唤醒操作。

从进程的状态信息中查看
通过命令 cat cat /proc/7059/status查看进程的状态信息
在这里插入图片描述
这2项就是该进程从启动到当前总的上下文切换情况。

线程的生命周期

操作系统层面线程生命周期

操作系统层面的线程生命周期基本上可以用下图这个“五态模型”来描述。这五态分别是:初始状态、可运行状态、运行状态、休眠状态和终止状态。
并发编程四:深入理解java线程_第3张图片

  1. 初始状态,指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层 面,真正的线程还没有创建。
  2. 可运行状态,指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行,但是还未分配时间片。
  3. 运行状态,当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态。
  4. 休眠状态,运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
  5. 线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状 态,进入终止状态也就意味着线程的生命周期结束了。

这五种状态在不同的编程语言当中有所不同。这属于操作系统层面的线程状态。

Java线程的生命周期

Java 语言中线程共有六种状态,分别是:

  1. NEW(初始化状态)
  2. RUNNABLE(可运行状态+运行状态)
  3. BLOCKED(阻塞状态)
  4. WAITING(无时限等待)
  5. TIMED_WAITING(有时限等待)
  6. TERMINATED(终止状态)

并发编程四:深入理解java线程_第4张图片
new Thread的时候 出入NEW 状态,调用start()方法的时候处于RUNNABLE状态,BLOCKED是针对synchronized来说的,
并发编程四:深入理解java线程_第5张图片
当调用wait()、join()、LockSupport.part()没有参数的方法是状态是WAITING 。带有参数时是TIMED_WAITING状态。线程异常或者执行结束后处于TERMINATED。

在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态, 即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。

并发编程四:深入理解java线程_第6张图片

java线程详解

java线程实现方式

思考一下java中创建线程有几种方式
1.使用 Thread类或继承Thread类

Thread t = new Thread() {
	public void run() {
		System.out.println("通过Thread创建的线程执行");
	}
};
t.start();

2.实现 Runnable 接口配合Thread
把【线程】和【任务】(要执行的代码)分开 Thread 代表线程 Runnable 可运行的任务(线程要执行的代码)

Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("通过Runnable接口实现的线程执行");
			}
		});
		t2.start();

3:使用有返回值的 Callable

public class CallableTask implements Callable<Integer> {
	@Override
	public Integer call() throws Exception {
		return new Random().nextInt();
	}
public static void main(String[] args) {
		ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
		Future<Integer> future = fixedThreadPool.submit(new CallableTask());
		try {
			Integer integer = future.get();
			System.out.println(integer);

		}catch (Exception e){
			e.printStackTrace();
		}
	}
}

4利用lambda创建线程
new Thread(() ‐ > System.out.println(Thread.currentThread().getName())).start();
看上去有多种创建线程的方法,其实本质上Java中实现线程只有一种方式。
对于使用lambda表达式来创建线程是因为Runnable接口上有@FunctionalInterface
在这里插入图片描述
基本上第四种和第一种是一样的只不过写法不同。再来看Thread方式创建线程。
首先Thread实现了Runnable接口。当我们继承Thread或者直接new Thread创建线程的时候,调用是它的run方法。它的run方法中有target的静态变量,这个target在Thread的定义就是一个Runnable。所以Thread的本质还是传入了一个Runnable和第二种通过构造器传入Runnable没有区别。
在这里插入图片描述
并发编程四:深入理解java线程_第7张图片
在这里插入图片描述
在来看第三种的通过实现Callable的方式创建线程。这种方式是通过线程池来创建线程的,我们来看看线程池怎么去创建线程。创建线程池有一个工具类Executors。底层在创建线程池用到了一个ThreadFactory。
并发编程四:深入理解java线程_第8张图片
并发编程四:深入理解java线程_第9张图片
ThreadFactory是一个接口,在Executors的实现类是DefaultThreadFactory。newThread的实现方法如下图
并发编程四:深入理解java线程_第10张图片
底层实现的方式是通过Thread来创建线程。
总结来说,线程的创建方式只有一种就是通过Thread来创建线程,但是执行任务的方式有多种,但是最后都是通过start()方法来启动线程。
思考下为什么线程需要通过start()方法来启动,而不是调用run()方法

Java线程实现原理

根据上面的问题用个案例来看下

public class ThreadDemo2 {

	public static void main(String[] args) {
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				System.out.println(Thread.currentThread().getName()+":通过Runnable方式执行任务");
			}
		};
		new Thread(runnable).start();
		new Thread(runnable).run();
		runnable.run();
	 }
}

为什么要通过start()来启动线程。来看下执行结果
并发编程四:深入理解java线程_第11张图片
三种方式都能执行run()方法的任务。但是new Thread(runnable).run();runnable.run();其实是主线程执行的结果,并没有创建一个线程,只是在main方法中进行对象的调用。那为什么start()方法就能创建一个线程呢。我们来看看这个start()的源码。start()的底层调了 start0()方法, start0()是一个本地方法,这涉及到jvm的层面。
并发编程四:深入理解java线程_第12张图片
在这里插入图片描述
Thread#start()jvm源码执行过程流程图
整个创建线程的流程是这样的:
首先start()方法调用start0()方法。start0()映射到jvm的代码是jvm_StartThread。在new Thread()的时候,如下图
并发编程四:深入理解java线程_第13张图片
Thread有一个静态块,静态块执行了registerNatives();也是一个本地方法,作用是把线程的方法映射到jvm层面如下图
并发编程四:深入理解java线程_第14张图片
就是说在这个时候start0()就映射到了jvm_StartThread。执行jvm_StartThread就创建了jvm层面javaThread对象。new javaThread()作用在于屏蔽不同操作系统之间的差异,对不同的操作系统执行不同的创建线程的方法。对于Linux系统来说,最终调了pthread_create方法。看下pthread_create命令的作用。Linux下输入man pthread_create;
在这里插入图片描述
很明显就是创建一个线程。那么从这里就是用户态到内核态的切换了。创建了线程后设置为初始化状态,Linux会自旋的进行判断线程是不是初始化状态,如果是该线程就会调用操作系统层面的wait()方法进行等待。那么这些过程就是jvm中new javaThread()的过程,线程创建完成时就是new javaThread()完成。
接下来把java的线程对象和jvm的线程对象绑定在一起。就是java的Thread和jvm的javaThread进行绑定。
为什么要进行绑定。之前jvm的new javaThread()操作是在操作系统上创建了一个线程,这样javaThread就和操作系统的线程建立了关联,但是操作系统的线程和java的线程没有关联,java的Thread和jvm的javaThread进行绑定以后,java线程就和操作系统的线程间接建立了关联。
绑定完成后设置java线程状态为RUNNABLE,然后去唤醒操作系统的线程。唤醒操作系统的线程后,会去调jvm线程的run方法,jvm最后调到java线程的run方法()。

Java线程属于内核级线程
JDK1.2——基于操作系统原生线程模型来实现。Sun JDK,它的Windows版本和Linux版本 都使用一对一的线程模型实现,一条Java线程就映射到一条轻量级进程之中。
内核级线程(Kernel Level Thread ,KLT):它们是依赖于内核的,即无论是用户进程中的线 程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。
用户级线程(User Level Thread,ULT):操作系统内核不知道应用线程的存在。

Java线程的调度机制

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式分两种,分别是协同式线程调 度和抢占式线程调度
协同式线程调度
线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另 外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。
抢占式线程调度
每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中, Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。

Java线程调度就是抢占式调度。
希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至 Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所 以线程调度最终还是取决于操作系统。

package demo;

public class BuyTicketDemo implements Runnable {

	private Integer ticket;

	public BuyTicketDemo() {
		this.ticket = 1000;
	}

	@Override
	public void run() {
		while (ticket > 0) {
			synchronized (this) {
				if (ticket > 0) {
					try {
						Thread.sleep(20);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + ":正在执行操作,余票:" + ticket--);
				}
			}
			Thread.yield();
		}

	}

	public static void main(String[] args) {
		BuyTicketDemo demo = new BuyTicketDemo();
		Thread thread1 = new Thread(demo,"Thread1");
		Thread thread2 = new Thread(demo,"Thread2");
		Thread thread3 = new Thread(demo,"Thread3");
		Thread thread4 = new Thread(demo,"Thread4");
		//给2和4最高的优先级 1和3最低的优先级
		thread1.setPriority(Thread.MIN_PRIORITY);
		thread2.setPriority(Thread.MAX_PRIORITY);
		thread3.setPriority(Thread.MIN_PRIORITY);
		thread4.setPriority(Thread.MAX_PRIORITY);
		thread1.start();
		thread2.start();
		thread3.start();
		thread4.start();
	}
}

从执行结果来看,优先级有点用但又不完全有用,优先级高的24不代表第一次就是这两个线程,13优先级低也不代表没有机会执行。

Thread 常用的方法

sleep方法
调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志 睡眠结束后的线程未必会立刻得到执行 sleep当传入参数为0时,和yield相同

yield方法
yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高 (至少是相同)的线程获得执行机会,不会释放对象锁; 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程; 具体的实现依赖于操作系统的任务调度器

不会释放锁对象是什么意思,比如一个线程通过synchronized加锁以后,如果通过sleep阻塞了之后,不会释放锁,其他线程依然获取不到加锁的资源。如果通过wait方式阻塞的话,会释放锁,其他线程能够获取加锁的资源。这个后续在分析。

join方法
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之 后才能继续运行的场景。

public class ThreadJoinDemo {
	public static void main(String[] args) throws InterruptedException {
		Thread t = new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("t begin");
				try {
					Thread.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("t finished");
			}
		});
		long start = System.currentTimeMillis();
		t.start();
		//main线程阻塞等待t线程完成
		t.join();
		System.out.println("执行时间:"+(System.currentTimeMillis() - start));
		System.out.println("main 线程完成");
	 }
}

stop方法
stop()方法已经被jdk废弃,原因就是stop()方法太过于暴力,强行把执行到一半的线程终止。会释放锁对象
那么如何正确优雅的停止线程?

Java线程的中断机制

Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。中断机制是一 种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。
优雅的停止线程和线程的中断机制有关。
java中断机制就是说,在线程当中添加了一个中断标志为true,但是具体什么时候判断中断标志,什么时候停止线程需要自己决定,这是比较安全的停止线程的方式。可能听起来不好理解。来看两个例子
API的使用

interrupt(): 将线程的中断标志位设置为true,不会停止线程
isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle

案例1:

package demo;

public class ThreadStopDemo {
	static int i = 0;
	public static void main(String[] args) {
		System.out.println("begin");
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				while (true){
					i++;
					System.out.println(i);
					//判断当前线程的中断标志
					if (Thread.currentThread().isInterrupted()){
						//如果中断标志为true 打印=======
						System.out.println("=========");
					}
					if(i==10){
						break;
					}
				}
			}
		});
		//线程开始执行
		t1.start();
		//给线程添加中断标志为true
		t1.interrupt();

	 }
}

当我们给线程设置中断标志,那么在线程中需要编写代码判断这个中断标志。上述代码运行结果是打印====10次
并发编程四:深入理解java线程_第15张图片
把Thread.currentThread().isInterrupted() 换成Thread.interrupted()。上面说了Thread.interrupted()会清除中断标志,所以只打印一次
并发编程四:深入理解java线程_第16张图片
如果上面代码改成这样
并发编程四:深入理解java线程_第17张图片
运行结果:
并发编程四:深入理解java线程_第18张图片
所以用到中断机制的时候遇到sleep、wait

  • sleep可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位
  • wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位

上述两个方法检测到线程中的中断标志为true就会抛出异常并且清除中断标志。因此需要在catch中补上中断标志
并发编程四:深入理解java线程_第19张图片
运行结果:
并发编程四:深入理解java线程_第20张图片
案例2:如何优雅中断线程

public class ThreadStopDemo2 implements Runnable{
	@Override
	public void run() {
		int count = 0;
		//中断标志为true或者 count 大于100 的时候跳出循环
		while (!Thread.currentThread().isInterrupted() && count < 1000){
			System.out.println("count = " + count++);
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
				//重新补上中断标志
				Thread.currentThread().interrupt();
			}
		}
		System.out.println("线程停止: stop thread");
	}
	public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread(new ThreadStopDemo2());
		thread.start();
		Thread.sleep(10);
		thread.interrupt();
	}
}

给线程添加中断标志,在线程判断中断标志如果为true跳出while循环。看下运行结果
并发编程四:深入理解java线程_第21张图片
如果没有在catch中重新补上中断标志为的话,在看下结果
并发编程四:深入理解java线程_第22张图片
会运行到999才会结束。

Java线程间通信

volatile
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程 之间进行通信。
在之前并发编程分析过了,就不在举例了。

等待唤醒(等待通知)机制
等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法, 线程将进入等待队列进行等待直到被唤醒
这个并不是那么好用,有局限性,必须要和synchronized使用,还有就是如果存在多个线程没法指定要唤醒那个线程,不过可以通过notifyAll()唤醒全部线程。

LockSupport
LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用 unpark则为指定线程提供“许可”。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。
这个是用的比较多的。举个例子

import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo1 {
	static class AA implements Runnable{
		@Override
		public void run() {
			System.out.println("线程开始执行");
			//等待许可
			LockSupport.park();
			System.out.println("线程执行结束");
		}
	}
	public static void main(String[] args) {
		Thread thread = new Thread(new AA());
		thread.start();
		System.out.println("唤醒AA");
		//给AA线程一个许可
		LockSupport.unpark(thread);
	}
}

管道输入输出流
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程 之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现: PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节, 而后两种面向字符。

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
public class PipedTest {
    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        // 将输出流和输入流进行连接,否则在使用时会抛出IOException
        out.connect(in);
        Thread printThread = new Thread(new Print(in), "PrintThread");
        printThread.start();
        int receive = 0;
        try {
            while ((receive = System.in.read()) != -1) {
                out.write(receive);
            }
        } finally {
            out.close();
        }
    }
    static class Print implements Runnable {
        private PipedReader in;

        public Print(PipedReader in) {
            this.in = in;
        }
        @Override
        public void run() {
            int receive = 0;
            try {
                while ((receive = in.read()) != -1) {
                    System.out.print((char) receive);
                }
            } catch (IOException ex) {
            }
        }
    }
}

上面代码意思是在控制台输入什么就打印什么。
并发编程四:深入理解java线程_第23张图片

Thread.join
join可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等 待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但 是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串 行的,最后join的实现其实是基于等待通知机制的。
Join方法上面有案例了,就不在举案例了。

你可能感兴趣的:(并发编程专题,java,并发编程,多线程)