一、基础知识

线程与进程概念

进程

  1. - 程序由指令和数据组成,但指令运行,数据读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中

还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的 。

  1. - 当一个程序被运行,从磁盘加载程序的代码至内存,这时就开启了一个进程。
  2. - 进程可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只

能启动一个实例进程(例如网易云音乐、360 安全卫士等)。

  1. - 操作系统以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。

线程

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

进程与线程的区别

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

进程间通信的方式

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

线程的同步与互斥

线程同步
指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。(例如阻塞队列,当队列为空,等待add信号,否则take阻塞)
线程互斥
指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
线程同步互斥的控制方法

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

上下文切换(Context switch)

是指CPU从一个进程或线程到另一个进程或线程的切换。
寄存器是CPU内部的一小部分非常快的内存(相对于CPU外部较慢的RAM主内存),它通过提供对常用值的快速访问来加快计算机程序的执行。
程序计数器是一种专门的寄存器,指示CPU在其指令序列中的位置,并保存正在执行的指令的地址或下一条要执行的指令的地址

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

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

上下文切换只能在内核模式下发生。内核模式是CPU的特权模式,其中只有内核运行,才有权对所有内存位置和所有其他系统资源的访问。其他程序(包括应用程序)最初在用户模式下运行,但它们可以通过系统调用运行部分内核代码。

线程模型

KLT模型、ULT模型。
JVM使用的KLT模型,Java线程与OS线程保持1:1的映射关系(一个java线程对应一个操作系统的线程),线程具体使用(创建、销毁、调度)由操作系统管理
ULT模型:线程操作由自己进行控制,实现起来比较复杂。

Kernel Mode

在内核模式下,执行代码可以完全且不受限制地访问底层硬件。它可以执行任何CPU指令和引用任何内存地址。内核模式通常为操作系统的最低级别、最受信任的功能保留。内核模式下的崩溃是灾难性的;他们会让整个电脑瘫痪。

User Mode

在用户模式下,执行代码不能直接访问硬件或引用内存。在用户模式下运行的代码必须委托给系统api来访问硬件或内存。由于这种隔离提供的保护,用户模式下的崩溃总是可恢复的。在您的计算机上运行的大多数代码将在用户模式下执行。

切换到内核模式的几种情况:

  1. 1. **系统调用**。
  2. 1. **异常事件**。当发生某些预先不可知的异常时,就会切换到内核态,以执行相关的异常事件。
  3. 1. **设备中断**。在使用外围设备时,如外围设备完成了用户请求,就会向CPU发送一个中断信号,此时,CPU就会暂停执行原本的下一条指令,转去处理中断事件。此时,如果原来在用户态,则自然就会切换到内核态

CPU保护模式

x86 CPU提供了四个保护环(protection rings):0、1、2和3。通常只使用0环(内核)和3环(用户)
image.png
Java线程的状态:新建(new)、可执行(runnable)、执行(running)、阻塞(blocked)、结束(terminated)

线程生命周期

操作系统5种线程状态

  1. 1. **新建**:指的是线程已经被创建,但是还**不允许分配 CPU 执行**。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,线程还没有创建。例如:new Thread()
  2. 1. **就绪**:指线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建,所以可以分配 CPU 执行。例如:**调用start方法**,线程进入可执行状态,**等待cpu时间片**。
  3. 1. **执行**:有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态。线程**获得cup时间片**,开始执行。如果丢失时间片,则进入就绪态.(例如:手动调用yield放弃cpu执行权)
  4. 1. **阻塞**:执行中的线程,**等待某些资源**(锁、IO、或手动执行sleep)【获得资源后,可进入就绪态\执行态】
  5. 1. **结束**:线程最终态。意味线程生命周期结束。(线程正常结束、异常结束(例如中断)、手动调用stop、机器宕机(crash))

结束状态不能向任何状态转换,即线程结束后,不能重新调起。(线程结束后,同一个对象,不能在调用start方法)

JAVA种6种线程状态

NEW、RUNNABLE、WAITING、TIMED_WAITING、BLOCKED、TERMINATED
1925ab34-86e6-4502-bf87-977694725755.png

协程

