1.线程的基础知识

进程:

操作系统分配资源的单元
哪些资源:指令,内存管理,IO管理,磁盘资源,CPU时间片

线程: 进程中的实体

CPU调度的基本单元;一个指令流
轻量级进程

区别:

  1. 进程基本相互独立,线程存在于进程内,
  2. 进程拥有共享资源,供内部线程共享
  3. 进程通信复杂
    同一机器的通信
    不同机器的通信:网络

进程间的通信方式:

  1. 管道:用于父子进程间,还有有名管道,允许无亲缘关系进程的通信
  2. 信号:软件层次对中断机制的模拟,比较复杂,用在通知进程事件发生,与中断请求效果一样
  3. 消息队列:消息的链接表:克服信号量有限的缺点,
  4. 共享内存:最有用的方式,依靠同步操作,互斥锁,信号量
  5. 信号量:进程间或同一进程间不同线程之间的同步互斥手段
  6. 套接字:用户网络间的不同机器的通信

线程同步互斥

线程间的制约关系,一个线程依赖另一个线程的消息
线程互斥:线程对共享的资源,在访问时的排他性,一种特殊的线程同步
四种线程同步互斥的控制方法

  1. 临界区 :串行访问的共享资源
  2. 互斥量:协同对同一资源单独访问
  3. 信号量:限制有限用户资源
  4. 事件:线程通知的事件

    上下文切换

    CPU:从一个线程或进程切换到另一个线程或进程
    内核(即操作系统的核心)对CPU上的进程(包括线程)执行以下活动

  5. 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方

  6. 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
  7. 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。

上下文切换通常是计算密集型的
尽可能地避免不必要的上下文切换

操作系统层面的线程什么周期

五种: 初始状态、可运行状态、运行状态、休眠状态和终止状态
image.png

  1. 初始状态:已创建,为分配CPU,语音意义上的创建,而非操作系统层面
  2. 可运行状态:线程可以分配CPU执行,操作系统层面的线程已被创建,可以分配CPU了
  3. 运行状态:被分配的CPU 线程
  4. 休眠状态:调用阻塞的api,等待,就会使休眠状态,释放CPU使用权,休眠的线程永远没有机会获得CPU使用权,需要等待事件通知线程进入可运行状态
  5. 终止状态:执行完成或异常,终止状态的线程不会切换到其他状态,等待销毁?

Java中吧可运行状态与运行状态合并了
但在操作系统调度有用,JVM不关注,


2:java线程详解

2.1 Java线程的实现方式

方式一:使用 Thread类或继承Thread类

  1. // 创建线程对象
  2. Thread t = new Thread() {
  3. public void run() {
  4. // 要执行的任务
  5. }
  6. };
  7. // 启动线程

方式二:实现 Runnable 接口配合Thread
把【线程】和【任务】(要执行的代码)分开

  • Thread 代表线程
  • Runnable 可运行的任务(线程要执行的代码)
    1. Runnable runnable = new Runnable() {
    2. public void run(){
    3. // 要执行的任务
    4. }
    5. };
    6. // 创建线程对象
    7. Thread t = new Thread( runnable );
    8. // 启动线程
    方式三:使用有返回值的 Callable
    1. class CallableTask implements Callable<Integer> {
    2. @Override
    3. public Integer call() throws Exception {
    4. return new Random().nextInt();
    5. }
    6. }
    7. //创建线程池
    8. ExecutorService service = Executors.newFixedThreadPool(10);
    9. //提交任务,并用 Future提交返回结果
    方式四:使用 lambda
    1. new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
    总结其实都是 Thread#start

Thread.start 与run的区别


2.2 Java线程的实现原理

Java线程属于内核级线程
涉及Thread#start()的源码了
Thead#start方法详解
https://www.processon.com/view/link/5f02ed9e6376891e81fec8d5
总结:
1647347031(1).png

