文章目录
- 前言
- 线程
- 线程优先级和时间片
- 创建多线程及运行
- 线程的状态
- 进程
- 查看进程的命令
- 进程的通信方式
- 线程和进程的区别
- 从关系上
- 疑问集锦
前言
- 并发
1、并发是指在同一时间段内,计算机系统能够处理多个任务的能力。
2、在并发编程中,我们可以理解为多个线程竞争同一个资源
3、操作系统中有 线程控制块 和 进程控制块,用来记录当前时间片执行到的程序位置 - 并行
多个线程或者进程各自执行各自的任务,互不干扰。
(并行不会出现竞争同一个资源,在此我们不多做讨论,后续文章均已并发为主) - 高速缓存
对于频繁重复访问的数据,我们可以存储在高速缓存中,这样可以释放总线,提高计算速度
高速缓存的容量比较小,当容量不足时会往内存中写回数据(具体的写回时间不确定)。
线程
- 定义:
- 是一个指令流(指令和数据的不断入栈和出栈),即程序的执行顺序,是进程中真正执行的基本单位
- 单线程:
- 只有一个虚拟机栈,程序从上到下依次执行
- 单线程是作为局部变量存在的
- 多线程:
- 1、多个指令流,相当于创建多个虚拟机栈
- 2、继承Thread后不再是局部变量,变成一个线程
线程优先级和时间片
- 和操作人员接触多的指令,它的优先级会更高(即更容易获得操作系统的时间片)
- 如果时间片结束时未完成所有任务,则记录执行到的位置,然后任务重新会进入就绪态
- 如果时间片未到时间但已完成任务,则剩余的时间片会执行其他任务
创建多线程及运行
-
线程代码:
public class Test { public static void main(String[] args) { int[] arr = {0}; Thread threadA = new Thread(){ @Override public void run() { System.out.println("theadA启动 ~~ "); for(int i=0;i<1000000;i++){ arr[0]++; } System.out.println("threadA下的arr[0] = " + arr[0]); } }; Thread threadB = new Thread(){ @Override public void run() { System.out.println("theadB启动 ~~ "); for (int i = 0; i < 1000000; i++) { arr[0]++; } System.out.println("threadB下的arr[0] = " + arr[0]); } }; threadA.start(); // threadA进入线程就绪队列 threadB.start(); // threadB进入线程就绪队列 System.out.println(arr[0]); } }
-
代码说明:
1、线程.start()是进入线程就绪队列,新建线程后需要启用start进入就绪队列
2、当前有三个线程:threadA、threadB 和main主线程 -
运行结果(打印结果是不确定的):
第一种可能结果:
第二种可能结果:
第三种可能结果:
第四种可能结果:
-
结果分析:
- 1、threadA和threadB是互相独立的,执行顺序是不确定的。
线程内部的代码时按顺序来执行的
- 2、主线程main优先执行的概率较大(注意只是概率哦)
- 3、先进入就绪队列(如threadA)的先执行的概率大
可以观察到上边几种结果,threadA先打印的次数比threadB要多
- 4、线程被选中后,分配的时间片时长是不固定的
我们以 结果一 和 结果三 来进行分析:
(1)线程threadA被选中,分配了时间片运行:打印开始启动后对arr[0]进行增加
(2)arr[0]增到到”35932“后,时间片结束,开始运行 主线程main ,打印”35932“
(3)仅 System.out.println(arr[0]) 是用不完分配给主线程main的时间片的,分配给主线程main的时间片用不完,当打印完后,就会结束主线程main继续执行下一个,要么选中threadA要么选中threadB
(4)【选中threadB】,分配时间片:打印”启动“后对arr[0]进行增加
(5)这里需要循环100w次,注意这里threadA和threadB中arr[0]增加是threadA和threadB交替着来的
(6)当循环都完成的时候,打印出每个线程下的结果:可能先是threadA也可能先是threadB
- 1、threadA和threadB是互相独立的,执行顺序是不确定的。
-
线程不安全:
如果线程安全:无论哪个线程,最后打印的结果应该是200w
(但实际结果并不这样)从严格物理上来讲,同一时刻只有一个线程指令通过总线来操作内存。
而内存同一时刻也只能由一个线程来操作。因为有高速缓存:
(1)当threadA读取到arr[0]=0,然后开始计算
(2)threadA操作的数据存储在高速缓存中,没有往回更新时,时间片结束
(3)threadB读取arr[0],此时arr[0]=0,然后开始计算
(4)当threadA计算到50w时,高速缓存更新arr[0],arr[0] = 50w
(5)然后threadB计算到100w时,高速缓存更新arr[0],直接覆盖arr[0] = 100w
(6)threadA再次读取到arr[0]=100w,执行剩下的50w次,最后计算结果arr[0]=150w,而不是200w
(7)这里有两个极限:最小是2,最大是200w
线程的状态
- 新建
新建的线程并不会进入就绪态,需要启动
- 就绪态
进入就绪队列的顺序是有先后的,但是执行的时候不会按照进入的顺序来。
即操作系统选中哪个线程先执行是不确定的
先进入就绪态先执行的概率会大,但是不是绝对的 - 运行态:被cpu选中
1、运行中,当前时间片内未完成线程任务,当时间片结束时,通过线程控制块记录数据,线程重新进入就绪态
2、在时间片内运行结束,进入死亡态 - 死亡态
线程运行完毕
- 阻塞态:线程竞争加锁的资源失败,会进入阻塞队列
当竞争的资源锁一旦被释放后,该线程会重新进入就绪队列参与竞争
- 等待态:
运行中的线程可以自己进入等待队列。
进入等待队列中的线程,如果没有通知,则会一直留在队列,操作系统不会选中该任务 - 睡眠态:
此处不细讲,以下为两个进入睡眠态的方法:
- Thread.sleep(时长) 方法进入休眠的线程不会释放持有的锁
- object.wait()方法进入休眠的线程会释放锁,但是唤醒需要其他线程调用了该对象的 notify() 或 notifyAll() 方法
进程
- 正在运行的程序,我们称为进程。
(如我们打开的chrome浏览器,QQ音乐,微信,均被称为进程) - QQ音乐这个线程包含了代码、账号信息登数据以及内存空间
- 如果我们选择了一个音乐播放,那么这是一个线程
- 如果我们让播放器显示桌面歌词,这也是一个线程
- 喜欢一个音乐,点击收藏,这也是一个线程
查看进程的命令
- windows
查看所有的进程:tasklist
杀死进程:taskkill /F /PID pid号 - linux
查看所有的进程:ps -ef
查看指定的进程:
杀死进程:kill -9 pid号
进程的通信方式
-
管道:
原理:内核中的一个缓存。是一种半双工的通信方式,即一个进程的输出作为另一个进程的输入
(单工:一方只能发送,其他只能接受,做不到信息互动。可以想象一下收音机)
(半双工:需要等一方发送完毕,另一方才可以发送。可以想象一下对讲机)
(双工:可以等一方发送完毕,也可以同时发送。可以想象一下升级的电话通话)优点:最简单
缺点:效率最差,不适合进程间频繁的交换数据 -
FIFO:
与管道类似,但是允许不想关的进程进行通信,且可以跨终端会话
-
消息队列:
允许进程以消息的形式进行通信,消息可以按照一定的顺序进行传递
优点:可以边发送边接收,不需要等待完整的数据
缺点:
(1)每个消息体有最大长度的限制,队列所包含消息体的总长度也有上限
(2)消息队列通信存在用户态和内核态之间的数据拷贝问题,耗性能 -
共享内存区
原理:不同的进程拿出一块虚拟内存空间,映射到相同的物理内存空间。这样一个进程写入的东西,另一个进程马上就能够看到,不需要进行拷贝。
优点:解决了消息队列存在
缺点:如果有多个进程网内存写入数据,后写数据的会对前边的数据有覆盖 -
信号量
- 原理
基于共享内存的多进程不安全的情况,增加一个保护机制-信号量。
信号量本质上是一个整型的计数器,用于实现进程间的互斥和同步。 - 过程:信号量S初始为1;P操作是让信号量-1;V操作是让信号量+1
1、进程A访问共享内存前执行P操作,更改信号量:S=S-1=0
2、进程A访问共享内存
3、进程B访问共享内存前执行P操作,更改信号量:S=S-1=0-1=-1,临界资源被占用
4、进程B被阻塞,进入阻塞队列
5、进程C访问共享内存前执行P操作,更改信号量:S=S-1=-1-1=-2,临界资源被占用
6、进程C被阻塞,进入阻塞队列
7、进程A访问完毕,执行V操作,更改信号量:S=S+1=-2+1=-1
8、唤醒阻塞队列中的进程B
信号量S=-1
1、进程B访问共享内存
2、进程B访问完毕,执行V操作,更改信号量:S=S+1=0
3、唤醒阻塞队列中的进程C
1、进程C访问共享内存
2、进程B访问完毕,执行V操作,更改信号量:S=S+1=1
3、结束 - 原理
-
信号量互斥:当多进程访问共享内存时,信号量<1时不允许其他进程访问;
-
信号量同步:当信号量恢复成初始值1时会唤醒其他进程来访问数据
线程和进程的区别
从关系上
1、进程是操作系统资源分配的基本单位,线程是程序执行的最小单位
2、每个进程都至少拥有一个线程来执行地址空间中的代码
3、进程是线程的容器,单个进程可以包含若干个线程,且这些线程可以同时执行进程地址空间中的代码
4、进程是安全的;多线程时会出现争抢同一个资源的情况,即多线程更改同一变量会出现问题,故线程是不安全的
5、多个线程共享进程的堆和方法区,但是每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。
疑问集锦
- 线程为什么不能独立存在
因为线程是依赖于进程存在的。线程是进程内的执行单元,自己本身并不拥有系统资源,线程使用本身的资源
线程如果允许独立存在,系统需要为它分配资源和空间,那么起始就等同于一个进程了。而线程是进程的轻量级,可以快速创建和销毁。 - 线程之间为什么会交替运行
因为有时间片轮转
(分时操作系统中,windows的时间片分配不是固定时间,linux中时间片分配时固定的时间) - 线程安全是什么?
同一个进程内的资源是共享给若干个线程的,当多个线程更改同一块内存区域时,会导致该内存的数据互相覆盖
- 怎么保证线程安全?
同步:使用synchronize关键字 或 reentrantlock
不可变对象:创建值无法更改的对象,如增加关键字final
原子类:使用atomicinteger等原子类执行原子操作
volatile关键字:确保变量在多线程间可见
内存屏障:通过thread.memorybarrier()方法强制执行内存访问顺序 - 为什么进程开销比线程大
进程之间资源不共享,每个进程都有自己独立的地址空间和系统资源,占用的内存大;线程之间资源共享,同一个进程中的线程共享进程的地址空间和系统资源,占用的内存小。