多线程基础篇
一、预热
1、进程VS线程
1)进程
进程是运行着的程序代码。一个进程只能对应一个程序代码,但一个程序代码可以对应多个进程。直白来说,程序代码是菜谱,进程是厨师烹饪的过程。对于同一个菜谱,可以有多个厨师同时按其制膳。
进程是操作系统分配资源的基本单位,进程之间的资源不共享。两个厨师同时在做清蒸鲈鱼,同一条鲈鱼不可同时被二者使用,因为最后做出来的是两盘,不是一盘。
2)线程
线程是进程的一段执行逻辑,一个进程可以由多个线程组成。厨师在制膳过程中,可能一边在一个锅里熬制浇汤,一边在另一个锅里蒸鱼,可能同时还在案板上制作配菜。
线程是操作系统进行任务调度的基本单位,同一个进程间的线程资源共享,不同进程间的线程资源不共享。
== 进程其实可以看做是承装线程的容器。==
2、并发vs并行
1)并发
并发是指在一段时间内,看起来像多个任务在同时执行。本质上是多个任务使用同一个cpu,但是它们轮流交替使用cpu的时间片,”天下武功唯快不破“,感知上误认为是自己独享cpu。
2)并行
并行是指在同一时间点上,任务在同时执行,即每个任务分别在不同的cpu上执行,是真正的独占cpu。
3、线程和进程的一些分类
主线程和子线程
一个进程只有一个主线程,它是程序的入口。子线程都是由主线程或者由主线程创建的子线程创建的。
父线程和子线程
创建子线程的线程是该子线程的父线程
用户线程和守护线程
用户线程是处理业务逻辑、提供服务的线程。守护线程是为用户线程提供服务的线程,如GC线程、编译优化的C1、C2线程。当全部的用户线程运行结束后,守护线程就会自动终止,无论它执行到什么位置。
僵尸进程和孤儿进程
僵尸进程和孤儿进程都是指子进程。
僵尸进程是指完成了任务但没有被父进程清除掉的子进程。进程存在必留痕,它有存在过的痕迹(进程id、进程状态等)=用于报告父进程的。如何清除僵尸进程?干掉它爹,让收容所init进程
接管就成。
孤儿进程是指自己还没执行完毕父进程就已经执行完毕结束了的子进程。孩子没了爹怎么办?交给收容所init进程
来处理就好。
4、查看进程&线程信息的命令和方式
1)windows
【命令行】
- 查看进程
- 查看all
tasklist
- 查看指定
tasklist | findstr 进程名(java)
- 查看all
- 杀死进程
taskkill /F /PID 进程id
【图形化界面】
邮件——任务管理
2)linux
【命令行】
-
查看进程信息
ps
-
查看所有进程
e
的完备信息f
——ps -ef
或者ps -aux
-
查看具体某个进程
ps -ef | grep java
-
按照cpu占用降序排序
ps -aux --sort=-pcpu
-
按内存占用降序排序
ps -aux --sort=-pmem
前9个ps 。。。| head -10
(表头算一行)
-
-
查看进程动态信息
top
-
查看线程动态信息
top -H -p 进程id
3)java
【命令行】
-
查看所有java进程
jps
-
查看详细某个进程静态的快照信息
jstack 进程id
-
查看某个进程动态信息,通过可视化工具jconsole
二、创建线程的方式
创建线程方式一共有四种
- 继承Thread类——不建议
- 实现Runnable接口——建议
- 实现Callable接口——建议
- 线程池——建议(本篇中不讲解,会单独写一篇)
1、继承Thread类
一般情况下,不采用这种方式创建线程。因为在实际生产开发中,我们需要确保资源类(比如Ticket、User…)的低耦合,不能让每个资源类都继承于Thread类以实现多线程,开发设计需要满足合成复用原则(能实现或聚合,就别继承)。
【使用方法】
1)创建Thread的子类
2) 重写其run()方法
3)线程子类对象调用start()方法启动线程
【提问环节】
为什么不能直接调用线程run方法,非要多次一举的使用start方法?
答:先了解一下start和run的区别(看下面)。
原因1:
由于启动线程涉及到操控操作系统的层面,而java语言不具备这样的能力,所以启动线程需要调用底层本地方法start0来启动,Thread类将其封装在了start方法中。
原因2:
启动线程涉及到栈空间的内存分配,虽然栈本身是线程私有的,但在将其分配给线程之前,它是共享的,存在资源竞争问题,所以需要它是同步方法。
【多此一问】:如果让run方法也调用start0,也是同步,是不是就可以直接调用run来启动线程了?
没错是没错,start方法已经封装好了启动线程的模板逻辑,简单的暴露出run方法让你只关注业务逻辑,你为什么要多此一举。
2、实现Runnable接口
【使用方法】
1)创建一个类实现Runnable接口
2)重写其run()方法
3)将实现接口的类对象作为Thread类构造器参数传入
4)线程对象调用start()方法启动线程
【提问环节】
Runnable中的run和Thread中的run是什么关系?
答:Runnable很简单,就是一个函函数式接口
Thread中run方法调用了其成员变量target的run方法,target的类型是Runnable。
如果将实现Runnable接口的对象作为参数传给Thread,就相当于把它赋值给target,这样调用Thread中run方法等价于调用Runncable中run方法。
3、实现Callable接口
【使用方法】
1)创建实现Callable接口的类
2)实现Callable的call方法
3)将实现Callable的类对象作为参数传递给FutureTask构造器,创建一个FutureTask对象
4)将FutureTask对象作为Thread类构造器参数传入
5)线程对象调用start()方法启动线程
【提问环节】
【问题1】Callable中的call和Thread为什么可以关联起来?
答:通过FurtureTask关联起来的,下面一一道来。
- FurtureTask为什么可以作为参数传递给Thread构造器?
FurtureTask实现了RunnableFurture接口
RunnableFurture实现了Runnable接口和Furture接口,所以等价于FurtureTask实现了Runnable接口,它可以作为Thread构造器参数进行传递
- FurtureTask的run方法如何与Callable中call方法关联?
Callable也是一个简单的函数式接口
在FurtureTask的run方法中调用了Callable的call方法
【问题2】Callable的call方法和Runnable的run方法有什么区别吗?为什么要设置这两种方法来创建线程?
答:区别见下图
有些时候,我们需要在线程中使用其它线程的处理结果,run没有办法满足我们的需求。通过FurtureTask中get方法,可以获取call方法中返回结果,并且get会阻塞当前进程,实现同步。
4、线程池
(后续写完贴链接)
三、线程的状态
1、OS角度
2、JVM角度
Thread类中有个内部枚举类State,专门用于表示线程的状态
共有六种状态:
-
new:刚创建
-
running:对应os中就绪态和运行态
-
waiting:对应os中阻塞——等待java层面其它线程唤醒的阻塞,无线等待
-
timed_waiting:对应os中阻塞——等待java层面其它线程唤醒的阻塞,有限等待的阻塞,如果超时则自动放弃
-
blocked:对应os中阻塞——等待获取java层面的锁机制造成的阻塞
-
terminated:终止
四、线程的常用方法
1、sleep
- 静态方法,可以且仅可以让当前线程阻塞,不能让其它线程阻塞
- 如果获取了锁,沉睡期间并不会释放锁资源
- 底层是调用本地sleep方法实现
- sleep中中断会抛异常
2、join
join()、join(毫秒数)
- 同步方法,利用synchronized实现同步阻塞,使得当前线程必须等待调用join方法的线程执行完毕/等待响应长时间后才可以向下执行
- 底层是调用wait方法实现的
【提问环节】
wait和sleep区别?
答:
1)来源不一样,sleep属于Thread,wait属于Object;
2)对锁的处理不一样,sleep不会释放锁,都睡着了没办法释放,wait会释放锁,它是清醒意识下的行为;
3)使用位置不一样,sleep随意,但wait中能在同步代码块中使用
4)叫醒方式不同,sleep时间到了就醒了,wait可以通过时间到了,或者其它线程唤醒notify()notifyAll()来清醒
为什么join方法中使用wait而不使用sleep?
答:wait比sleep更高效和灵活,看区别就知道。因为wait可以释放锁资源,线程挂起期间可以让其它线程工作,并且由于它叫醒方式多样,不会死板的等待。
3、获取/设置线程名getName/setName
4、设置为守护线程setDaemon
线程.setDaemon(true)
5、获取/设置线程优先级 getPriority/setPriority
- 优先级最小为1,最大为10,默认为5
- 但是优先级只是作为参考,不一定”生效“,线程调度取决于操作系统的调度算法
6、interrupt
- 执行中断,但中断只是设置中断标记,不一定马上就中断了,何时中断取决于os
- 如果标记了中断,再次调用此方法,中断将失效
7、interrupted
- 判断是否发生中断,判断完会清除标记
- 静态方法
8、isInterrupted
- 判断是否发生中断,判断完并不清除标记
- 实例方法
9、获取当前线程Thread.currentThread
五、如何优雅地让线程停止
通过stop
会暴力停止线程,使得后事无法妥善处理,比如锁资源没有释放等会造成死锁,System.exit
则更暴力,明明只是终止线程,却强力停止了整个线程。一张图让你优雅停止: