关于深入理解Java线程
一、线程基础
1、进程和线程
进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的 。
- 当一个程序被运行,从磁盘加载此程序的代码到内存时,就开启了一个进程。
- 进程可视为程序的一个实例。大部分程序可同时运行多个实例进程(如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(如网易云音乐、360 安全卫士等)。
- 操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。
线程
- 线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行 。
- 线程有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。
线程在CPU中表现为指令序列,一行指令序列对应一个线程
JDK自带的工具jconsole.exe可以查看线程(C:\Program Files\Java\jdk1.8.0_301\bin)
以启动Tomcat为例,会开启对应进程,包含以下线程:
main:主方法线程
RMI TCP Accept-0:需要处理对外的请求,因此会有连接发送过来(请求),此线程处理对应的socket
ajp-nio-8009-ClientPoller-0:nio模型,会转交给两个poller处理(默认处理器的最小值,一般为2个)
ajp-nio-8009-ClientPoller-1:poller处理写事件(响应)
进程与线程的区别
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂
a. 同一台计算机的进程通信称为IPC(Inter-process communication)
b. 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP - 线程通信相对简单,因为它们共享进程内的内存,例如多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上比进程上下文切换低
进程间通信方式
- 管道(pipe)及有名管道(named pipe):
管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
- 信号(signal):
信号是在软件层次上对中断机制的一种模拟,是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果一致。
- 消息队列(message queue):
消息队列是消息的链接表,克服了上述两种通信方式中信号量有限的缺点,具有写权限的进程可按照一定的规则向消息队列中添加新信息;对消息队列有读权限的进程则可从消息队列中读取信息。
- 共享内存(shared memory):
为最有用的进程间通信方式。多个进程可访问同一块内存空间,不同进程可及时看到对方进程中对共享内存中数据的更新。此方式需依靠某种同步操作,如互斥锁和信号量等。
- 信号量(semaphore):
主要作为进程之间及同一种进程的不同线程之间的同步和互斥手段。
- 套接字(socket):
一种更为一般的进程间通信机制,可用于网络中不同机器之间的进程间通信,应用非常广泛。
2、线程的同步互斥
线程同步:
线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程互斥:
对于共享的进程系统资源,在各单个线程访问时存在的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可看成是一种特殊的线程同步。
四种线程同步互斥的控制方法
- 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
在一段时间内只允许一个线程访问的资源则为临界资源。
- 互斥量:用来协调共同对一个共享资源的单独访问。
- 信号量:用来控制一个具有有限数量用户资源。
- 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。
3、上下文切换(Context switch)
上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。
进程是程序的一个执行实例。在Linux中,线程是轻量级进程,可以并行运行,并与父进程(即创建线程的进程)共享一个地址空间和其他资源。
上下文是CPU寄存器和程序计数器在任何时间点的内容。
寄存器是CPU内部的一小部分非常快的内存(相对于CPU外部较慢的RAM主内存),它通过提供对常用值的快速访问来加快计算机程序的执行。
程序计数器是一种专门的寄存器,它指示CPU在其指令序列中的位置,并保存着正在执行的指令的地址或下一条要执行的指令的地址,这取决于具体的系统。
上下文切换可以更详细地描述为内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动:
- 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
- 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
- 返回到程序计数器指示的位置(即返回到进程被中断的代码行)来恢复进程。
上下文切换只能在内核模式下发生
内核模式是CPU的特权模式,其中只有内核运行,并提供对所有内存位置和所有其他系统资源的访问。
其他程序(包括应用程序)最初在用户模式下运行,但它们可以通过系统调用运行部分内核代码。
内核模式(Kernel Mode)
在内核模式下,执行代码可以完全且不受限制地访问底层硬件。可执行任何CPU指令和引用任何内存地址。内核模式通常为操作系统的最低级别、最受信任的功能保留。内核模式下的崩溃是灾难性的,会使整个电脑瘫痪。
用户模式(User Mode)
在用户模式下,执行代码不能直接访问硬件或引用内存。在用户模式下运行的代码必须委托给系统api来访问硬件或内存。由于这种隔离提供的保护,用户模式下的崩溃总是可恢复的。在计算机上运行的大多数代码将在用户模式下执行。
CPU保护模式
x86 CPU提供了四个保护环(protection rings):0、1、2、3。通常只使用0环(内核)和3环(用户)。
应用程序切换到内核模式的情况:
- 系统调用。
- 异常事件。当发生某些预先不可知的异常时,就会切换到内核态,以执行相关的异常事件。
- 设备中断。在使用外围设备时,如外围设备完成了用户请求,就会向CPU发送一个中断信号,此时,CPU就会暂停执行原本的下一条指令,转去处理中断事件,若原来在用户态,则会切换到内核态。
上下文切换是多任务操作系统的一个基本特性
在多任务操作系统中,多个进程似乎同时在一个CPU上执行,彼此之间互不干扰。这种并发的错觉是通过快速连续发生的上下文切换(每秒数十次或数百次)来实现的。这些上下文切换发生的原因是进程自愿放弃它们在CPU中的时间,或者是调度器在进程耗尽其CPU时间片时进行切换的结果。
上下文切换通常是计算密集型的。就CPU时间而言,上下文切换对系统来说是一个巨大的成本,实际上,它可能是操作系统上成本最高的操作。因此,操作系统设计中的一个重点是尽可能地避免不必要的上下文切换。与其他操作系统(包括一些其他类unix系统)相比,Linux的众多优势之一是它的上下文切换和模式切换成本极低。
通过命令查看CPU上下文切换情况
linux系统可以通过命令统计CPU上下文切换数据
#可以看到整个操作系统每1秒CPU上下文切换的统计
vmstat 1
cs列为CPU上下文切换的统计。
只是CPU上下文切换不等价于线程切换,很多操作会造成CPU上下文切换:
- 线程、进程切换
- 系统调用
- 中断
查看某一个线程\进程的上下文切换
使用pidstat命令
常用的参数:
-u 默认参数,显示各个进程的 CPU 统计信息
-r 显示各个进程的内存使用情况
-d 显示各个进程的 IO 使用
-w 显示各个进程的上下文切换
-p PID 指定 PID
# 显示进程5598每一秒的切换情况
pidstat ‐w ‐p 5598 1
cswch表示主动切换,nvcswch表示被动切换。从统计数据中看到,该进程每秒主动切换次数达到将近500次,因此代码中存在大量的 睡眠\唤醒 操作。
从进程的状态信息中查看
通过命令 cat /proc/5598/status 查看进程的状态信息
voluntary_ctxt_switches: 40469351
nonvoluntary_ctxt_switches: 2268
上述两项即为该进程从启动到当前时间,所有的上下文切换情况
4、操作系统层面的线程生命周期
操作系统层面的线程生命周期为5种状态:
初始状态、可运行状态、运行状态、休眠状态、终止状态。
- 初始状态:
线程已经被创建,但还不允许分配CPU执行。此状态属于编程语言特有的,但此处的被创建,仅为编程语言层面的被创建,而在操作系统层面,真正的线程还未被创建。
- 可运行状态:
线程可分配CPU执行。此状态下,真正的操作系统线程已被成功创建,所以可分配CPU执行。
- 可运行状态与运行状态:
当有空闲的CPU时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU的线程的状态就转换成了运行状态。
- 休眠状态:
运行状态的线程若调用一个阻塞的API(如以阻塞方式读文件)或等待某个事件(如条件变量),此时线程的状态就会转换为休眠状态,同时释放CPU使用权,休眠状态的线程永远没有机会获得CPU使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
- 终止状态:
线程执行完或出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态意味着线程的生命周期彻底结束。
这五种状态在不同编程语言里会有简化合并。例如,C 语言的POSIX Threads规范,就把初始状态和可运行状态合并;Java语言里则把可运行状态和运行状态合并,这两个状态在操作系统调度层面有用,而JVM层面不关心这两个状态,因为JVM把线程调度交给操作系统处理了。
5、查看进程线程的方法
windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
linux
- ps - fe 查看所有进程
- ps - fT - p 查看某个进程( PID )的所有线程
- kill 杀死进程
- top 按大写 H 切换是否显示线程
- top - H - p 查看某个进程( PID )的所有线程
Java
- jps 命令查看所有Java进程
- jstack 查看某个Java进程( PID )的所有线程状态
- jconsole 查看某个Java进程中线程的运行情况(图形界面)
Linux系统中线程实现方式
- LinuxThreads:linux/glibc包在2.3.2之前只实现了LinuxThreads
- NPTL(Native POSIX Thread Library)
// 可以通过以下命令查看系统是使用哪种线程实现
getconf GNU_LIBPTHREAD_VERSION
二、Java线程详解
1、Java线程的实现方式
方式1:使用 Thread类或继承Thread类
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
方式2:实现 Runnable 接口配合Thread
把 线程 和 任务(要执行的代码)分开
- Thread:线程
- Runnable:可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
方式3:使用有返回值的 Callable
class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return new Random().nextInt();
}
}
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//提交任务,并用 Future提交返回结果
Future<Integer> future = service.submit(new CallableTask());
方式4:使用 lambda
new Thread(() ‐> System.out.println(Thread.currentThread().getName())).start();
本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终都会调用Thread#run方法
2、Java线程实现原理
Java线程实现的本质都是调用start方法,而start方法的底层会通过启动Java的线程,使得JVM和操作系统也创建出对应的线程,其中涉及到用户态切换到内核态,经过一系列的操作,最后才调用run方法。
若直接调用run方法,只是普通对象的方法调用,并不是一个真正的线程。
线程创建和启动的流程:
- 使用new Thread()创建一个线程,再调用start()进行Java层面的线程启动;
- 通过调用本地方法start0(),来调用JVM中的JVM_StartThread方法进行线程创建和启动;
- 调用new JavaThread(&thread_entry, sz)进行线程的创建,并根据不同的操作系统平台调用对应的os::create_thread方法进行线程创建;
- 新创建的线程状态为Initialized,调用了sync的wait方法进行等待,等到被唤醒才继续执行thread的run方法;
- 调用Thread::start(native_thread)进行线程启动,此时将线程状态置为RUNNABLE,接着再调用os:start_thread(thread),根据不同的操作系统选择不同的线程启动方式;
- 线程启动之后状态置为RUNNABLE,并唤醒第4步中等待的线程,再接着执行thread的run方法;
- JavaThread::run()会回调第1步new Thread中复写的run方法。
Java线程属于内核级线程
JDK1.2:基于操作系统原生线程模型来实现。
Sun JDK:Windows版本和Linux版本都使用一对一的线程模型实现,一条Java线程就映射到一条轻量级进程之中。
内核级线程(Kernel Level Thread ,KLT)
依赖于内核,即无论是用户进程中的线程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。
用户级线程(User Level Thread,ULT)
操作系统内核不知道应用线程的存在:
协程(Coroutines)
一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理的,是完全由程序所控制(即在用户态执行),具有对内核来说不可见的特性。
这样的好处是极大地提升性能,不会像线程切换那样消耗资源。
子程序(函数)在所有语言中都是层级调用。如A调用B,B在执行过程中又调用C,C执行完毕返回,B执行完毕返回,最后A执行完毕。
协程的调用和子程序不同。协程在子程序内部是可中断的,中断后去执行别的子程序,在适当的时候再返回来接着执行。
def A():
print '1'
print '2'
print '3'
def B():
print 'x'
print 'y'
print 'z'
假设由协程执行,在执行A的过程中,可以随时中断,转去执行B,B也可能在执行过程中中断再去执行A,因此结果可能是:1 2 x y 3 z。
协程的特点是一个线程执行,和多线程比,协程的优势:
- 线程的切换由操作系统调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
- 线程的默认stack大小是1M,而协程更轻量,接近1k。因此可以在相同的内存中开启更多的协程。
- 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需判断状态即可,所以执行效率比多线程高很多。
注意: 协程适用于被阻塞的,且需要大量并发的场景(网络io)。不适合大量计算的场景。
3、Java线程的调度机制
系统为线程分配处理器使用权的过程,主要调度方式分两种:协同式线程调度、抢占式线程调度。
协同式线程调度
线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。
最大优点是实现简单,且切换操作对线程自己是可知的,无线程同步问题。
缺点是线程执行时间不可控制,若一个线程产生问题,可能引起永久阻塞。
抢占式线程调度
每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。
Java线程调度即为抢占式调度
希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不绝对,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。
public class SellTicketDemo implements Runnable {
/**
* 车票
*/
private int ticket;
public SellTicketDemo() {
this.ticket = 1000;
}
@Override
public void run() {
while (ticket > 0) {
synchronized (this) {
if (ticket > 0) {
try {
//线程进入暂时的休眠
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前正在执行的程序的名称,打印余票
System.out.println(Thread.currentThread().getName()
+ ":正在执行操作,余票:" + ticket--);
}
}
Thread.yield();
}
}
public static void main(String[] args) {
SellTicketDemo demo = new SellTicketDemo();
Thread thread1 = new Thread(demo, "thread1");
Thread thread2 = new Thread(demo, "thread2");
Thread thread3 = new Thread(demo, "thread3");
Thread thread4 = new Thread(demo, "thread4");
//priority优先级默认为5,最低1,最高10
thread1.setPriority(Thread.MAX_PRIORITY);
thread2.setPriority(Thread.MAX_PRIORITY);
thread3.setPriority(Thread.MIN_PRIORITY);
thread4.setPriority(Thread.MIN_PRIORITY);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
4、Java线程的生命周期
Java语言中线程共有6种状态
-
NEW(初始化状态):
只是普通对象
-
RUNNABLE(可运行状态+运行状态):
调用start(),变为就绪状态
-
BLOCKED(阻塞状态):
休眠状态之阻塞,针对synchronized:
①其中一条线程获取到锁,另一条线程就进入阻塞状态;
②当线程调用wait(),即释放锁,阻塞状态的线程被唤醒再去参与获取锁又失败时,又会进入阻塞状态 -
WAITING(无时限等待):
休眠状态之无时间限制的等待(调用wait()、join()、park())
-
TIMED_WAITING(有时限等待):
休眠状态之有时间限制的等待(加上超时时间 (long) )
-
TERMINATED(终止状态)
在操作系统层面,Java 线程中的BLOCKED、WAITING、TIMED_WAITING是一种状态,即前文提到的休眠状态。即若Java线程处于这三种状态之一,则此线程永远没有CPU的使用权。
测试:
public class ThreadStateTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread();
System.out.println("线程状态 >>> " + thread.getState());
thread.start();
System.out.println("线程状态 >>> " + thread.getState());
Thread.sleep(100);
System.out.println("线程状态 >>> " + thread.getState());
}
}
运行结果:
线程状态 >>> NEW
线程状态 >>> RUNNABLE
线程状态 >>> TERMINATED
线程启动没做其他操作,睡眠100ms直接终止
若启动线程之后进行了其他任务,则不会立即终止:
public class ThreadStateTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
LockSupport.park();
}
});
System.out.println("线程状态 >>> " + thread.getState());
thread.start();
System.out.println("线程状态 >>> " + thread.getState());
Thread.sleep(100);
System.out.println("线程状态 >>> " + thread.getState());
}
}
运行结果:
线程状态 >>> NEW
线程状态 >>> RUNNABLE
线程状态 >>> WAITING
从JavaThread的角度,JVM定义了一些针对Java Thread对象的状态(jvm.h)
从OSThread的角度,JVM还定义了一些线程状态给外部使用,如用jstack输出的线程堆栈信息中线程的状态(osThread.hpp)
5、Thread常用方法
sleep方法
- 调用sleep会让当前线程从Running进入TIMED_WAITING状态,不会释放对象锁
- 其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException,并且会清除中断标志
- 睡眠结束后的线程未必会立刻得到执行
- sleep当传入参数为0时,和yield相同
yield方法
- yield会释放CPU资源,让当前线程从Running进入Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁
- 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程
- 具体的实现依赖于操作系统的任务调度器
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(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t finished");
}
});
long start = System.currentTimeMillis();
t.start();
//主线程等待线程t执行完成
t.join();
System.out.println("执行时间:" + (System.currentTimeMillis() - start));
System.out.println("Main finished");
}
}
stop方法
- stop方法已被jdk废弃,由于stop方法太过暴力,强行把执行到一半的线程终止,并释放锁,容易造成数据不一致。
public class ThreadStopDemo {
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获取锁");
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "执行完成");
}
});
thread.start();
Thread.sleep(2000);
//停止thread,并释放锁
thread.stop();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "等待获取锁");
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获取锁");
}
}
}).start();
}
}
5、Java线程的中断机制
- Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。
- 中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。
- 被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。
API的使用
- interrupt(): 将线程的中断标志位设置为true,不会停止线程
- isInterrupted():判断当前线程的中断标志位是否为true,不会清除中断标志位
- Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle
public class ThreadInterruptTest {
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);
//Thread.interrupted():清除中断标志位
//Thread.currentThread().isInterrupted():不会清除中断标志位
if (Thread.currentThread().isInterrupted()) {
System.out.println("==========");
}
if (i == 10) {
break;
}
}
}
});
t1.start();
//不会停止线程t1,只会设置一个中断标志位 flag=true
t1.interrupt();
}
}
利用中断机制优雅地停止线程
while(!Thead.currentThread().isInterrupted() && more work to do) {
do more work
}
public class StopThread implements Runnable {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted() && count < 1000) {
System.out.println("count = " + count++);
}
System.out.println("线程停止:stop thread");
};
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new StopThread());
thread.start();
Thread.sleep(5);
thread.interrupt();
}
}
注意:使用中断机制时一定要注意是否存在中断标志位被清除的情况
sleep期间可感受到中断
修改上面的代码,线程执行任务期间有休眠需求:
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted() && count < 1000) {
System.out.println("count = " + count++);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程停止:stop thread");
};
处于休眠中的线程被中断,线程是可感受到中断信号,且会抛出一个InterruptedException异常,同时清除中断信号,将中断标记位设置成false。这样就会导致while条件Thread.currentThread().isInterrupted()为false,程序会在不满足count < 1000这个条件时退出。若不在catch中重新手动添加中断信号,不做任何处理,就会屏蔽中断请求,有可能导致线程无法正确停止。
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
//重新设置线程中断状态为true
Thread.currentThread().interrupt();
}
sleep可以被中断,抛出中断异常:sleep interrupted, 清除中断标志位
wait可以被中断,抛出中断异常:InterruptedException, 清除中断标志位
6、Java线程间通信
volatile
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。
public class VolatileDemo {
public static volatile boolean flag = true;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (flag) {
System.out.println("turn on");
flag = false;
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (!flag) {
System.out.println("turn off");
flag = true;
}
}
}
}).start();
}
}
等待唤醒(等待通知)机制
等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待,直到被唤醒。
public class WaitDemo {
public static Object lock = new Object();
public static boolean flag = true;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
while (flag) {
try {
System.out.println("wait start …………");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("wait end …………");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
if (flag) {
synchronized (lock) {
if (flag) {
lock.notify();
System.out.println("notify …………");
flag = false;
}
}
}
}
}).start();
}
}
LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用unpark则为指定线程提供“许可”。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。
public class LockSupportTest {
public static void main(String[] args) {
Thread parkThread = new Thread(new ParkThread());
parkThread.start();
System.out.println("唤醒parkThread");
LockSupport.unpark(parkThread);
}
static class ParkThread implements Runnable{
@Override
public void run() {
System.out.println("ParkThread开始执行");
LockSupport.park();
System.out.println("ParkThread执行完成");
}
}
}
管道输入输出流
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处:
主要用于线程之间的数据传输,而传输的媒介为内存。
管道输入/输出流主要包括了如下4种具体实现:
PipedOutputStream、PipedInputStream、PipedReader和PipedWriter;
前两种面向字节,后两种面向字符。
public class Piped {
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.println((char) receive);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Thread.join
join可理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的优点是能够保证线程的执行顺序,但若调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但本质上仍为串行,最后join的实现其实是基于等待通知机制的。