1.线程的基础知识
进程:
操作系统分配资源的单元
哪些资源:指令,内存管理,IO管理,磁盘资源,CPU时间片
线程: 进程中的实体
区别:
- 进程基本相互独立,线程存在于进程内,
- 进程拥有共享资源,供内部线程共享
- 进程通信复杂
同一机器的通信
不同机器的通信:网络
进程间的通信方式:
- 管道:用于父子进程间,还有有名管道,允许无亲缘关系进程的通信
- 信号:软件层次对中断机制的模拟,比较复杂,用在通知进程事件发生,与中断请求效果一样
- 消息队列:消息的链接表:克服信号量有限的缺点,
- 共享内存:最有用的方式,依靠同步操作,互斥锁,信号量
- 信号量:进程间或同一进程间不同线程之间的同步互斥手段
- 套接字:用户网络间的不同机器的通信
线程同步互斥
线程间的制约关系,一个线程依赖另一个线程的消息
线程互斥:线程对共享的资源,在访问时的排他性,一种特殊的线程同步
四种线程同步互斥的控制方法
- 临界区 :串行访问的共享资源
- 互斥量:协同对同一资源单独访问
- 信号量:限制有限用户资源
-
上下文切换
CPU:从一个线程或进程切换到另一个线程或进程
内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
- 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
- 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。
上下文切换通常是计算密集型的
尽可能地避免不必要的上下文切换
操作系统层面的线程什么周期
五种: 初始状态、可运行状态、运行状态、休眠状态和终止状态
- 初始状态:已创建,为分配CPU,语音意义上的创建,而非操作系统层面
- 可运行状态:线程可以分配CPU执行,操作系统层面的线程已被创建,可以分配CPU了
- 运行状态:被分配的CPU 线程
- 休眠状态:调用阻塞的api,等待,就会使休眠状态,释放CPU使用权,休眠的线程永远没有机会获得CPU使用权,需要等待事件通知线程进入可运行状态
- 终止状态:执行完成或异常,终止状态的线程不会切换到其他状态,等待销毁?
Java中吧可运行状态与运行状态合并了
但在操作系统调度有用,JVM不关注,
2:java线程详解
2.1 Java线程的实现方式
方式一:使用 Thread类或继承Thread类
// 创建线程对象Thread t = new Thread() {public void run() {// 要执行的任务}};// 启动线程
方式二:实现 Runnable 接口配合Thread
把【线程】和【任务】(要执行的代码)分开
- Thread 代表线程
- Runnable 可运行的任务(线程要执行的代码)
方式三:使用有返回值的 CallableRunnable runnable = new Runnable() {public void run(){// 要执行的任务}};// 创建线程对象Thread t = new Thread( runnable );// 启动线程
方式四:使用 lambdaclass CallableTask implements Callable<Integer> {@Overridepublic Integer call() throws Exception {return new Random().nextInt();}}//创建线程池ExecutorService service = Executors.newFixedThreadPool(10);//提交任务,并用 Future提交返回结果
总结其实都是 Thread#startnew Thread(() -> System.out.println(Thread.currentThread().getName())).start();
Thread.start 与run的区别
2.2 Java线程的实现原理
Java线程属于内核级线程
涉及Thread#start()的源码了
Thead#start方法详解
https://www.processon.com/view/link/5f02ed9e6376891e81fec8d5
总结:
基于操作系统原生线程模式来实现,一条Java线程映射一条轻量级线程
内核线程:内核态
用户级线程:操作系统不知道他的存在
协程
不归操作系统内核管理,由用户程序控制,内核不可见
协程优势:
- 不涉及内核,减少上下文切换,效率高
- 协程大小1K,数量更多
- 不需要多线程锁,只有一个线程,
注意: 协程适用于被阻塞的,且需要大量并发的场景(网络io)。不适合大量计算的场景
2.3 线程的调度机制
一:协同式线程调度
线程时间由线程本身控制,线程完成后,会通知系统切换到另一个线程
好处:
实现简单:,切换对线程可知,
坏处:
线程执行时间不可控,万一有问题会一直阻塞
二:抢占式线程调度
每个线程由系统分配,线程的切换不由线程本身来决定,
执行时间可控,也不会造成一个线程堵塞
Java中,Thread.yield()可以让出执行时间,但无法获取执行时间
优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统
2.4 Java线程的生命周期
Java中线程六种状态,分别是:
- new :初始化
- runnable:可运行 + 运行状态
- BlockEd : 阻塞
- waiting : 无时限的等待
- TIMED_WAITING :有时限的等待
- TERMINATED:终止
操作系统层面: BOLKED,WATING, timed_waiting是一种状态, 休眠
都是永远没有CPU的使用权,需要事件通知他
JavaThread角度JVM定义的状态
操作系统角度定义的
2 .5 Thread的常用方法
sleep :
- 会从running 进入 timed_waiting ,不会释放锁
- 其他线程可以使用interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
- 睡眠结束后的线程未必会立刻得到执行
-
yield:
会释放CPU ,从running进入runnable,但不会释放锁对象
- main方法yield 不会让出去,它优先级最高
-
join:
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景
public class ThreadJoinDemo {public static void main(String[] sure) throws InterruptedException {Thread t = new Thread(new Runnable() {@Overridepublic 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
interrupt
Java线程的中断机制
- 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() {@Overridepublic 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=truet1.interrupt();}
使用中断机制时一定要注意是否存在中断标志位被清除的情况public class StopThread implements Runnable {@Overridepublic 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可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位
wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位
2. 6 java 线程间通信
volatile
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。
等待唤醒(等待通知)机制
1: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() {@Overridepublic 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() {@Overridepublic void run() {if (flag){synchronized (lock){if (flag){lock.notify();System.out.println("notify .......");flag = false;}}}}}).start();}
2: park &unpark
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{@Overridepublic 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();// 将输出流和输入流进行连接,否则在使用时会抛出IOExceptionout.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;}@Overridepublic void run() {int receive = 0;try {while ((receive = in.read()) != -1) {System.out.print((char) receive);}} catch (IOException ex) {}}}
Thread.join
理解为线程合并,一个线程调用另一个线程的join,当前会阻塞,等待被调用的join线程完成才会继续
可以保证顺序,就变成了串行,而非并行
join的实现其实是基于等待通知机制的
3:扩展
3.1内核模式 用户模式
https://note.youdao.com/ynoteshare/index.html?id=a12216347600326ac714d4539db03585&type=note&_time=1647267913299
线程之所以重,涉及到了内核
协程轻,仅在用户层
3.2 一些命令
linux系统可以通过命令统计CPU上下文切换数据
vmstat 1 #可以看到整个操作系统每1秒CPU上下文切换的统计
cs列就是CPU上下文切换的统计
造成CPU上下文切换的操作:
- 线程,经常切换
- 系统调用
- 中断
pidstat ‐w ‐p 5598 1 # 显示进程5598每一秒的切换情况
常用的参数:-u 默认参数,显示各个进程的 CPU 统计信息-r 显示各个进程的内存使用情况-d 显示各个进程的 IO 使用-w 显示各个进程的上下文切换-p PID 指定 PID
cswch表示主动切换
- 从进程的状态信息中查看
cat /proc/5598/status 查看进程的状态信息
voluntary_ctxt_switches: 40469351nonvoluntary_ctxt_switches: 2268
查看进程线程的方法
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 #查看系统是使用哪种线程实现
问题:
1.CAS涉及到用户模式到内核模式的切换吗?
2.为什么说创建Java线程的方式本质上只有一种?Java线程和go语言的协程有什么区别?
3.如何优雅的终止线程?
4.Java线程之间如何通信的,有哪些方式?