目的
是为了追求最大力度的发挥硬件性能和提升软件的速度。
原理
在某个点挂起当前的任务,并保存栈信息,去执行另一个任务,等完成或达到某个条件是,再还原原来的栈信息并继续执行(整个过程不需上下文切换
Java原生不支持协程,在纯Java代码里需要使用协程,需要引入第三方包。例如:quasar
协程运行在线程之上,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。
协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。
golang语言天然支持协程

二、Java线程

Java线程实现方式

Runnable、Thread、Callable是线程的实现
本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终都会调用Thread#run方法

Java线程调度机制

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式分两种,协同式线程调度和抢占式线程调度
Java线程是抢占式调度
如果希望控制线程执行优先级,可以通过设置线程优先级来完成。
Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。

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

Java线程通信

1.利用volatile关键字
2.等待唤醒机制
wait、notify机制。或者LockSupport
LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用unpark则为指定线程提供“许可”。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序(可以先调用unpark、则调用park不会被阻塞),但要注意连续多次唤醒的效果和一次唤醒是一样的。

Thread中方法

interrupt

通过中断,用户可以自己控制线程优雅结束。
中断此线程。 始终允许当前线程中断自身(本质设置中断标识);非自身中断,将调用此线程的checkAccess方法,这可能会导致抛出SecurityException。
1.如果调用对象类的wait()、join()、sleep()方法被阻塞时,再调用中断方法,则它的中断状态将被清除,并抛出InterruptedException。
2.如果该线程在中断通道上的I/O操作中被阻塞,那么该通道将被关闭,线程的中断状态将被设置,线程将收到java.nio.channels.ClosedByInterruptException。
3.如果该线程在java.nio.channels.Selector中被阻塞,那么该线程的中断状态将被设置,并且它将立即从Selector操作返回,可能带有非零值,就像调用了选择器的唤醒方法一样。
如果前面的条件都不成立(即在正常执行过程中,执行interrupt方法),则设置当前线程的中断状态,线程可以继续正常执行。
中断方法,可以环境park()[无参方法]阻塞的线程
注:
1.中断线程,并不等于被打断线程生命周期结束,仅仅是中断阻塞状态
2.join功能等待当前线程结束。例如main方法中开启新线程,新线程调用join,则仅且当期,新新线程执行结束,才会继续执行main

  1. //无法退出循环,再sleep期间,线程被中断,则抛出异常(而异常被catch),并清除中断标志。
  2. @Override
  3. public void run() {
  4. while (!Thread.currentThread().isInterrupted()) {
  5. try {
  6. Thread.sleep(1);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. }
  11. //必须重新设置中断标识
  12. @Override
  13. public void run() {
  14. while (!Thread.currentThread().isInterrupted()) {
  15. try {
  16. Thread.sleep(1);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. Thread.currentThread().interrupted()
  20. }
  21. }

interrupted

判断线程对象中是否设置了中断标识,如果设置,则返回true,同时清除中断标识,再次调用为false。

isInterrupted

判断线程对象中是否设置了中断标识,如果设置,则返回true,但不会清除中断标识,再次调用仍为true。

wait

wait方法是可中断的方法,
wait方法阻塞当前线程,线程调用某对象的wait方法后,线程会被加入与该对象monitor关联的wait set中,并释放该对象的monitor锁

notify

notify可以唤醒阻塞线程,将线程从wait set中弹出,可以继续争夺monitor锁,notifyAll弹出wait set 中所有线程
wait、notify方法,必须在同步方法中使用,因为wait、notify前提是必须持有同步的方法的monitor的所有权。
同步代码的monitor 必须与执行wait、notify方法的对象一致。即:使用那个monitor进行同步,只能使用那个对象的wait、notify操作。
wait set是虚拟机的一种规范,不同虚拟机可能实现不同
monitor 是操作系统提出来的一种高级原语,操作系统本身并不支持 monitor 机制,实际上,monitor 是属于编程语言的范畴

三、Hook线程

Hook线程只有在收到退出信号时被执行(正常结束也有退出信号),如果kill时,使用-9,则不会执行。进程直接退出。
可以利用Hook做一些资源回收,但不要做耗时操作。
注册Hook线程
Runtime.getRuntime().addShutdownHook(new Thread());