基于操作系统原生线程模式来实现,一条Java线程映射一条轻量级线程
内核线程:内核态
用户级线程:
操作系统不知道他的存在
image.png

协程

不归操作系统内核管理,由用户程序控制,内核不可见
协程优势:

  1. 不涉及内核,减少上下文切换,效率高
  2. 协程大小1K,数量更多
  3. 不需要多线程锁,只有一个线程,

注意: 协程适用于被阻塞的,且需要大量并发的场景(网络io)。不适合大量计算的场景

2.3 线程的调度机制

一:协同式线程调度

线程时间由线程本身控制,线程完成后,会通知系统切换到另一个线程
好处:
实现简单:,切换对线程可知,
坏处:
线程执行时间不可控,万一有问题会一直阻塞

二:抢占式线程调度
每个线程由系统分配,线程的切换不由线程本身来决定,
执行时间可控,也不会造成一个线程堵塞
Java中,Thread.yield()可以让出执行时间,但无法获取执行时间
优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统

2.4 Java线程的生命周期

Java中线程六种状态,分别是:

  1. new :初始化
  2. runnable:可运行 + 运行状态
  3. BlockEd : 阻塞
  4. waiting : 无时限的等待
  5. TIMED_WAITING :有时限的等待
  6. TERMINATED:终止

操作系统层面: BOLKED,WATING, timed_waiting是一种状态, 休眠
都是永远没有CPU的使用权,需要事件通知他
image.png
JavaThread角度JVM定义的状态
image.png
操作系统角度定义的
image.png

2 .5 Thread的常用方法

