并发基础
进程与线程
进程
- 程序由指令和数据组成,指令的运行和数据的读写,必须将指令加载到CPU,数据加载到内存。在指令运行过程中还需要用到磁盘,网络等设备。进程就是用来加载指令,管理内存,管理IO的。
- 进程是资源分配的最小单位。
线程
- 一个进程内可以分为一到多个线程。
- 线程是CPU调度的基本单位。一个线程就是一个指令流,将指令流中的一条条指令以一定顺序交给CPU执行。
进程和线程的对比
- 进程基本上相互独立,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间,供其内部的线程共享
- 进程间通信较为复杂
- 同一台计算机的进程通信称为IPC(Inter-process communication),todo 有哪些
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,如HTTP
- 线程通信相对简单,因为它们共享进程内的内存,多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般比进程上下文切换低
协程
一个进程可以产生许多线程,每个线程有自己的上下文,当我们在使用多线程的时候,如果存在长时间的 I/O 操作,线程会一直处于阻塞状态,这个时候会存在很多线程处于空闲状态,会造成线程资源的浪费。这就是协程适用的场景。
协程,其实就是在一个线程中,有一个总调度器,对于多个任务,同时只有一个任务在执行,但是一旦该任务进入阻塞状态,就将该任务设置为挂起,运行其他任务,在运行完或者挂起其他任务的时候,再检查待运行或者挂起的任务的状态,使其继续执行。
协程的方式更多用来做阻塞密集型(比如 I/O)的操作,计算密集型的还是使用线程更加合理。
Java 官方并没有协程库。但是Java社区提供了一个优秀的库 Quasar。
并行与并发
一个CPU轮流做多件事就是并发。
多个CPU分别做多件事就是并行。
Linux 查看进程和线程状态
ps -fe
查看所有进程ps -fT -p <PID>
查看某个进程(PID)的所有线程kill
杀死进程top
按大写H切换是否显示线程top -H -p <PID>
查看某个进程的所有线程
Java命令
jps
查看所有Java进程jstack <PID>
查看某个Java进程(PID)的所有线程状态jconsole
查看某个Java进程中线程的运行情况(图形界面)
栈与栈帧(Frames)
- 每个栈由多个栈帧(Frame)组成,对应这个每次方法调用锁占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行得方法
如上图所示,程序进入main方法,生成main栈帧,main方法调用method1,生成method1栈帧,右侧局部变量表,分配了 x,y的内存,同理后面又调用了method2,生成了method2栈帧。当method2执行完,method2栈帧内存释放,以此类推,根据调用栈依次释放栈帧内存。
对应的内存分布图如下:
- 局部变量是线程安全的,根据这个思路实现的线程安全技术叫线程封闭,即仅在单线程内访问数据
如数据库连接池,数据库连接池通过线程封闭技术,保证一个 Connection 一旦被一个线程获取之后,在这个线程关闭 Connection 之前的这段时间里,不会再分配给其他线程,从而保证了 Connection 不会有并发问题
线程上下文切换(Thread Context Switch)
CPU不再执行当前线程,转而执行另一个线程的代码:
- 线程的CPU时间片用完了
- 垃圾回收
- 有更高优先级的线程需要执行
- 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法,主动放弃线程自己对CPU的使用权
当Context Switch发生时,需要操作系统来执行保存当前线程的状态,并恢复另一个线程的状态,JVM中对应的就是程序计数器(Program Counter Register),它的作用是记住下一条JVM指令的执行地址,是线程私有的
- 线程的状态包含程序计数器,虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch频繁发生会影响性能