进阶笔录- 并发编程之深入理解Java线程
问题:
-
CAS涉及到用户模式到内核模式的切换吗?
-
为什么说创建Java线程的方式本质上只有一种?Java线程和go语言的协程有什么区别?
-
如何优雅的终止线程?
-
Java线程之间如何通信的,有哪些方式?
1. 线程与进程
线程:cpu调度的基本单位,一个指令序列
进程:进程是资源分配的最小单位,操作系统会以进程为单位,分配系统资源(Cpu时间片、内存等资源)。
查看进程线程的方法
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线程创建有几种方式
- 继承Thead
- 实现Runnable
- 实现Callable(带返回值)
.start()和.run()的区别?
.sart()才是正在的线程启用,其底层操作系统会切换到内核态去创建线程
Java、Jvm、os调用关系:
Java Thread–> Jvm JavaThread --> os Thread(通过两两映射关系,使得用java即可创建出对应的线程)
可以通过 new Object()–> jvm JavaThread去创建线程,最终内核态创建线程
线程创建和启动的流程总结
- 使用new Thread(创建一个线程,然后调用start()方法进行java层面的线程启动);
- 调用本地方法start0(),去调用jvm中的JVM_StartThread方法进行线程创建和启动;
- 调用new JavaThread(8thread entry, sz)进行线程的创建,并根据不同的操作系统平台调用对应的os:create _thread方法进行线程创建;
- 新创建的线程状态为Initialized,调用了sync->wait()的方法进行等待,等到被唤醒才继续执行thread->run();
- 调用Thread:istart(native thread);方法进行线程启动,此时将线程状态设置为RUJNNABLE,接着调用osostart thread(thread),根据不同的操(系统选择不同的线程启动方式;
- 线程启动之后状态设置为RUNNABLE,并唤醒第4步中等待的线程,接着执行thread->run()的方法;
- JavaThread::run()方法会回调第1步new Thread中复写的run()方法。
协程(线程又往下分,用户级线程)
Thread常用方法
sleep方法
- 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
- 睡眠结束后的线程未必会立刻得到执行
- sleep当传入参数为0时,和yield相同
yield方法(让出时间片/切换一次上下文)
- yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁;
- 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程;
- 具体的实现依赖于操作系统的任务调度器
join方法
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。
wait方法
当某个线程获取到锁后,发现当前还不满足执行的条件,就可以调用对象锁的wait方法,进入等待状态。
直到某个时刻,外在条件满足了,就可以由其他线程通过调用notify()或者notifyAll()方法,来唤醒此线程。
这篇文章将侧重于讨论wait()方法对于线程状态的影响,以及被唤醒后线程的状态变更。
只有已经获取锁的线程,才可以调用锁的wait方法,否则会抛出异常IllegalMonitorStateException。(wait 方法需要配合synchronized使用)
stop方法
stop()方法已经被jdk废弃,原因就是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();
}
}
stop会释放对象锁,可能会造成数据不一致。
进程间通信的方式
- 管道(pipe)及有名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
- 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
- 消息队列(message queue):消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
- 共享内存(shared memory):可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
- 信号量(semaphore):主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
- 套接字(socket):这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
上下文切换
-
上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换
-
上下文切换是多任务操作系统的一个基本特性。
-
尽可能地避免不必要的上下文切换。(切换线程需要从用户态切换到内核态,非常消耗性能和时间)
内核态和用户态:
内核模式(Kernel Mode)vs 用户模式(User Mode)
在现代操作系统中,CPU实际上都在两种截然不同的模式中花费时间:
Kernel Mode
在内核模式下,执行代码可以完全且不受限制地访问底层硬件。它可以执行任何CPU指令和引用任何内存地址。内核模式通常为操作系统的最低级别、最受信任的功能保留。内核模式下的崩溃是灾难性的;他们会让整个电脑瘫痪。
User Mode
在用户模式下,执行代码不能直接访问硬件或引用内存。在用户模式下运行的代码必须委托给系统api来访问硬件或内存。由于这种隔离提供的保护,用户模式下的崩溃总是可恢复的。在您的计算机上运行的大多数代码将在用户模式下执行。
应用程序一般会在一下几种情况下切换到内核模式:
- 系统调用。
- 异常事件。
- 设备中断。
线程生命周期
操作系统层面:线程生命周期可以用五态模型来描述:初始状态、可运行状态、运行状态、休眠状态和终止状态
Java层面:6种
BLOCKED 只针对synchronize
2. 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);
// try {
// Thread.sleep(10000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// Thread.interrupted(); 清除中断标志位
//Thread.currentThread().isInterrupted() 不会清除中断标志位
if (Thread.currentThread().isInterrupted() ) {
System.out.println("=========");
// break;
}
// if(i==10){
// break;
// }
}
}
});
t1.start();
// try {
// Thread.sleep(2000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//不会停止线程t1,只会设置一个中断标志位 flag=true
t1.interrupt();
}
}
利用中断机制优雅的停止线程
while (!Thread.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 期间能否感受到中断
修改上面的代码,线程执行任务期间有休眠需求
public class StopThread implements Runnable {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted() && count < 1000) {
System.out.println("count = " + count++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
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();
}
}
处于休眠中的线程被中断,线程是可以感受到中断信号的,并且会抛出一个InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。这样就会导致while条件Thread.currentThread().isInterrupted()为false,程序会在不满足count < 1000这个条件时退出。如果不在catch中重新手动添加中断信号,不做任何处理,就会屏蔽中断请求,有可能导致线程无法正确停止。
重置中断异常退出:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
wait抛出中断异常:
public class TheadStopDemo3 {
public static Object lock =false;
public static void main(String[] args) {
Thread test = new Thread(()->{
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
System.out.println(e);
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
});
test.start();
test.interrupt();
}
}
sleep可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位
wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位
3. Java线程间通信
volatile: 查看->JMM的内存可见性保证
等待唤醒(等待通知)机制
等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒。
public class WaitDemo {
private static Object lock = new Object();
private 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){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//通知
lock.notifyAll();
System.out.println("notify .......");
flag = false;
}
}
}
}
}).start();
}
}
LockSupport
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");
//为指定线程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,前两种面向字节,而后两种面向字符。
以下程序将输入流和输出流连接成管道,然后new一个线程将输入流连接,打印通过管道传输的数据
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) {
}
}
}
}
回答开头的几个问题:
- CAS涉及到用户模式到内核模式的切换吗?
答:CAS相当于在用户态代码里边插入了一个cmpxchg指令,这个指令由硬件保证原子性,所谓不可再分的CPU同步原语,不需要去调用内核api - 为什么说创建Java线程的方式本质上只有一种?Java线程和go语言的协程有什么区别?
答:本质上都是实现Runnable接口,然后start()一个线程。协程是线程再分,协程只涉及到用户态,而创建线程,需要内核态去创建线程 - 如何优雅的终止线程?
使用中断机制或者根据业务需要加状态去判断是否需要结束线程
详细 查看 2.2 Java线程的中断机制 - Java线程之间如何通信的,有哪些方式?
答:使用volatitle或者等待唤醒机制