sleep :

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

    yield:

  • 会释放CPU ,从running进入runnable,但不会释放锁对象

  • main方法yield 不会让出去,它优先级最高
  • 具体实现依赖操作系统的任务调度器

    join:

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

    1. public class ThreadJoinDemo {
    2. public static void main(String[] sure) throws InterruptedException {
    3. Thread t = new Thread(new Runnable() {
    4. @Override
    5. public void run() {
    6. System.out.println("t begin");
    7. try {
    8. Thread.sleep(5000);
    9. } catch (InterruptedException e) {
    10. e.printStackTrace();
    11. }
    12. System.out.println("t finished");
    13. }
    14. });
    15. long start = System.currentTimeMillis();
    16. t.start();
    17. //主线程等待线程t执行完成
    18. t.join();
    19. System.out.println("执行时间:" + (System.currentTimeMillis() - start));
    20. System.out.println("Main finished");
    21. }

    stop

    不建议使用,会放锁,数据回不一致

    interrupt

    Java线程的中断机制

  • interrupt(): 将线程的中断标志位设置为true,不会停止线程
  • isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
  • Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle
    1. public class ThreadInterruptTest {
    2. static int i = 0;
    3. public static void main(String[] args) {
    4. System.out.println("begin");
    5. Thread t1 = new Thread(new Runnable() {
    6. @Override
    7. public void run() {
    8. while (true) {
    9. i++;
    10. System.out.println(i);
    11. //Thread.interrupted() 清除中断标志位
    12. //Thread.currentThread().isInterrupted() 不会清除中断标志位
    13. if (Thread.currentThread().isInterrupted() ) {
    14. System.out.println("=========");
    15. }
    16. if(i==10){
    17. break;
    18. }
    19. }
    20. }
    21. });
    22. t1.start();
    23. //不会停止线程t1,只会设置一个中断标志位 flag=true
    24. t1.interrupt();
    25. }
    利用中断机制优雅的停止线程
    1. public class StopThread implements Runnable {
    2. @Override
    3. public void run() {
    4. int count = 0;
    5. while (!Thread.currentThread().isInterrupted() && count < 1000) {
    6. System.out.println("count = " + count++);
    7. }
    8. System.out.println("线程停止: stop thread");
    9. }
    10. public static void main(String[] args) throws InterruptedException {
    11. Thread thread = new Thread(new StopThread());
    12. thread.start();
    13. Thread.sleep(5);
    14. thread.interrupt();
    15. }
    使用中断机制时一定要注意是否存在中断标志位被清除的情况
    sleep可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位
    wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位

2. 6 java 线程间通信

volatile

volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。

等待唤醒(等待通知)机制

1:wait notify
调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒。

  1. public class WaitDemo {
  2. private static Object lock = new Object();
  3. private static boolean flag = true;
  4. public static void main(String[] args) {
  5. new Thread(new Runnable() {
  6. @Override
  7. public void run() {
  8. synchronized (lock){
  9. while (flag){
  10. try {
  11. System.out.println("wait start .......");
  12. lock.wait();
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. System.out.println("wait end ....... ");
  18. }
  19. }
  20. }).start();
  21. new Thread(new Runnable() {
  22. @Override
  23. public void run() {
  24. if (flag){
  25. synchronized (lock){
  26. if (flag){
  27. lock.notify();
  28. System.out.println("notify .......");
  29. flag = false;
  30. }
  31. }
  32. }
  33. }
  34. }).start();
  35. }

2: park &unpark
LockSupport:
jdk用来实现线程阻塞,和唤醒的工具,调用park等待许可,UNpark为指定线程提供许可,

  1. public class LockSupportTest {
  2. public static void main(String[] args) {
  3. Thread parkThread = new Thread(new ParkThread());
  4. parkThread.start();
  5. System.out.println("唤醒parkThread");
  6. LockSupport.unpark(parkThread);
  7. }
  8. static class ParkThread implements Runnable{
  9. @Override
  10. public void run() {
  11. System.out.println("ParkThread开始执行");
  12. LockSupport.park();
  13. System.out.println("ParkThread执行完成");
  14. }
  15. }

管道输入输出流(了解)

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

  1. public class Piped {
  2. public static void main(String[] args) throws Exception {
  3. PipedWriter out = new PipedWriter();
  4. PipedReader in = new PipedReader();
  5. // 将输出流和输入流进行连接,否则在使用时会抛出IOException
  6. out.connect(in);
  7. Thread printThread = new Thread(new Print(in), "PrintThread");
  8. printThread.start();
  9. int receive = 0;
  10. try {
  11. while ((receive = System.in.read()) != -1) {
  12. out.write(receive);
  13. }
  14. } finally {
  15. out.close();
  16. }
  17. }
  18. static class Print implements Runnable {
  19. private PipedReader in;
  20. public Print(PipedReader in) {
  21. this.in = in;
  22. }
  23. @Override
  24. public void run() {
  25. int receive = 0;
  26. try {
  27. while ((receive = in.read()) != -1) {
  28. System.out.print((char) receive);
  29. }
  30. } catch (IOException ex) {
  31. }
  32. }
  33. }

Thread.join

理解为线程合并,一个线程调用另一个线程的join,当前会阻塞,等待被调用的join线程完成才会继续
可以保证顺序,就变成了串行,而非并行
join的实现其实是基于等待通知机制的


3:扩展

3.1内核模式 用户模式

https://note.youdao.com/ynoteshare/index.html?id=a12216347600326ac714d4539db03585&type=note&_time=1647267913299
线程之所以重,涉及到了内核
协程轻,仅在用户层

3.2 一些命令

通过命令查看CPU上下文切换情况

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

vmstat 1 #可以看到整个操作系统每1秒CPU上下文切换的统计
1647344683(1).png
cs列就是CPU上下文切换的统计
造成CPU上下文切换的操作:

  1. 线程,经常切换
  2. 系统调用
  3. 中断

pidstat ‐w ‐p 5598 1 # 显示进程5598每一秒的切换情况

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

1647344885(1).pngcswch表示主动切换

  • 从进程的状态信息中查看

cat /proc/5598/status 查看进程的状态信息

  1. voluntary_ctxt_switches: 40469351
  2. nonvoluntary_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线程之间如何通信的,有哪些方式?