JUC-02-Java线程

1. 创建线程

1.1 直接使用Thread

1
2
3
4
5
6
7
8
9
// 构造方法的参数是给线程指定名字,,推荐给线程起个名字
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();

1.2 Runnable+Thread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1.创建一个实现 Runnable 接口的类
class MyRunnable implements Runnable {
// 2.实现 Runnable 的抽象 run() 方法
@Override
public void run() {
for (int i = 0; i < 100; i++) System.out.println(i);
}
}

public class Test {
public static void main(String[] args) {
// 3.创建实现了 Runnable 的类的对象
MyRunnable myRunnable = new MyRunnable();
// 4.把此对象作为参数放入 Thread 类的构造器,从而创建 Thread 类的对象
Thread thread = new Thread(myRunnable);
// 5.调用 Thread 类的对象的 start() 方法
thread.start();
}
}

相比于继承 Thread 类的方式,用实现 Runnable 接口的方式更好。

  1. Java 是单继承多实现,继承了 Thread 就不能继承别的类了(那岂不是所有自己写的子类都没法用于多线程了吗?),但实现了 Runnable 却可以同时实现别的接口。
  2. 用一个 Runnable 实现类的对象就可以创建多个线程(这些线程共享这个对象),因而可以很方便地让多个线程共享数据(各个线程共享同一个 Runnable 类型的对象)。

1.3 Callable+Thread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 1.创建一个实现 Callable 接口的类
class MyCallable implements Callable<Integer> {
// 2.实现 Callable 的抽象 call() 方法
@Override
public Integer call() throws Exception { // Callable 的 call() 方法可以抛出异常
int sum = 0;
for (int i = 0; i < 100; i++) sum += i;
return sum; // Callable 的 call() 方法可以有返回值
}
}

public class Test {
public static void main(String[] args) {
// 3.创建实现了 Callable 的类的对象
MyCallable myCallable = new MyCallable();
// 4.把此对象作为参数放入 FutureTask 类的构造器,从而创建 FutureTask 类的对象
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
// 5.把 FutureTask 类的对象作为参数放入 Thread 类的构造器,从而创建 Thread 类的对象
Thread thread = new Thread(futureTask);
// 6.调用 Thread 类的对象的 start() 方法
thread.start();
// 7.调用 FutureTask 类的对象的 get() 方法,以获取线程的返回值(如果不需要返回值则不需要这一步)
try {
Object sumObject = futureTask.get(); // 获取线程的返回值
System.out.println(sumObject);
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
}
  • CallableRunnable 的主要区别:
    • Callablecall() 方法可以有返回值。
    • Callablecall() 方法可以抛异常。
    • Callable 支持泛型。

1.4 线程池创建线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1.创建一个实现 Runnable 接口的类
class MyRunnable implements Runnable {
// 2.实现 Runnable 的抽象 run() 方法
@Override
public void run() {
for (int i = 0; i < 100; i++) System.out.println(i);
}
}

public class Test {
public static void main(String[] args) {
// 3.创建 ExecutorService 类的对象(创建线程池)
ExecutorService executorService = Executors.newFixedThreadPool(10); // 固定线程数量的线程池
// 4.创建实现了 Runnable 的类的对象
MyRunnable myRunnable = new MyRunnable();
// 5.把实现了 Runnable 的类的对象作为参数放入 ExecutorService 类的对象的 execute() 方法
executorService.execute(myRunnable); // Runnable 用 execute() 方法,Callable 用 submit() 方法
// 6.关闭线程池
executorService.shutdown();
}
}

实际上创建线程的方式只有一种:

new 一个 Thread 对象,并给它一个 Runnable 类型的对象

Thread 对象负责启动线程,Runnable 对象负责执行线程

2. 查看进程线程方法

在linux下:

  • ps -fe 查看所有进程
  • ps -fT -p <PID> 查看某个进程(PID)的所有线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p <PID> 查看某个进程(PID)的所有线程

3. 线程运行原理

3.1 栈与栈帧

  1. 每个线程启动后,虚拟机就会为其分配一块栈内存。
  2. 每个方法被执行的时候都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,是属于线程的私有的。
  3. 当java中使用多线程时,每个线程都会维护它自己的栈,每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  4. 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

3.2 线程上下文切换

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  1. 线程的 cpu 时间片用完

  2. 垃圾回收

  3. 有更高优先级的线程需要运行

  4. 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器,它的作用是记住下一条 jvm 指令的执行地址,是线程私有的。

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

5. 常见方法

5.1 start/run

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

5.2 sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

5.3 yeild

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器

5.4 join

  1. 用于等待某个线程结束。哪个线程内调用join()方法,就等待哪个线程结束,然后再去执行其他线程。
  2. 如在主线程中调用ti.join(),则是主线程等待t1线程结束

5.5 interrupt

  1. 用于打断阻塞(sleep wait join)的线程。 处于阻塞状态的线程,CPU不会给其分配时间片。
  2. 如果一个线程在在运行中被打断,打断标记会被置为true。线程不会停止,会继续执行。如果要让线程在被打断后停下来,需要使用打断标记来判断。
  3. 如果是打断因sleep wait join方法而被阻塞的线程,会将打断标记置为false。线程抛出异常InterruptedException。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Test7 {
public static void main(String[] args) throws InterruptedException {
Monitor monitor = new Monitor();
monitor.start();
Thread.sleep(3500);
monitor.stop();
}
}

@Slf4j
class Monitor {

Thread monitor;

/**
* 启动监控器线程
*/
public void start() {
//设置线控器线程,用于监控线程状态
monitor = new Thread(() -> {
//开始不停的监控
while (true) {
//判断当前线程是否被打断了
if(Thread.currentThread().isInterrupted()) {
log.info("处理后续任务");
//终止线程执行
break;
}
log.info("监控器运行中...");
try {
//线程休眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//如果是在休眠的时候被打断,不会将打断标记设置为true,这时要重新设置打断标记
Thread.currentThread().interrupt();
}
}
});
monitor.start();
}

/**
* 用于停止监控器线程
*/
public void stop() {
//打断线程
monitor.interrupt();
}
}

5.6 线程优先级

  1. 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  2. 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Thread thread = new Thread(() -> log.info("thread run"));
thread.setPriority();


/**
* The minimum priority that a thread can have.
*/
public static final int MIN_PRIORITY = 1;

/**
* The default priority that is assigned to a thread.
*/
public static final int NORM_PRIORITY = 5;

/**
* The maximum priority that a thread can have.
*/
public static final int MAX_PRIORITY = 10;

10. sleep/yield/wait/join区别

  1. sleep,join,yield,interrupted是Thread类中的方法
  2. wait/notify是object中的方法
  3. sleep 不释放锁、释放cpu
  4. join 释放锁、join的线程抢占cpu,如t1.join(), t1抢占cpu
  5. yield 不释放锁、释放cpu
  6. wait 释放锁、释放cpu

11. 主线程/守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

注意
垃圾回收器线程就是一种守护线程

12. 操作系统线程五种状态

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态
    • 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】
    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

13. java线程六种状态

这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED, WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分。
  • TERMINATED 当线程代码运行结束

JUC-02-Java线程
https://baijianglai.cn/JUC-02-Java线程/6f24c74f93c5/
作者
Lai Baijiang
发布于
2024年4月24日
许可协议