JUC系列:(二)线程

布鸽不鸽 Lv4

创建线程

方法一:继承于Thread类

  • 继承Thread类,重写run方法
  • 创建MyThread对象,并调用start方法
1
2
3
4
5
6
7
8
9
10
11
12
13
class MyThread extends Thread {
@Override
public void run() {
System.out.println("run");
}
}

public class Main {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
}
}

方法二:实现Runnable接口

  • 实现Runnable接口,实现run方法
  • 创建MyRunnable对象,作为Thread构造方法的参数
  • 创建Thread对象,调用start方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("run");
}
}

public class Main {
public static void main(String[] args) {
Runnable r1 = new MyRunnable();
Thread t1 = new Thread(r1);
t1.start();
}
}

开发中优先选择Runnable接口的方式,因为:

  • 没有单继承的局限性
  • 可以实现多个线程共用一个Runnable,共享数据

方法三:实现Callable接口

Callable提供带返回值的call方法,需要和Future实现类连用,后续章节会讲到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "hello";
}
}

public class Main {
public static void main(String[] args) {
Callable<String> callable = new MyCallable();
FutureTask<String> task = new FutureTask<>(callable);
Thread thread = new Thread(task);
thread.start();
}
}

Thread和Runnable的关系

Runnable作为参数,构造线程源码

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
51
// 先调用它
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}

// 然后调用它
public Thread(ThreadGroup group, Runnable target, String name, long stackSize) {
this(group, target, name, stackSize, null, true);
}

// 主要的代码:
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
// ...

// 最重要的:这里将Runnable接口(target)赋值给Thread自己的target成员属性
this.target = target;

// ...
}

// 如果实现了runnable接口,那么target不会为null,最终调用实现的run方法
@Override
public void run() {
if (target != null) {
target.run();
}
}

// start方法,只能被调用一次
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();

group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then it will be passed up the call stack */
}
}
}

private native void start0();

设计模式(代理模式)

Thread是代理类,用户自定义核心功能的实现,辅助功能交由Thread类来实现

img
1
2
3
4
5
6
7
// 如果实现了runnable接口,那么target不会为null,最终调用实现的run方法
@Override
public void run() {
if (target != null) {
target.run();
}
}

设计模式(策略模式)

Runnable接口使用了策略模式,将执行逻辑(run方法)和程序的执行单元(start0方法)分离出来。使得用户可以定义自己的程序处理逻辑,更符合面向对象的思想。

为什么线程的启动不直接使用run()而必须使用start():

  • run的调用是在本地方法start0中
  • start0方法依赖于不同的操作系统实现
    • JNI(Java Native Interface),Java本地接口

Thread的构造方法

  • 创建Thread对象,默认有一个线程名(Thread-0,Thread-1,Thread-2)
  • 如果构造线程对象时,未传入ThreadGroup,Thread会默认使用父线程的ThreadGroup
  • stackSize可以提高线程栈的深度,放更多栈帧,会减少能创建线程的数目
  • stackSize默认是0,代表着被忽略,该参数会被JNI函数调用
    • JVM一旦启动,虚拟机栈的大小已经确定了
    • 但是如果你创建Thread的时候传了stackSize,该参数会被JNI函数去使用
    • 某些平台可能会失效,可以通过-Xss10m设置

线程的方法

Thread类的API

静态方法:

方法说明
static Thread currentThread()获取当前线程对象
static void sleep(long time)让当前线程休眠多少毫秒再继续执行
Thread.sleep(0) : 让操作系统立刻重新进行一次 CPU 竞争
static native void yield()提示线程调度器,让出当前线程对CPU的使用
static boolean interrupted()判断当前线程是否被打断,清除打断标记

实例方法:

方法说明
void start()启动一个新线程,JVM会调用此线程的run方法
void run()线程启动后,调用该方法
void setName(String name)设置线程名字
void getName()获取当前线程名字
线程存在默认名称:子线程是Thread-索引,主线程是main
final int getPriority()获取此线程的优先级
final void setPriority(int priority)设置此线程的优先级,常用1,5,10
void interrupt()中断这个线程,异常处理机制(不清除打断标记)
boolean isInterrupted()判断当前线程是否被打断(不清除打断标记)
final void join()等待这个线程结束
final void join(long millis)等待这个线程结束,最多millis毫秒,0意味着永远等待
final native boolean isAlive()判断线程是否存活(还没有运行完毕)
final void setDaemon(boolean on)将此线程标记为守护线程或用户线程

sleep与yield方法

Thread.sleep:

  • 当前线程从Running进入Timed Waiting状态
  • 其他线程可以使用interrupt方法打断它的睡眠,此时sleep方法抛出InterruptedException
  • 睡眠结束后,未必立即执行,需要抢占CPU
  • 锁资源不会释放

Thread.yield:

  • 提示线程调度器,让出当前线程对CPU的使用
  • 锁资源不会释放

join方法

