进程与线程介绍
进程
程序由指令和数据组成,程序工作时,就会将指令加载至CPU,数据加载至内存,进程就是用来加载指令,管理内存,管理IO的。
当程序运行,磁盘加载这个程序的代码至内存,这时就开启了一个进程。
进程可以视为程序的一个实例,大部分程序能同时运行多个实例进程(记事本,浏览器),也有程序只能启动一个实例进程(网易云音乐)。
线程
一个进程之内可以有为一个或多个线程。
一个线程就是一个指令流,指令流中的一条条指令将以一定的顺序交给CPU执行。
Java中,线程作为最小调度单位,进程作为资源分配的最小单位。window中进程是不活动的,只是作为线程的容器。
对比
两者相互独立,进程包含线程,进程拥有的资源供线程共享,进程间通信复杂,线程相对简单
(因为资源共享),线程更轻量,上下文切换成本更低。
并行并发
单核CPU下,单个CPU轮流处理多个线程,这被称为并发。
多核CPU下,多个CPU同时处理多个线程,这被称为并行(并行中也有并发)。
比喻
CPU:一座工厂。
进程:工厂的一个车间(工厂电力有限,一次只能运行一个车间)。
线程:车间的工人(车间内可以有多个工人,车间内的资源为工人们所共享)。
并行:单核CPU下,同一时间只能有一个工人工作,其他工人不能工作,但是工作一段时间后会停止工作,把时间让给另外一个工人。工人轮流工作。
并发:多核CPU下,同一时间多个工人能同时工作。
异步和同步
同步:需要等待结果返回,才能继续运行(如读取文件操作)。
异步:不需要等待结果返回,就能继续运行。
结论:为防止同步操作等待费时,可以新开一个线程进行该费时操作,避免阻塞主线程(如 tomat的异步servlet,ui程序中开新线程防止阻塞ui线程)。
多核CPU在某一情形下,开多个线程并行可以提供效率。单核仍然是轮流执行,可以实现任务间切换,但是上下文切换会耗费一段时间。IO操作不占用CPU,一般拷贝文件使用的是阻塞IO,需要一直等到IO结束,所以才有后面的非阻塞IO和异步IO优化。
创建线程
java程序启动时默认有一个主方法线程(主线程),在主线程之外创建线程可以使用如下方法:
原理:如果传入Thread对象中的Runable对象不为空,Thread对象中的run()方法将会调用 Runnable对象的run()方法,如方法2。方法1则是直接重写了Thread对象中的run方法。无论是哪个方法,都是运行run()方法。推荐方法2,使用Runnable更容易与线程池等高级API配合,让任务类脱离了Thread继承体系,更加灵活。
方法3:
FutureTask配合Thread
FutrueTask能够接受Callable类型的参数,用来处理有返回结果的情况
多个线程运行
交替执行。
谁先谁后,不受我们控制,由底层调度器控制。
线程上下文切换(Thread Context Switch)
上下文切换原因:
线程的CPU时间片用完垃圾回收
有更高优先级的线程需要运行
线程调用了sleep,wait,lock,synchronized,yield,park,join等方法
Context Switch发生时,操作系统会保持当前线程状态,然后恢复另一个线程状态(如下
图),Java中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条jvm指令的执行地址,是线程私有的。
状态包括程序计数器,jvm中每个栈帧的信息(如局部变量,操作数栈,返回地址等)。频繁发生上下文切换会降低性能。
查看进程,线程的方法
windows:
任务管理器 tasklist查看线程 taskkill杀死线程
linux:
ps -fe查看所有进程
ps -fT -p <PID>查看某个进程(PID)的所有线程 kill 杀死进程
top按大写H切换是否显示线程
top -H -p <PID> 查看某个进程的所有线程
Java:
jps命令查看所有Java进程
jstack <PID>查看某个Java进程(PID)的所有线程状态
jconsole来查看某个java进程中线程的运行状况(图形界面)
常见方法
方法汇总
run vs start
调用run()方法是由主线程执行,调用start()方法由新线程执行,启动新线程必须用 start()方法。start()方法不能被多次调用。
sleep
调用sleep方法,线程会从Running状态进入Timed Waiting状态(不能分得CPU时间片)。其他线程可以用interrupt方法打断正在睡眠的方法,sleep方法会抛出interruptedException。
睡眠结束后线程未必会立刻执行。
建议用TimeUnit的sleep代替Thread中的sleep来获得更好的可读性。
sleep应用
防止CPU占用100%
没有用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用sleep和yield把cpu使用权交给其他程序
可以用wait或条件变量达到类似效果
不同的时,后两者需要加锁,并且需要唤醒操作,一般适用于进行同步的场景 sleep适用于无需同步的场景。
yield
调用yield方法,线程会从Running状态进入Runnable就绪状态(可能分得CPU时间片),然后调用其他同优先级的线程。如果这时没有同优先级的线程,将不能保证让当前线程停止。
具体实现依赖于操作系统的任务调度器。
线程优先级
线程优先级会提示(hint)任务调度器优先调度该线程。
CPU繁忙时,优先级高的线程会获得更多时间片;CPU空闲时,优先级几乎没用。
线程的调度并不受线程优先级和yield控制,实际上是由任务调度器决定的。
join
等待某一线程运行结束才能执行接下来的代码,应用于同步。
有参的join方法可以限制等待的最长时间,最长等待时间过后,将不再等待,继续向下运行。
interrupt
线程正常运行时被打断,打断标记设为真;执行sleep,wait,join方法时被打断,会以抛出异常的方式表示被打断,打断标记设为假(清空打断标记)。
打断标记可以用来判断打断后线程是运行还是终止。
如下代码中t1将一直运行
可以使用打断标记优雅地停止线程,代码如下
interrupt打断park
park方法能使线程停止运行,使用interrupt方法打断park能使线程继续运行,打断标记设为真,之后再调用park方法线程也不会停止运行(打断标记为真park失效)
如下代码中,第二个park方法将不再生效
使用interrupted方法能清空打断标记(输出结果仍为真),第二个park方法能够再生效
不推荐使用的方法
这些方法已经过时,容易破坏同步代码块,造成线程死锁,interrupt,wait,notify取代了这些方法
主线程和守护线程
默认情况下,Java进程只会在所有线程都结束运行才结束。但有一种特殊的线程叫守护线程,在非守护线程结束后,守护线程将强制结束。
设置守护线程代码如下,t1线程将强制结束。
应用
垃圾回收器就是一种守护线程。
Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收shutdown命令后,不会等它们处理完当前请求。