Java 线程
创建、运行线程
1、直接使用 Thread
//创建线程对象
Thread t = new Thread() {
public void run() {
//执行任务
}
};
//启动线程
t.start();
//构造方法的参数,指定线程名字(建议)
Thread t1 = new Thread("t1") {
@Override
//run方法内,实现所执行代码
public void run() {
}
};
t1.start();
2、使用 Runnable 配合 Thread(建议)
(1)把线程、任务(所执行代码)分开
(2)Thread 代表线程
(3)Runnable 可运行的任务(线程所执行代码)
Runnable runnable = new Runnable() {
public void run(){
//执行代码
}
};
//创建线程对象
Thread t = new Thread(runnable);
//启动线程
t.start();
//创建任务对象
Runnable task2 = new Runnable() {
@Override
public void run() {
//线程执行代码
}
};
//参数1:是任务对象; 参数2:线程名字(建议)
Thread t2 = new Thread(task2, "t2");
t2.start();
3、FutureTask 配合 Thread
(1)对 Runnable 的拓展
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
(2)Future 对于具体 Runnable 或 Callable 任务的执行结果,进行取消、查询是否完成、获取结果,必要时可以通过 get 方法获取执行结果,该方法会阻塞直到任务返回结果
public interface Future<V> {
//取消任务
boolean cancel(boolean mayInterruptIfRunning);
//判断任务是否已经取消
boolean isCancelled();
//判断任务是否已经结束
boolean isDone();
//获取任务执行结果
V get() throws InterruptedException, ExecutionException;
//获取任务执行结果,带有超时时间限制
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
(3)Callable 类似 Runnable,可以返回值,可以抛出异常
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
(4)FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
//创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
//线程任务代码
return 100;
});
//参数1:任务对象;参数2:线程名字(建议)
new Thread(task3, "t3").start();
//主线程阻塞,同步等待 task 执行完毕结果
Integer result = task3.get();
Callable 接口
1、当线程终止时,即 run() 完成时,使线程返回结果
2、特点
(1)对于 Callable,需要实现在完成时返回结果的 call()
(2)call() 可以引发异常,而 run() 不能
(3)为实现 Callable 而必须重写 call()
3、不能直接替换 Runnable
(1)因为 Thread 类的构造方法没有 Callable
(2)Runnable 接口有实现类 FutureTask,FutureTask 构造器可以传递 Callable
Future 接口
1、将 Future 视为保存结果的对象
(1)它可能暂时不保存结果,但将来会保存(一旦 Callable 返回)
(2)Future 基本上是主线程可以跟踪进度,以及其他线程的结果的一种方式
(3)call() 完成时,使用 Future 对象接收结果,主线程可以知道该线程返回结果
2、要实现此接口,必须重写 5 种方法
(1)如果尚未启动,它将停止任务;如果已启动,则仅在 mayInterrupt 为 true时,才会中断任务
boolean cancel(boolean mayInterruptIfRunning);
(2)如果该任务在正常完成之前被取消,则返回 true
boolean isCancelled();
(3)如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果
V get() throws InterruptedException, ExecutionException;
(4)如果有必要,最多等待给定的时间完成计算,然后检索其结果(如果有)
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
(5)如果这个任务完成,返回 true。完成可能是由于正常终止、异常、取消,以上情况下,该方法将返回 true
boolean isDone();
FutureTask
1、实现 Runnable、Future,并将两种功能组合
(1)可以通过为其构造函数,提供 Callable 来创建 FutureTask
(2)将 FutureTask 对象,提供给 Thread 的构造函数,以创建 Thread 对象
(3)可间接使用 Callable 创建线程
2、使用
(1)在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给 Future 对象在后台完成
(2)当主线程将来需要时,可以通过 Future 对象,获得后台作业的计算结果 / 执行状态
(3)一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果
(4)仅在计算完成时,才能检索结果;如果计算尚未完成,则阻塞 get 方法
(5)一旦计算完成,就不能再重新开始 / 取消计算
(6)get 方法获取结果,只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果 / 抛出异常
(7)get 只计算一次,因此 get 方法放到最后执行
Thread 与 Runnable 关系
1、Thread 源码
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
2、结论
(1)实现 Runnable 接口:分开线程、任务
(2)直接使用 Thread:合并线程、任务
(3)使用 Runnable 更容易与线程池等高级 API 配合
(4)Runnable 让任务类脱离 Thread 继承体系,更灵活
查看进程、线程
1、Windows
(1)任务管理器查看进程、线程数
(2)tasklist:查看进程
(3)taskkill:杀死进程
2、Linux
(1)ps -fe:查看所有进程
(2)ps -fT -p <PID>:查看某个进程(PID)的所有线程
(3)kill <PID>:杀死进程
(4)top -H -p <PID>:查看某个进程(PID)的所有线程
3、Java
(1)jps:查看所有 Java 进程
(2)jstack <PID>:查看某个 Java 进程(PID)的所有线程状态
(3)jconsole:查看某个 Java 进程中线程的运行情况(图形界面)
4、jconsole
(1)远程监控配置
(2)需要以如下方式运行 Java 类
java -Djava.rmi.server.hostname=IP地址
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=连接端口
-Dcom.sun.management.jmxremote.ssl=是否安全连接
-Dcom.sun.management.jmxremote.authenticate=是否认证
Java类
(3)修改 /etc/hosts 文件将 127.0.0.1 映射至主机名
(4)如果要认证访问,还需要做如下步骤
(5)复制 jmxremote.password 文件
(6)修改 jmxremote.password 和 jmxremote.access 文件的权限为 600,即文件所有者可读写
(7)连接时填入 controlRole(用户名),R&D(密码)
栈、栈帧
1、Java 虚拟机栈:Java Virtual Machine Stacks
2、栈内存分配给线程
(1)每个线程启动后,虚拟机就会为其分配一块栈内存
(2)每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
(3)每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch)
1、导致 CPU 不再执行当前的线程,转而执行另一个线程的代码
2、主动原因
(1)线程的 CPU 时间片用完
(2)垃圾回收
(3)有更高优先级的线程需要运行
3、被动原因
(1)线程自己调用 sleep、yield、wait、join、park、synchronized、lock 等方法
4、当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态
(1)Java 中对应的概念为程序计数器(Program Counter Register),其作用是记住下一条 JVM 指令的执行地址,属于线程私有
(2)状态包括程序计数器、虚拟机栈中每个栈帧的信息,如:局部变量、操作数栈、返回地址等
(3)Context Switch 频繁发生会影响性能
常见方法
方法名 | static | 功能 | 注意 |
start() | 启动一个新线程,在新的线程运行 run() 中的代码 |
start 方法只是让线程进入就绪,不一定立刻运行代码(CPU 时间片还未分配) 每个线程对象的 start() 只能调用一次,如果调用多次会出现 IllegalThreadStateException |
|
run() | 新线程启动后会调用的方法 |
如果在构造 Thread 对象时,传递 Runnable 参数,则线程启动后会调用 Runnable 中的 run(),否则默认不执行任何操作 但可以创建 Thread 的子类对象,来覆盖默认行为 |
|
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待 n 毫秒 | ||
getId() | 获取线程长整型 id | id 唯一 | |
getName() | 获取线程名 | ||
setName(String) | 修改线程名 | ||
getPriority() | 获取线程优先级 | ||
setPriority(int) | 修改线程优先级 | Java 中规定线程优先级是 1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率 | |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED | |
isInterrupted() | 判断是否被打断 | 不会清除打断标记 | |
isAlive() | 线程是否存活(是否运行完毕) | ||
interrupt() | 打断线程 |
如果被打断线程正在 sleep / wait / join 会导致被打断线程抛出 InterruptedException,并清除打断标记 如果打断的正在运行的线程,则会设置打断标记 park 线程被打断,也会设置打断标记 |
|
interrupted() | static | 判断当前线程是否被打断 | 清除打断标记 |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行线程休眠 n 毫秒, 休眠时让出 CPU 时间片给其它 线程 | |
yield() | static | 提示线程调度器,让出当前线程对 CPU 的使用 | 为了测试、调试 |
start、run
1、直接调用 run,是在主线程中执行 run,没有启动新的线程
2、使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
sleep、yield
1、sleep
(1)调用 sleep 会让当前线程,从 Running 进入 Timed Waiting(阻塞)
(2)其它线程可以使用 interrupt 方法,打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
(3)睡眠结束后的线程,未必会立刻得到执行
(4)建议用 TimeUnit 的 sleep,代替 Thread 的 sleep 来获得更好的可读性
2、yield
(1)调用 yield 会让当前线程,从 Running 进入 Runnable(就绪),然后调度执行其它线程
(2)具体的实现依赖于操作系统的任务调度器,即只是非强制让步
3、线程优先级
(1)线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
(2)如果 CPU 繁忙时,则优先级高的线程会获得更多的时间片,但 CPU 空闲时,优先级几乎没作用
限制使用 CPU
1、sleep 实现
(1)在没有利用 CPU 来计算时,不要让 while(true) 空转浪费 CPU,这时可以使用 yield 或 sleep,让出 CPU 使用权给其他程序
while(true) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
(2)可以用 wait / 条件变量达到类似的效果
(3)不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
(4)sleep 适用于无需锁同步的场景
2、wait 实现
synchronized(锁对象) {
while(条件不满足) {
try {
锁对象.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
// do sth...
}
3、条件变量实现
lock.lock();
try {
while(条件不满足) {
try {
条件变量.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// do sth...
} finally {
lock.unlock();
}
join
1、等待某个线程结束
(1)在 t1 线程内调用 t2.join(),则是 t1 线程等待 t2 线程结束
(2)join 采用同步
2、需要等待结果返回,才能继续运行为同步
3、不需要等待结果返回,就能继续运行为异步
4、指定等待 t1 时间
(1)未超过 t1 等待时间,t1 线程执行完毕,t2 不需要继续等待,直接执行
(2)超过 t1 等待时间,t1 线程未执行完成,t1、t2 线程并发执行
interrupt
1、设置线程的中断标记
(1)通过检查中断标志,判断线程是否被中断
(2)线程允许忽略中断标记继续执行,即使设置中断,线程仍然可能会继续执行
2、interrupt 打断线程的两种情况
(1)如果一个线程在在运行中被打断,打断标记会被置为 true
(2)如果打断因为 sleep / wait / join(都会让线程进入阻塞状态),而被阻塞的线程,打断标记会被置为 false,即清空,抛出异常 InterruptedException
3、isInterrupted、interrupted 比较
(1)isInterrupted 是实例方法,interrupted 是静态方法,作用都是查看当前打断的状态
(2)isInterrupted 查看线程时,不会将打断标记置为 false,即不清空
(3)interrupted 查看线程打断状态后,会将打断标志置为 false,即清空
(4)interrupt 类似于 setter 设置中断值;isInterrupted 类似于 getter 获取中断值;interrupted 类似于 getter + setter 先获取中断值,然后清除标志
4、打断 park 线程,不会将打断标志置为 false,即不清空打断状态
(1)park():将当前线程停止在 park() 所处行,可以由打断标志控制
(2)当打断标志为 true ,park() 失效;当打断标志为 false 时,park() 生效
(3)被打断的 park 线程,可以使用 Thread.interrupted() 清除打断状态
两阶段终止模式
1、错误思路
(1)使用线程对象 stop() 停止线程:stop 会真正杀死线程,如果这时线程锁住共享资源,则当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
(2)使用 System.exit(int) 停止线程:目的只是停止一个线程,但这种做法会让整个程序都停止
2、实现思路
(1)用一个监控线程监控所有线程,运行过程中,时刻检测当前线程的打断标志是否为 true,当打断标志为 true 时,停止线程
(2)当前线程正常运行时被打断,可以得到(1)的结果
(3)当线程阻塞时被打断,需要手动将标志改为 true
(4)监控线程需要有间隔监控,如进入睡眠,但睡眠时间可能被打断
3、使用 isInterrupted 实现监控线程(例)
(1)interrupt 可以打断正在执行的线程,无论线程状态为 sleep / wait / 正常运行
class TwoParseTermination {
//监控线程
private Thread monitor;
//启动监控线程
public void start() {
monitor = new Thread(() -> {
while (true) {
//获取并监控当前线程
Thread thread = Thread.currentThread();
//调用isInterrupted,不会清除标记
if(thread.isInterrupted()) {
//当前线程正常打断
//break前,处理当前线程退出操作
break;
} else {
try {
//监控间隔为1秒
Thread.sleep(1000);
//执行监控的功能
} catch (InterruptedException e) {
//在监控线程sleep时,当前线程被打断
//手动设置打断标记为true
thread.interrupt();
e.printStackTrace();
}
}
//执行监控操作
}
}, "monitor");
monitor.start();
}
//终止监控线程
public void stop() {
monitor.interrupt();
}
}
已过时方法
1、容易破坏同步代码块,造成线程死锁
2、stop 使用 interrupt 代替
3、suspend、resume 使用 wait、notify 代替
非 static 方法 | 功能说明 |
stop() | 停止线程运行 |
suspend() | 挂起(暂停)线程运行 |
resume() | 恢复线程运行 |
主线程、守护线程
1、默认情况下,Java 进程需要等待所有线程都运行结束,才会结束
2、守护线程,只要其它非守护线程运行结束,即使守护线程的代码没有执行完,也会强制结束
3、setDaemon(true):设置调用线程为守护线程
4、例
(1)垃圾回收器线程
(2)Tomcat 中的 Acceptor、Poller 都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
捕获线程的执行异常
1、在线程的 run() 中,如果有受检异常,必须进行捕获处理
2、如果想要获得 run() 中出现的运行时异常信息,可以通过回调 UncaughtExceptionHandler 接口,获知哪个线程出现运行时异常
3、在 Thread 类中,处理运行异常有关方法
(1)getDefaultUncaughtExceptionHandler():获得全局(默认)的 UncaughtExceptionHandler
(2)getUncaughtExceptionHandler():获得当前线程的 UncaughtExceptionHandler
(3)setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):设置全局的 UncaughtExceptionHandler
(4)setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):设置当前线程的 UncaughtExceptionHandler
4、当线程运行过程中出现异常
(1)JVM 会调用 Thread 类的 dispatchUncaughtException(Throwable e) 方法,该方法会调用 getUncaughtExceptionHandler().uncaughtException(this, e);
(2)如果想要获得线程中出现异常的信息,就需要设置线程的 UncaughtExceptionHandler
注入 Hook 钩子线程
1、MySQL、Zookeeper、kafka 等都存在 Hook 线程的校验机制
2、作用
(1)校验进程是否已启动,防止重复启动程序
(2)资源释放
(3)尽量避免在 Hook 线程中进行复杂操作
3、常在程序启动时创建一个 .lock 文件
(1)使用 .lock 文件,校验程序是否启动
(2)在程序退出(JVM 退出)时,执行 Hook 线程,删除该 .lock 文件
线程的 5 种状态
1、从操作系统层面描述
2、初始状态
(1)仅是在语言层面创建线程对象
(2)还未与操作系统线程关联
3、可运行状态
(1)又称就绪状态
(2)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
4、运行状态
(1)指获取 CPU 时间片运行中的状态
(2)当 CPU 时间片用完,会从运行状态转换至可运行状态,导致线程的上下文切换
5、阻塞状态
(1)如果调用阻塞 API,如:BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入阻塞状态
(2)等 BIO 操作完毕,由操作系统唤醒阻塞的线程,转换至可运行状态
(3)与可运行状态区别:对于阻塞状态的线程,只要它们一直不唤醒,调度器就一直不会考虑调度它们
6、终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
线程 6 种状态
1、从 Java API 层面描述:根据 Thread.State 枚举,分为六种状态
2、NEW
(1)线程刚被创建,但是还没有调用 start()
(2)相当于初始状态
3、RUNNABLE
(1)线程调用 start() 方法之后
(2)注意:Java API 层面的 RUNNABLE 状态,涵盖操作系统层面:可运行状态、运行状态、阻塞状态
(3)由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行
(4)RUNNABLE 在 debug 中,显示为 RUNNING
4、BLOCKED、WAITING、TIMED_WAITING 都是 Java API 层面,对阻塞状态的细分
(1)BLOCKED 在 debug 中,显示为 Monitor
5、TERMINATED:当线程代码运行结束
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战