进程、线程、纤程
干货 | 进程、线程、协程 10 张图讲明白了! - 知乎 (zhihu.com)
进程:
进程 是 OS 分配资源的基本单位。一个进程对应一个端口号, OS 会为每一个进程 独立分配一部分资源。通常我们每运行一次程序,都会产生一个进程。
程有三大状态分别是:就绪状态,运行状态,死亡状态。
进程间通信的方式
1.管道:
管道主要包括无名管道和命名管道:管道可用于具有亲缘关系的父子进程间的通信, 有名管道除了具有管道所具有的功能外, 它还允许无亲缘关系进程间的通信。
2.消息队列
消息队列, 是消息的链接表, 存放在内核中。 一个消息队列由一个标识符(即队列 ID) 来标记。具有写权限得进程可以按照一定得规则向消息队列中添加新信息; 对消息队列有读权限得进程则可以从消息队列中读取信息;
3.信号量
信号量(semaphore) 是一个计数器, 可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步, 若要在进程间传递数据需要结合共享内存。
4.信号 signal
信号是一种比较复杂的通信方式, 用于通知接收进程某个事件已经发生。
5.共享内存(Shared Memory)
它使得多个进程可以访问同一块内存空间, 不同进程可以及时看到对方进程中对共享内存中数据得更新。 这种方式需要依靠某种同步操作, 如互斥锁和信号量等。
6.套接字 SOCKET:
socket 也是一种进程间通信机制, 与其他通信机制不同的是, 它可用于不同主机之间的进程通信。
线程:
是 CPU 执行 调度 的基本单位。线程是进程的子任务, 一个进程包含多个线程,线程共享进程的内存空间,它没有独立的内存空间。hospot虚拟机线程和系统的线程一一对应,linux上实际没有线程的概念,线程只是轻量化的进程
什么机制管理了线程的切换
CPU通过时间分配算法管理线程切换
cpu上下文包括:如果站在线程的场景回答就是线程的私有内存.虚拟机栈和程序计数器。
线程间的同步方式
1.信号量
信号量是一种特殊的变量, 可用于线程同步。 它只取自然数值, 并且只支持两种操作:
P(SV):如果信号量 SV 大于 0, 将它减一; 如果 SV 值为 0, 则挂起该线程。
V(SV): 如果有其他进程因为等待 SV 而挂起, 则唤醒, 然后将 SV+1; 否则直接将 SV+1。
2.互斥量
又称互斥锁, 主要用于线程互斥, 不能保证按序访问, 可以和条件锁一起实现同步。当进入临界区时, 需要获得互斥锁并且加锁; 当离开临界区时, 需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程。
3.条件变量
又称条件锁, 用于在线程之间同步共享数据的值。 条件变量提供一种线程间通信机制:当某个共享数据达到某个值时, 唤醒等待这个共享数据的一个/多个线程。 此时操作共享变量时需要加锁。
如何理解线程切换引起了开销这句话
切换的步骤有两步:线程切换,需要保存A的私有内存,加载B线程的私有内存。需要CPU去执行这两个步骤,而这个切换消耗和CPU执行B线程是串行的.总结一下开销就是这保存和加载的时间开销.
引起切换的场景
- 当前执行任务的时间片用完之后,系统CPU正常调度下一个任务
- 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一任务
- 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务
- 用户代码挂起当前任务,让出CPU时间
- 硬件中断
4种锁机制
互斥锁: mutex, 用于保证在任何时刻, 都只能有一个线程访问该对象。 当获取锁操作失败时, 线程会进入睡眠, 等待锁释放时被唤醒
读写锁: rwlock, 分为读锁和写锁。 处于读操作时, 可以允许多个线程同时获得读操作。 但是同一时刻只能有一个线程可以获得写锁。 其它获取写锁失败的线程都会进入睡眠状态, 直到写锁释放时被唤醒。 注意: 写锁会阻塞其它读写锁。 当有一个线程获得写锁在写时, 读锁也不能被其它线程获取; 写者优先于读者(一旦有写者, 则后续读者必须等待, 唤醒时优先考虑写者) 。适用于读取数据的频率远远大于写数据的频率的场合。
自旋锁: spinlock, 在任何时刻同样只能有一个线程访问对象。 但是当获取锁操作失败时,不会进入睡眠, 而是会在原地自旋, 直到锁被释放。 这样节省了线程从睡眠状态到被唤醒期间的消耗, 在加锁时间短暂的环境下会极大的提高效率。 但如果加锁时间过长, 则会非常浪费 CPU资源。
RCU: 即 read-copy-update, 在修改数据时, 首先需要读取数据, 然后生成一个副本, 对副本进行修改。 修改完成后, 再将老数据 update 成新的数据。 使用 RCU 时, 读者几乎不需要同步开销, 既不需要获得锁, 也不使用原子指令, 不会导致锁竞争, 因此就不用考虑死锁问题了。 而对于写者的同步开销较大, 它需要复制被修改的数据, 还必须使用锁机制同步并行其它写者的修改操作。 在有大量读操作, 少量写操作的情况下效率非常高。
单核机器上写多线程程序, 是否需要考虑加锁?
在单核机器上写多线程程序, 仍然需要线程锁。因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序, 仍然存在线程同步的问题。 因为在抢占式操作系统中, 通常为每个线程分配一个时间片, 当某个线程时间片耗尽时, 操作系统会将其挂起, 然后运行另一个线程。如果这两个线程共享某些数据, 不使用线程锁的前提下, 可能会导致共享数据修改引起冲突。
线程间通信的方式
1.临界区:
通过多线程的串行化来访问公共资源或一段代码, 速度快, 适合控制数据访问;
2.互斥量 Synchronized/Lock:
采用互斥对象机制, 只有拥有互斥对象的线程才有访问公共资源的权限。 因为互斥对象只有一个, 所以可以保证公共资源不会被多个线程同时访问。
3.信号量 Semphare:
为控制具有有限数量的用户资源而设计的, 它允许多个线程在同一时刻去访问同一个资源, 但一般需要限制同一时刻访问此资源的最大线程数目。
4.事件(信号), Wait/Notify:
通过通知操作的方式来保持多线程同步, 还可以方便的实现多线程优先级的比较操作
线程状态切换
Thead.State 枚举类
线程状态
获取线程状态
线程状态:JVM管理的,但JVM管理也要通过操作系统
/**
* 线程的六种状态:
* NEW:通过 new 关键字创建
* RUNNABLE:调用 start()
* BLOCKED:被阻塞等待监视器锁定的线程处于此状态
* WAITING:正在等待另一个线程执行特定动作的线程处于此状态。
* TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
* TERMINATED:已退出的线程处于此状态。
*
* @author: BlackSoil
* @date: 2022-11-18 09:43
* @version: 1.0
*/
public class T01_State {
public static void m() {
LockSupport.park();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static synchronized void n() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> m(),"线程1");
// thread 线程执行了 new ,观察 NEW 状态
System.out.println(thread.getName()+" new 之后的状态:"+thread.getState());
// thread 线程执行了 start ,观察 RUNNABLE 状态
thread.start();
System.out.println(thread.getName()+" start 方法之后:"+thread.getState());
// thread 线程执行了 LockSupport.park() ,观察 WAITING 状态
Thread.sleep(500);
System.out.println(thread.getName()+" LockSupport.park() 之后的状态:"+thread.getState());
LockSupport.unpark(thread); //唤醒线程
// thread 线程执行了 sleep(long) ,观察 TIMED_WAITING 状态
Thread.sleep(500);
System.out.println(thread.getName()+" sleep(long) 之后的状态:"+thread.getState());
// thread 执行结束 ,观察 TERMINATED 状态
thread.join();
System.out.println(thread.getName()+" 执行完成的状态:"+thread.getState());
//测试 BLOCKED 状态
Thread thread2 = new Thread(() -> n(),"线程2");
Thread thread3 = new Thread(() -> n(),"线程3");
thread2.start();
Thread.sleep(500);// 让 thread2 先执行
thread3.start();
Thread.sleep(500); //保证 thread 已经执行
System.out.println(thread3.getName()+" 执行完成的状态:"+thread3.getState());
}
}
纤程(fiber):
JVM 运行在用户空间,当它 new 一个 Thread 时,会对应在 OS 中起一个线程(内核空间),所以这叫重量级线程。而纤程则是在用户态有多条。操作系统不调度纤程, 和线程比起来,协程的切换不需要操作系统进行保存和恢复CPU 上下文,自己的缓存数据等,因为所有的协程都存在于同一个线程之中,所以协程的切换只有单纯的 CPU 上下文切换(不涉及内核空间),开销很小。一个线程包含多个纤程,又叫协程,只是协程是一个概念,而纤程是 Windows 系统对协程的一个具体实现。
协程也有一个调度器,但是是被动调度的,也就是说,只有当前运行的协程主动的让出 CPU,调度器才会从协程池中调用下一个协程。这也是协程和上边几种最大的区别:协程的调度方式是合作式(Cooperative),也可以叫做非抢占式(Non-Preemptive);而上边三种都是抢占式调度(Preemptive)。
纤程比线程快在哪?
上面说到,纤程在用户空间,不需要和内核打交道,是语言层面自己实现的,那么为什么和内核打交道的线程效率会比较低呢?这是因为程序一旦涉及用户态到内核态的转换,都要进行以下步骤,效率较低。
并发(concurrency)和并行(parallelism)
并发(concurrency) : 指宏观上看起来两个程序在同时运行, 比如说在单核 cpu 上的多任务。 但是从微观上看两个程序的指令是交织着运行的, 你的指令之间穿插着我的指令, 我的指令之间穿插着你的, 在单个周期内只运行了一个指令。 这种并发并不能提高计算机的性能, 只能提高效率。
并行(parallelism) : 指严格物理意义上的同时运行, 比如多核 cpu, 两个程序分别运行在两个核上, 两者之间互不影响, 单个周期内每个程序都运行了自己的指令, 也就是运行了两条指令。 这样说来并行的确提高了计算机的效率。 所以现在的 cpu 都是往多核方面发展。
进程与程序的联系与区别
① 程序是指令的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程是程序在处理机上的一次执行过程,它是一个动态的概念。
② 程序可以作为一种软件资料长期存在,而进程是有一定生命期的。程序是永久的,进程是暂时的。
注:程序可看作一个菜谱,而进程则是按照菜谱进行烹调的过程。
③ 进程和程序组成不同:进程是由程序、数据和进程控制块三部分组成的。
④ 进程与程序的对应关系:通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包括多个程序。
进程和线程的区别
1.线程依赖于进程而存在。一个线程只能属于一个进程, 而一个进程可以有多个线程, 但至少有一个线程。
2.进程在执行过程中拥有独立的内存单元, 而多个线程共享进程的内存。
(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段,数据段,堆存储 。 但是每个线程拥有自己的栈段, 用来存放所有局部变量和临时变量)
3.进程是资源分配的最小单位, 线程是 CPU 调度的最小单位;
4.进程间不会相互影响;
5.进程在创建、 切换和销毁时开销比较大, 而线程比较小。
(由于在创建或撤消进程时, 系统都要为之分配或回收资源, 如内存空间、 I/o 设备等。 因此, 操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。 类似地,在进行进程切换时,涉及到整个当前进程 CPU 环境的保存以及新被调度运行的进程的 CPU 环境的设置。 而线程切换只须保存和设置少量寄存器的内容, 并不涉及存储器管理方面的操作。 可见,进程切换的开销也远大于线程切换的开销。)
6.进程间通信比较复杂, 而同一进程的线程由于共享代码段和数据段, 所以通信比较容易。
启动线程的三种方式
- 继承Thread,重写run方法
- 实现Runnable接口,重写run方法(或Lambda表达式)
- 通过线程池来启动(实际上也是以上两种之一)
Executor a = Executors.newCachedThreadPool();
a.execute(new Runnable() {
@Override
public void run() {
}
});
Sleep Yield Join 的含义
sleep:睡眠,当前线程暂停一段时间,让出cpu让别人来执行,不会释放锁。睡眠时间到,自动复活
yield:当前线程正在执行的时候,停下来进入ready队列。系统调度算法去决定哪个线程继续运行(有可能还是自己)
join:在自己当前线程加入你调用的join线程,本线程等待等调用的线程运行完了,自己再去执行。(自己join自己没有意义)
死锁
发生的条件
死锁是指两个或两个以上进程在执行过程中, 因争夺资源而造成的下相互等待的现象。 死锁发生的四个必要条件如下:
1.互斥条件: 一个资源每次只能被一个进程使用。
2.请求与保持条件: 一个进程因请求资源而阻塞时, 对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源, 在末使用完之前, 不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
解决死锁的方法
即破坏上述四个条件之一, 主要方法如下:
资源一次性分配, 从而剥夺请求和保持条件
可剥夺资源: 即当进程新的资源未得到满足时, 释放已占有的资源, 从而破坏不可剥夺的条件
资源有序分配法: 系统给每类资源赋予一个序号, 每个进程按编号递增的请求资源, 释放则相反, 从而破坏环路等待的条件
*interrupt*的区别
java---interrupt、interrupted和isInterrupted的区别
1、interrupt()
interrupt方法用于中断线程。调用该方法的线程的状态为将被置为"中断"状态。
注意:interrupt方法是用于中断线程的,调用该方法的线程的状态将被置为"中断"状态。注意:线程中断仅仅是设置线程的中断状态位,不会停止线程。所以当一个线程处于中断状态时,如果再由wait、sleep以及jion三个方法引起的阻塞,那么JVM会将线程的中断标志重新设置为false,并抛出一个InterruptedException异常,然后开发人员可以中断状态位“的本质作用-----就是程序员根据try-catch功能块捕捉jvm抛出的InterruptedException异常来做各种处理,比如如何退出线程。总之interrupt的作用就是需要用户自己去监视线程的状态位并做处理。”
2、interrupted() 和 isInterrupted()
public static boolean interrupted () {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted () {
return isInterrupted( false);
}
1. interrupted 是作用于当前线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程。(线程对象对应的线程不一定是当前运行的线程。例如我们可以在A线程中去调用B线程对象的isInterrupted方法。)
2. 体现在调用的方法的参数上,被调用的方法 isInterrupted(boolean arg)(Thread类中重载的方法)的定义:
private native boolean isInterrupted( boolean ClearInterrupted);
本地方法,通过参数名ClearInterrupted我们就能知道,这个参数代表是否要清除状态位。
如果为true,说明返回线程的状态位后,要清掉原来的状态位(恢复成原来情况)。这个参数为false,就是直接返回线程的状态位。
这两个方法很好区分,只有当前线程才能清除自己的中断位(对应interrupted()方法)
主线程main启动了一个子线程Worker,然后让worker睡500ms,而main睡200ms,之后main调用worker线程的interrupt方法去中断worker,worker被中断后打印中断的状态。下面是执行结果:
public class Interrupt {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new Worker());
t.start();
Thread.sleep(200);
t.interrupt();
System.out.println("Main thread stopped.");
}
public static class Worker implements Runnable {
public void run() {
System.out.println("Worker started.");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Worker IsInterrupted: " +
Thread.currentThread().isInterrupted());
}
System.out.println("Worker stopped.");
}
}
}
执行结果:
- Worker started.
- Main thread stopped.
- Worker IsInterrupted: false
- Worker stopped.
Worker明明已经被中断,而isInterrupted()方法竟然返回了false,为什么呢?
查看了Thread.sleep方法的文档,doc中是这样描述这个InterruptedException异常的:
- InterruptedException - if any thread has interrupted the current thread. The interrupted status of the current thread is cleared when this exception is thrown.
java的中断并不是真正的中断线程,而只设置标志位(中断位)来通知用户。如果你捕获到中断异常,说明当前线程已经被中断,不需要继续保持中断位。
结论:interrupt方法是用于中断线程的,调用该方法的线程的状态将被置为"中断"状态。注意:线程中断仅仅是设置线程的中断状态位,不会停止线程。所以当一个线程处于中断状态时,如果再由wait、sleep以及jion三个方法引起的阻塞,那么JVM会将线程的中断标志重新设置为false,并抛出一个InterruptedException异常,然后开发人员可以中断状态位“的本质作用-----就是程序员根据try-catch功能块捕捉jvm抛出的InterruptedException异常来做各种处理,比如如何退出线程。总之interrupt的作用就是需要用户自己去监视线程的状态位并做处理。”