进程与线程
进程
- 程序由指令和数据组成,工作时将指令加载至CPU,将数据加载至内存。此外在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令和管理内存的。
- 将一个程序被执行,相关的代码由硬盘加载至内存,此时就开启了一个新的进程。
- 因此,进程是程序运行起来的状态。
线程
- 一个进程可以划分为多个线程。
- 一个线程就相当于一个指令流,将指令流中的一条条指令一一定顺序交给CPU执行。
- Java中线程是最小的调度单位,进程是资源分配的最小单位。
进程和线程的区别
- 进程相互独立,线程存在于进程中,相当于进程的一个子集
- 进程拥有共享的资源,比如内存空间等,供其内部的线程进行共享
- 线程通信相对简单,因为它们共享进程内的内存,因此多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般比进程低
并发和并行
并发 (concurrent)
- 并发一定是单核CPU,各线程串行执行。
- 操作系统中的任务调度器将CPU的时间片分给不同的线程使用。
- 由于CPU在线程间的切换非常快,所在带来并行的效果。实际上宏观并行,微观串行
并行
- 一定是多核CPU,多核,不一定是多个
- 各线程在各个核上同时运行
创建线程
继承 Thread
Thread t = new Thread() {
@override
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
实现 Runnable
Runnable runnable = new Runnable() { // 此方法可以使用lambda简化,因为这是一个接口的形式
@override
public void run() {
// TODO
}
};
Thread t = new Thread(runnable);
t.start();
实现 Callable
// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@override
public Integer call() throws Exception {
return 100;
}
});
new Thread(task,"t").start();
// 主线程阻塞,同步等待task执行完毕的结果
Integer result = task.get();
三者区别
- 继承Thread: 将线程的创建和方法体写在一起,编码简单
- 实现Runnable:将线程创建和方法体分离,便于调用线程池等api,结构清晰
- 实现Callable:又增加了返回值
查看进程的方法
windows
- 任务管理器查看进程和线程数,也可用来杀死进程
- tasklist指令查看进程
- taskkill指令杀死进程
Linux
ps -fe查看所有进程ps -fT -p <PID>查看某个进程的所有线程kill杀死进程top -H -p <PID>查看某个进程的所有线程
线程运行原理
栈与栈帧
JVM由堆、栈、方法区组成。每个线程启动后,虚拟机会为其分配一块栈内存。
- 每个栈由多个栈帧组成,对应每次方法调用时所占用的内存
- 每个线程只有一个活动栈帧,对应当前正在执行的那个方法
线程上下文切换
CPU不再执行当前的线程,转而执行另一个线程的代码。CPU可能因下面的情况导致不再执行当前的线程:
- 线程的CPU时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 县城自己调用了sleep、yield、wait、join、park、synchronized、lock等方法
当发生上下文切换时,操作系统保存当前的线程状态,并且恢复另一个线程的状态,也就是Java中的程序计数器,其作用为:记住下一条jvm指令的执行地址,是线程私有的。
相关方法
start 和 run
- 启动线程应调用start方法,start方法进而去调用run方法
- 如果直接调用run方法,也可以运行,但运行的是main主线程,并没有启动新的线程,不能起到异步调用的结果
- 如果连续两次调用start会抛异常
sleep 与 yield
sleep让线程休眠,从Running状态进入Timed Waiting状态- 其他线程可使用
interrupt方法打断正在休眠的线程,此时sleep方法抛出InterruptedException - 休眠结束后的线程未必会立刻执行
yield会让线程从Running状态变为Runnable状态,然后调度执行其他同优先级的进程。若此时没有同优先级的线程,则当前线程不会暂停。具体实现依赖操作系统的任务调度器
线程优先级
- 线程优先级会提示调度器区优先调度该线程,但仅仅是一个提示,调度器可以忽略它
- 如果CPU忙,则优先级高的线程会获得更多的时间片,但CPU闲时,优先级不起作用
join
- 等待线程运行结束,可设置等待时间(传参,传入毫秒值)
- 哪个线程对象调用join方法,就等待哪个线程运行结束
- 底层原理就是wait
interrupt
打断阻塞状态的线程或正在运行状态的线程
interrupt(): 打断线程,并清除打断标记isInterrupted():判断是否被打断,不会清除打断标记interrupted():静态方法,判断当前线程是否被打断,会清除打断标记
两阶段终止模式 interrupt
若使用 stop 方法停止线程,则线程被真正杀死。如果线程锁住了共享资源,那么在杀死线程后就无法释放锁,其他线程无法获取锁。
若使用 System.exit 方法停止线程,则会让整个程序都停止。因此,使用两阶段终止模式。
两阶段终止模式:
- 若被打断,执行相应处理,结束循环
- 若未被打断,睡眠2s,有异常则设置打断标记,继续循环
- 若无异常,则执行监控记录(一般记录到日志中),继续循环
interrupt 打断 park
park和unpark均为LockSupport类中的方法
LockSupport.park(); // 暂停当前线程
LockSupport.unpark(被暂停的线程对象); // 恢复某个线程
示例:
Thread thread = new Thread(() -> {
System.out.println("start.....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("park....");
LockSupport.park();
System.out.println("resume.....");
});
thread.start();
Thread.sleep(2000);
System.out.println("unpark....");
LockSupport.unpark(thread);
运行结果:
start.....
park....
unpark....
resume.....
park中的线程处于wait状态unpark既可在park之前调用或之后调用,都是用来恢复某个线程的运行。调用unpark后再调用park,线程不会暂停
原理:每个线程都有自己的Parker对象,由_counter,_cond,_mutex组成
_cond:资源_counter:资源的数量,0耗尽,1充足_mutex:互斥信号量
调用park:
- 当前线程调用park方法
- 检查
_counter,若为0则获得_mutex互斥锁 - 线程进入
_cond条件变量阻塞 - 设置
_counter=0
调用unpark:
- 调用unpark方法,设置
_counter=1 - 唤醒
_cond条件变量中的线程 - 该线程恢复运行
- 设置
_counter=0
先调用unpark再调用park
- 调用unpack方法,设置
_counter=1 - 当前线程调用park方法
- 检查
_counter,值为1,则线程无需阻塞,继续运行 - 设置
_counter=0
几个不推荐使用的方法
stop():停止线程运行,suspend():挂起(暂停)线程resume():回复线程运行
会破坏同步代码块,造成对象的锁得不到释放,造成死锁。应使用两阶段终止模式(interrupt)
守护线程
Java进程需要等待所有线程结束后才会结束。有一种特殊的线程叫守护线程,只要其他非守护线程结束了,即使守护线程未执行完毕,也会结束。
Thread t = new Thread();
t.setDaemon(true); // 设为守护线程
- 垃圾回收器就是一种守护线程
- tomcat中的Acceptor和Poller线程(用于发送和接收)都是守护线程,tomcat接收到shutdown命令后不会等待它们处理完当前请求
线程的状态
线程五种状态
五种状态是从操作系统层面来描述的
- 初始状态:仅在语言层面上创建了线程对象,还未与操作系统线程关联(new了一个Thread对象,但未调用start方法)
- 就绪状态:该线程已被创建,与操作系统线程相关联,可由CPU调度执行,但还未获得CPU时间片
- 运行状态:获取了CPU时间片运行中的状态。若时间片用完,则转回就绪状态
- 阻塞状态:线程让出CPU使用权,导致进程上下文切换。后期操作系统可唤醒阻塞的线程,转为就绪状态
- 终止状态:线程已执行完毕,生命周期结束,不会再转为其他状态
线程的六种状态
六种状态是从Java API层面来描述的,根据Thread.State枚举类
NEW:线程刚创建,未调用start,跟“初始状态”一样RUNNABLE:调用了start方法,涵盖了“就绪状态”、“运行状态”和“阻塞状态”。由BIO导致的线程阻塞,在Java中无法区分,仍认为就绪BLOCKED:(阻塞),线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态WAITING:(等待),处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态TIME_WAITING:(超时等待),处于这种状态的线程不会被分配CPU执行时间,在达到一定时间后会被自动唤醒TERMINATED:相当于“终止状态”