1.进程与线程
1.1 进程与线程
进程:
- 运行程序必须将程序指令加载到CPU,程序数据加载至内存,进程就是来加载指令,管理内存和IO的。
- 程序被运行相当于开启了进程,进程就是程序的实例,实际中的exe类型文件
线程:
- 进程由多个线程组成
- 一个线程就是一个指令流,最终执行指令的还是线程
- Java中,CPU从线程中获取指令
区别:
- 进程之间相互独立
- 进程内的线程共享一个内存空间
- 进程间通信分为两种:
- 同一计算机进程间通信:IPC(Inter-process Communication)
- 不同计算机进程间通信:需要网络,遵守协议,比如http
- 线程通信相对简单,上下文切换成本低(上下文切换:内存不够时,把一些暂时不用的代码暂停,切换到其他代码进行)
1.2 并行与并发
单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感 觉是 同时运行的 。
总结为一句话就是:微观串行,宏观并行,
一般会将这种 线程轮流使用 CPU 的做法称为并发, concurrent
2. Java线程
2.1 创建线程
匿名内部类
相当于简化的继承或实现接口,不必再重新创建一个类,直接在接口后跟{方法的实现}
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Running with anonymous class");
}
};
new Thread(runnable).start();
lambda表达式
对匿名内部类的简化,
Runnable runnable = () -> System.out.println("Running with Lambda");
new Thread(runnable).start();
//也可以直接写为
new Thread(() ->{
System.out.println("Running with Lambda")
}).start();
2.2 查看进程线程方法
- windows 任务管理器可以查看进程和线程数,也可以用来杀死进程, tasklist 查看进程, taskkill 杀死进程
- linux
ps -fe
查看所有进程,ps -fT -p
查看某个进程(PID)的所有线程,kill
杀死进程,top
按大写 H 切换是否显示线程,top -H -p
查看某个进程(PID)的所有线程 - Java jps 命令查看所有 Java 进程 ,jstack 查看某个 Java 进程(PID)的所有线程状态, jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
2.3 线程运行
栈与栈帧
每个线程启动后,虚拟 机就会为其分配一块栈内存。
-
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
-
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
-
执行过程
-
执行结束后逐渐释放掉内存
线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
2.4 常见方法
start与run方法
run方法,不能异步,还是main方法调用
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
FileReader.read(Constants.MP4_FULL_PATH);
}
};
t1.run();
log.debug("do other things ...");
}
改为t1.start,就可以异步了
sleep与yield
- sleep,在哪被调用就让哪个线程休眠
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用interrupt方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException异常
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
- yield,让出cpu使用权
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器,有时想让但让不出去
- 线程优先级,在cpu忙的时候可能会有用,空闲时几乎没用
- sleep应用:在循环中加sleep防止cpu占满
join
等待线程运行结束,谁调用就等谁运行结束。
可以用于线程同步
有时限的join,如果线程提前结束,join也会结束。
Interrupt
- 打断阻塞
- 打断后会抛出异常,会清空打断状态,即打断状态清空为false
- 打断正常
- 打断状态为true
- 打断park,不会清空打断状态,如果已经是true,则park会失效,Interrupted方法会将打断标记设为false,使park生效
2.5 不推荐方法
stop()、suspend()、resume(),会让锁得不到释放
2.6 守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守 护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
sleep(2);
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");
-
垃圾回收器线程就是一种守护线程
-
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求
2.7 线程状态
五种状态
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态
- 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】
- 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
java六种状态
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
- BLOCKED , WAITING , 详述 TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节
- TERMINATED 当线程代码运行结束