作用:等待这个线程结束

原理:调用者轮询检查线程alive

1
2
3
4
5
public final synchronized void join(long millis) throws InterruptedException {
while (isAlive()) {
wait(0);
}
}

setDaemon方法

作用:将此线程标记为守护线程

  • 只要其他非守护线程(用户线程)结束了,守护线程会强制结束

常见的守护线程:

  • 垃圾回收线程
  • Tomcat中的Acceptor和Poller线程。所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求

interrupt方法

方法说明
void interrupt()中断这个线程,清空打断标记,异常处理机制
  • sleep、wait、join方法都会让线程进入阻塞状态
    • 此时打断线程,会清空打断状态(interrupted = false)
1
2
3
4
5
6
7
8
9
10
11
12
13
Thread t1 = new Thread(()->{
try {
Thread.sleep(1000); // 阻塞状态
} catch (InterruptedException e) {
e.printStackTrace(); // 打断后,进入异常处理
}
}, "t1");

t1.start();
Thread.sleep(500);
t1.interrupt();

System.out.println(" 打断状态: " + t1.isInterrupted()); // 打断状态: false
  • 打断正常线程,不会清空打断状态(interrupted = true)
1
2
3
4
5
6
7
8
9
10
11
12
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(" 打断状态: " + interrupted); // 打断状态: true
break;
}
}
}, "t2");

t2.start();
Thread.sleep(500);
t2.interrupt();

打断park方法

park方法作用类似于sleep方法,但是打断它,不会清空打断状态

1
2
3
4
5
6
7
8
9
10
Thread t1 = new Thread(() -> {
System.out.println("park...");
LockSupport.park();
System.out.println("unpark...");
System.out.println("打断状态: " + Thread.currentThread().isInterrupted()); // 打断状态: true
}, "t1");

t1.start();
Thread.sleep(2000);
t1.interrupt();

优雅的终止线程

thread.stop():会真正的杀死线程,其资源将永远不能释放,因此需要更优雅的方式

  • 使用中断
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
class MyThread {
private Thread monitor;

public void start() {
monitor = new Thread(() -> {
while (true) {
if (Thread.interrupted()) {
break;
}

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
});
monitor.start();
}

public void stop() {
monitor.interrupt();
}
}

线程原理

运行机制

虚拟机栈:

  • 每个线程启动后,虚拟机就会为其分配一块栈内存
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用
  • 每个线程只有一个活动栈帧

线程上下文切换:

  • CPU不再执行当前线程,转而执行另一个线程

    • 线程的 CPU 时间片用完

    • 垃圾回收

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

    • 线程自己调用了sleep,yield,wait,join,park等方法

程序计数器:

  • 线程私有的,记住下一条JVM指令的地址

上下文切换:

  • 当上下文切换发生时,由操作系统保存当前线程的状态,并恢复另一个线程的状态

    • 包括:程序计数器,虚拟机栈中每个栈帧的信息(如局部变量,操作数栈,返回地址等等)
  • JVM 规范并没有限定线程模型,以HotSpot为例

    • Java 的线程是内核级线程(1:1线程模型),每个Java 线程都映射到一个操作系统原生线程,需要消耗一定的内核资源

    • 线程的调度是在内核态运行的,而线程中的代码是在用户态运行,所以线程切换(状态改变)会导致用户与内核态转换进行系统调用,这是非常消耗性能

线程调度

方式有两种:

  • 协同式线程调度:线程做完任务才通知系统切换到其他线程
  • 抢占式线程调度(Java选择):线程的执行时间由系统分配

线程状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum State {

NEW, // 创建状态:线程已创建,但是还没start

RUNNABLE, // 就绪状态:调用了start,可能在CPU上运行,也可能没有,取决于操作系统

BLOCKED, // 阻塞状态:尝试获取一个锁,但是获取失败

WAITING, // 无限等待状态:等待其他线程的唤醒(其他线程调用notify)

TIMED_WAITING, // 超时等待状态:保持到期满,或者接收到唤醒通知(如:Thread.sleep, Object.wait)

TERMINATED; // 结束状态:正常退出,或者有没有捕获的异常而终止
}

查看线程

Windows:

  • tasklist:查看进程
  • taskkill:杀死进程

Linux:

  • ps -ef:查看所有进程
  • ps -fT -p <PID>:查看某个进程的线程
  • kill:杀死进程
  • top + 大写H:显示所有进程 + 开启显示线程
  • top -H -p <PID>:查看某个进程的所有线程

Java:

  • jps:查看所有Java进程
  • jstack <PID>:查看某个Java进程的所有线程
  • jconsole查看所有进程中线程的运行情况(图形界面)
  • 标题: JUC系列:(二)线程
  • 作者: 布鸽不鸽
  • 创建于 : 2024-01-10 14:54:58
  • 更新于 : 2024-01-10 14:55:57
  • 链接: https://xuedongyun.cn//post/19310/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论