章节目录
1. 进程线程
1.1 进程
程序由指令和数据组成。但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备,而进程就是用来加载指令、管理内存、管理IO的
程序和进程
当一个程序被运行,从磁盘加载这个程序的代码至内存。这时,就开启了一个进程。进程(动态)可以视为程序(静态)的一个实例。大部分程序可以同时运行多个实例进程。如:记事本、浏览器;也有的程序只能启动一个实例进程。如:任务管理器
1.2 线程
一个进程可以分为多个线程,而一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
在Java中,线程作为最小调度单位,进程作为资源分配的最小单位。
2. Java中的线程
在Java程序启动时,都会创建一个线程----Main线程。
2.1 创建并运行线程
如果你想在Main线程之外创建其他线程,可以用如下方法。
- Thread
- Runnable接口
- FutureTask
2.1.1 Thread
// 创建线程 (使用了匿名内部类)
Thread t1 = new Thread() {
public void run() {
// 要执行的任务
}
}
// 启动线程
t1.start()
2.1.2 Runnable
Runnable r = new Runnable() {
public void run() {
// 要执行的任务
}
}
// 创建线程对象
Thread t1 = new Thread(r, "t1");
// 启动线程
t1.start();
Java8 以后可以使用 Lambda 表达式来精简代码。如:
Runnable r = () -> {
// 要执行的任务
}
// 创建线程对象
Thread t1 = new Thread(r, "t1");
// 启动线程
t1.start();
如果对 Lambda 表达式不了解的,可以看看这篇博客 手把手地带你走进Lambda表达式之门
2.1.3 FutureTask
FutureTask 能够接收 Callable 接口的参数,用来处理有返回结果的情况。
FutureTask<Integer> task = new FutureTask<>(() -> {
// 要执行的任务
return 666;
})
new Thread(task, "t1").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task.get();
2.1.4 Thread与Runnable
查看 Thread 源码:
- Thread 实现了 Runnable 接口
- Thread 有一个 Runnable 类型的成员变量 target
查看 Thread 的构造方法:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
最终会调用:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
this.target = target;
...
}
将构造方法中的参数传递给了成员变量 target。
查看 Thread 中的 run() 方法:
public void run() {
if (target != null) {
target.run();
}
}
如果 target != null,则运行 Runnable 接口中的 run() 方法;否则,直接运行Thread 中的 run() 方法。
方法1 中的原理:
通过 匿名内部类实现的方式可以看做是 Thread 的子类(亦或继承 Thread),子类重写了父类中的 run() 方法。所以,最终运行的是子类中的方法
方法2 中的原理:
把 Runnable 类型的变量作为 Thread 的构造方法的参数,在此构造方法中,将此参数传递给成员变量 target,所以 target != null,最终会执行 Runnable 接口中的方法,而上述的 Lambda 表达式将 Runnable 接口中的 run() 方法进行重写,所以,最终会执行 Lambda表达式。
2.2 栈与栈帧
JVM是由堆、栈、方法区等组成,其中,栈内存是给线程用的,每个线程启动后,JVM会为它分配一个栈内存。而每个栈是由多个栈帧组成,对应着每次方法调用时所占用的内存
2.3 上下文切换
因为以下原因导致CPU不再执行当前的线程,转而执行另一个线程的代码:
- 线程的CPU时间片用完
- 垃圾回收
- 有更优先级的线程需要执行
- 线程自己调用了sleep()、wait()、lock() 方法等
3. 常用的方法
方法名 | 功能描述 | 注意 |
---|---|---|
start | 启动一个新的线程,在新的线程中运行 run() 方法 | start() 方法只是让线程进入就绪状态,里面的代码不一定就马上执行(CPU时间片还没分给它)。每个线程对象的 start() 方法只能调用一次,如果调用多次,会抛出异常IllegalThreadStateException |
run | 新的线程启动后,会执行的代码 | |
join | 等待线程运行结束 | |
isInterrupted | 判断是否被打断 | 不会清除打断标记 |
interrupt | 打断线程 | 如果被打断的线程正在sleep、wait、join 会导致被打断的线程抛出异常 InterruptedException,并清除标记;如果打断正在运行的线程,则会设置打断标记;park 线程被打断,也会设置打断标记 |
interrupted | 判断当前线程是否被打断 | 会清除打断标记 |
sleep(n) | 让当前执行的线程休眠 n 毫秒,休眠时间让出 CPU | |
yield | 提示线程调度器让出当前线程对 CPU 的使用 |
3.1 start()与run()
start() 方法表示启动一个新的线程,run() 方法表示线程启动后要执行的代码,那能否能直接调用 run() 方法?
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run () {
System.out.println("hello world" + "=====" +Thread.currentThread().getName());
}
};
t1.run();
}
run() 方法还是可以执行,但并没有创建新的线程,还是在 main 线程中执行的
3.2 sleep()
- 调用 sleep() 方法会让当前线程从 Running 进入 Timed Waiting 状态
- 其它线程可以使用 interrupt() 方法打断正在睡眠的线程,这时,sleep() 方法会抛出 InterruptedException异常
public static void main(String[] args) throws Exception{
Thread t1 = new Thread() {
@Override
public void run () {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("wake up");
}
}
};
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
3.3 join()
看下这段代码,打印 i 是什么?
static int i = 1;
private static void test() throws Exception{
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
i = 100;
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
System.out.println("i = " + i);
}
public static void main(String[] args) throws Exception{
test();
}
流程分析:main 线程启动,调用 test() 方法,开启一个新的线程 t1,main 和 t1 线程都是在同时运行的,main 线程会先执行语句 “System.out.println("i = " + i);”,只有当 t1 线程休眠1s后,才会将 i 赋值为 100。
那么,如何让打印的 i 为100呢?
只需要添加一个 join() 方法
t1.start();
t1.join();
System.out.println("i = " + i);
当 main 线程 执行到语句 “t1.join()”时,main 线程会一直等待着 t1 线程(t1 调用了 join()方法),直到 t1 线程运行结束后,main 线程才会继续执行。
3.4 interrupt()
3.4.1 打断阻塞的线程
打断 sleep、wait、join 的线程会清空打断标记。以 sleep 为例
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(100);
t1.interrupt();
System.out.println("打断标记:" + t1.isInterrupted());
}
3.4.2 打断运行的线程
打断运行的线程是不会清除标记的
3.4.3 应用
两阶段终止模式
在线程 t1 中如何优雅地终止线程 t2?
先看看两阶段终止模式的一个应用场景----做一个系统的健康监控,如:定时地去监控CPU 的使用率、内存的使用率等。可以使用一个后台的监控线程每隔2s不断地进行记录即可,当点击停止按钮时便不再监控了。
代码实现:
public class TwoPhraseTermination {
// 监控线程
private Thread monitor;
// 启动监控线程
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
System.out.println("料理后事");
break;
}
try {
Thread.sleep(2000);
System.out.println("执行监控记录");
} catch (InterruptedException e) {
// 重新设置打断标记
current.interrupt();
}
}
});
monitor.start();
}
public void stop() {
monitor.interrupt();
}
public static void main(String[] args) throws Exception{
TwoPhraseTermination termination = new TwoPhraseTermination();
termination.start();
Thread.sleep(5000);
termination.stop();
}
}
这就是实现了在一个 main 线程中优雅地终止了另一个线程。