线程状态
Java一共有六种状态
- NEW: 通过new Thread() 创建出来的线程, 此时它仅仅算是一个Java对象而已, 还不算真正的线程, 此时可以设置线程的优先级, 线程是否守护线程等
- RUNNABLE: 调用start() 方法之后, Java虚拟机为线程分配虚拟机栈,本地方法栈和程序计数器等, 线程处于RUNNABLE状态, 处于RUNNABLE 状态的线程可能在运行, 也可能只是就绪状态,取决于cpu 调度器由有没有分配时间片
- BLOCKED: 线程因为synchronized, 而进入阻塞状态
- WAITING: 线程执行 Thread.sleep, t.join, obj.wait 方法而进入等待状态(sleep是Thread的静态方法, join 是Thread的成员方法, wait 则是Object的成员方法)
- TIMED WAITING: 执行带超时时间的上述方法会进入该状态
- TERMINATED: 线程终止
线程的创建方式
本质上只有一种方式创建线程, 就是 new Thread.
不管是new Thread, 然后重写它的run() 方法, 还是 创建一个Runnable 对象, 然后传递给Thread, 本质上都是新建一个线程, 然后交给它一个任务去执行. Thread 负责线程本身相关的职责和控制, Runnable 负责逻辑执行单元的部分.
Thread 构造函数
Thread的构造函数中存在以下这些参数:
- group: ThreadGroup类型, 表示线程的分组, 如果不指定,默认采用它的父线程的分组. 每个线程的父线程是创建它的线程. 我们知道 main线程Java虚拟机创建的, 然后如果我们在main线程中创建线程, 那么新建出来的线程的父线程是main线程, 一个线程默认继承父线程的分组,线程优先级, 是否守护线程, 上下文加载器
- target: Runnable类型, 表示该线程的任务,即Runnable 对象
- name: 线程的名称, 如果不指定默认是Thread-x, x 是从0开始递增的整数. 一般建议给线程命名,这样方便日志查看和问题定位
- stackSize: 线程的虚拟机栈大小, 默认是设置为0, 即不会产生影响. 一般通过虚拟机参数 -Xss 设置虚拟机栈大小
虚拟机栈是线程私有的, 在调用start方法后,Java虚拟机为该线程分配的方法执行内存空间. 在线程中, 方法在执行时,会创建一个栈帧, 用来存放局部变量表, 操作数栈,动态链接和方法出口等信息, 方法的调用就对应着一个栈帧在虚拟机栈中的压栈和弹出的过程.
所以, 这个虚拟机栈的大小就控制着一个线程的最大调用深度.
但是虚拟机栈也不是越大越好, 如果虚拟机栈越大,可创建的线程就越少.
毕竟一个进程可用的地址空间是有限的, 比如在32位的windows 操作系统中, 进程的最大进程大小是2G
Java进程的内存大小 = jvm堆内存 + 系统保留内存 + 线程数 * stackSize, 系统保留内存在 136M 左右.
其中, jvm 堆内存可以使用 -Xmx 和 -Xms 分别设置堆的最大内存和最小内存
守护线程
守护线程是一些后台线程,比如垃圾收集线程等. 如果Java进程中除了守护线程, 没有其他线程时, 进程会结束.
可以通过 t.setDeamon(true) 设置, 但是只有在调用start 方法之前设置才会生效.
Thread 的API
sleep() 方法
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos)throws InterruptedException
静态方法, 可中断, 在哪个线程中执行, 哪个线程就进入休眠状态
[注意] 如果是在持有了对象锁,然后进入sleep, 线程不会释放对象锁
yield() 方法
public static native void yield();
静态方法, 表示线程让出当前cpu 的执行机会, 它只是给调度器一个提示, cpu 调度器不保证每次都会满足yield 提示
线程优先级
public final void setPriority(int newPriority);
public final int getPriority();
设置和获取线程优先级, 理论上优先级高的线程会获得更多的执行机会, 但实际上无法保证总是这样, 所以不能让业务严重依赖线程的优先级
可设置的优先级是 1-10, 默认是5. 设置的线程优先级,不能高于它所在group的最高优先级
获取当前线程
public static native Thread currentThread()
静态方法, 返回当前线程的引用
线程上下文加载器
public void setContextClassLoader(ClassLoader cl) ;
public ClassLoader getContextClassLoader();
中断线程
与线程中断有关的方法有三个
public void interrupt();
public static boolean interrupted();
public boolean isInterrupted();
在线程内部有一个中断标志,interrupt flag. 如果调用一个线程的 interrupt() 方法, 这个中断标志就会设置为true.
此时如果调用成员方法 isInterrupted(), 则会返回true.
调用静态方法 interrupted(),也会返回true, 但是这个静态方法会清除打断标志. 成员方法 isInterrupted() 则不会.
除此还有一个需要注意的, 当调用线程的interrupt() 方法时, 如果线程正阻塞在以下方法时, 阻塞会中断,并抛出InterruptedException异常, 并且, 中断标志也会擦除
- Object 的wait 方法,不管是否带超时参数
- Thread的sleep 方法, 不管是否带超时参数
- Thread的join 方法, 不管是否带超时参数
- InterruptibleChannel 的io 操作
- Selector的wakeup 操作
- 其他可中断方法
此外, 如果在一个线程调用可中断方法之前, 就调用了它的中断方法, 之后它进入可中断方法会怎么呢?
结果是, 如果一个线程设置了中断标志, 那么接下来的可中断方法会立即中断
join 方法
join 方法也是可中断方法, 可以利用join方法做一些线程同步操作.
如果A线程调用B线程的 join() 方法, 那么A线程会阻塞. 直到线程B执行结束. 但如果在A线程阻塞期间,别的线程再调用了A线程的interrupt() 方法, 那么A线程会中断, 并抛出InterruptedException异常.
关闭线程
JDK提供了一个方法stop() 方法来关闭线程, 但是已经不推荐使用, 因为它关闭线程时可能不会释放monitor锁, 所以我们要采用别的方法来关闭线程
- 线程任务执行完, 正常结束
这种方式不需要做额外的操作, 线程执行完任务, 自然结束生命周期. 但是一般都不会只让线程执行一个任务, 就让它结束生命周期, 毕竟申请线程是需要一定开销的, 所以一般会将线程放进线程池中,然后不断从任务队列获取任务来执行, 所以我们还需要别的线程关闭方法 - 调用interrupt 方法中断线程
如果一个线程正在执行可中断方法, 调用它的interrupt方法, 线程会抛出 InterruptedException异常,此时,可以捕获这个异常, 然后让线程结束任务的执行.
但是在捕获异常的方法中结束线程的方式不总是可行, 因为当线程不是在执行可中断方法时, 调用interrupt 方法, 仅仅会设置线程的中断标志. 虽然可以通过检查中断标志来判断是否要结束线程, 但是, 中断标志是可以被擦除, 所以通常还会配合一个变量来识别 - volatile变量 + 中断标志
volatile 变量保证了所有的线程对该变量的读取都是最新的
在关闭方法中, 还调用了interrupt 方法是为了保证如果线程正执行可中断方法时, 也能进行中断
而在while循环的条件中, 还加 !isInterrupted() 的判断,则是为了如果线程在正常执行任务的时候, 别的线程调用了它的interrupt方法, 它之后也能感知, 从而不再执行其他任务,进行线程关闭
private volatile boolean closed = false;
@Override
public void run() {
while(!closed && !isInterrupted()){
// 执行任务
}
}
public void close(){
closed = true;
this.interrupt();
}
线程同步
如果一个可变的变量, 不同的线程共享, 那么会存在线程安全问题.
为了避免这个问题, 可以在对该变量的访问之前使用synchronized进行加锁.
synchronized 包含两条JVM 指令, monitor enter 和 monitor exit.
JVM保证任何时候 任何线程执行到 monitor enter成功后, 都必须从主存中获取数据, 而不是从线程的缓存中获取
另外, 也保证在执行monitor exit之后, 共享变量更新后的值会 刷入到主存
synchronized 也称为对象锁, 它是与对象关联的, 可以是一个普通Java对象, 也可以是一个类对象.
它的原理是这样的:
每个对象都与一个monitor 关联, 一个monitor 在同一时刻只能被一个线程持有, 当一个线程尝试获得一个对象关联的monitor的所有权时, 会出现以下几种情况:
- 如果monitor的计数器为0, 那么该线程称为该monitor的owner, 计数器加一
- 如果monitor的计数器不为0, 但是该线程发现monitor的owner时自己, 那么重入,计数器加一
- 如果一个线程发现monitor计数器不为0, owner 也不是自己, 那么进入BLOCKED 状态, 直到monitor的计数器为0
当一个线程执行monitor exit 指令时, monitor的计数器就减一,当计数器为0, 那么就相当于释放了锁.
使用 synchronized 进行线程同步时, 要注意是否对同一个对象加锁, 以及注意加锁的粒度, 如果粒度太大,则会丧失了并发的本意
使用 synchronized 加锁有两个弊端, 一个是无法控制阻塞时长, 一个是无法中断阻塞
无法控制阻塞时长指的是, 一个线程阻塞加锁过程, 它无法知道它要阻塞多久, 只有等占有锁的线程释放了锁,它才有机会去竞争锁
无法中断阻塞指的是, 一个线程一旦开始阻塞在加锁过程, 没有方法可以中断这个阻塞状态, 进程意外终止除外
线程间通信
我们假定一个这样的场景, 有一个盘子最大可以放10个苹果, 父母负责往盘子放苹果, 孩子们则从苹果上拿苹果吃. 如果盘子满了,父母就要等到盘子有空间了才能放; 如果盘子空了,孩子也必须要等到有苹果才能吃.
那么该怎么实现呢?
// 设计一个盘子
public class Plate<T> {
private static final int MAX = 10;
private Deque<T> queue = new ArrayDeque<>(MAX);
public void put(T e){
synchronized (this){
while(queue.size()>MAX){
try {
this.wait(); // 释放锁
} catch (InterruptedException ex) {
}
// 当继续执行,则说明已经重新竞争获得了锁
}
queue.addLast(e);
this.notifyAll(); // 唤醒所有在wait set 中的线程
}
}
public T take(){
synchronized (this){
while(queue.size() == 0){
try {
this.wait();
} catch (InterruptedException e) {
}
}
T e = queue.pollFirst();
this.notifyAll();
return e;
}
}
}
// 测试
public class TestPlate {
public static void main(String[] args) {
Plate<Apple> plate = new Plate<>();
new Thread(()->{
while(true) {
// 模拟生产苹果
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
Apple a = new Apple();
print("放了一个苹果"+ a);
plate.put(a);
}
},"parent").start();
for(int i = 0;i<2;i++){
new Thread(()->{
while(true) {
print("想吃苹果");
Apple a = plate.take();
print("吃了一个苹果" + a);
// 模拟吃苹果
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
}
}
},"child-"+i).start();
}
}
public static void print(Object msg){
System.out.println(String.format("[%s] [%s] %s", LocalDateTime.now(),Thread.currentThread().getName(),msg));
}
}
class Apple{
}
输出结果
========================================
[2022-08-29T23:02:52.644] [child-1] 想吃苹果
[2022-08-29T23:02:52.644] [child-0] 想吃苹果
[2022-08-29T23:02:53.584] [parent] 放了一个苹果com.lion.gmall.start.spi.Apple@51d1628b
[2022-08-29T23:02:53.585] [child-0] 吃了一个苹果com.lion.gmall.start.spi.Apple@51d1628b
[2022-08-29T23:02:54.590] [parent] 放了一个苹果com.lion.gmall.start.spi.Apple@5f1696c1
[2022-08-29T23:02:54.592] [child-1] 吃了一个苹果com.lion.gmall.start.spi.Apple@5f1696c1
[2022-08-29T23:02:55.590] [child-0] 想吃苹果
[2022-08-29T23:02:55.595] [parent] 放了一个苹果com.lion.gmall.start.spi.Apple@5b2909fb
[2022-08-29T23:02:55.595] [child-0] 吃了一个苹果com.lion.gmall.start.spi.Apple@5b2909fb
[2022-08-29T23:02:56.597] [child-1] 想吃苹果
[2022-08-29T23:02:56.599] [parent] 放了一个苹果com.lion.gmall.start.spi.Apple@1e2b9cfa
[2022-08-29T23:02:56.601] [child-1] 吃了一个苹果com.lion.gmall.start.spi.Apple@1e2b9cfa
[2022-08-29T23:02:57.600] [child-0] 想吃苹果
[2022-08-29T23:02:57.605] [parent] 放了一个苹果com.lion.gmall.start.spi.Apple@4434708e
[2022-08-29T23:02:57.605] [child-0] 吃了一个苹果com.lion.gmall.start.spi.Apple@4434708e
[2022-08-29T23:02:58.602] [child-1] 想吃苹果
[2022-08-29T23:02:58.609] [parent] 放了一个苹果com.lion.gmall.start.spi.Apple@2fff4657
[2022-08-29T23:02:58.610] [child-1] 吃了一个苹果com.lion.gmall.start.spi.Apple@2fff4657
[2022-08-29T23:02:59.612] [parent] 放了一个苹果com.lion.gmall.start.spi.Apple@3c0af482
注意点:
- wait() 方法和notify()/notifyAll() 方法执行之前必须要先获取对象锁
- 执行wait方法, 线程释放获得的对象锁,然后进入这个对象关联的monitor 的wait set 中等待,状态为WAIT(TIMED WAIT), 当其他线程执行notify或者notifyAll 时会唤醒, 然后重新参与对象锁的竞争, 竞争锁成功之后,才会重新往下执行, 如果竞争失败,则线程状态是BLOCKED, 等下次monitor的计数器为0时再次参与竞争
- 执行notify 方法, 则会随机唤醒一个在 wait set中的线程, 执行notifyAll方法,则会唤醒wait set 中的所有线程
wait 和sleep 的区别
4. 线程是WAIT/TIMED-WAIT状态
5. 都是可中断方法
6. wait 是Object 的方法, sleep 是Thread 特有的方法
7. wait 要在同步方法中执行,sleep 不需要
8. 如果sleep在同步方法中,它不会释放锁, 而wait一定会释放锁
9. sleep进行了指定时间的休眠之后,会主动往下执行, 而wait(除带超时参数的) 需要其他线程唤醒
ThreadGroup
每个线程都会属于一个线程组, 在创建线程时,可以为线程声明一个分组, 如果没有声明,则默认使用创建它的线程的线程分组
Thread group 不能对线程进行管理,它最主要的功能是组织线程
除了system 线程组,其他的线程组都有父线程组
但是它有个方法, interrupt, 如果调用线程组的interrupt方法, 则该组内的所有线程都会被设置中断标志, 并且会递归设置它子线程组的线程
UncaughtExceptionHandler
线程的任务执行单元,Runnable 是不能抛出受检异常的, 所以,需要将受检异常包装成非受检异常.
另外, 如果想要对线程中抛出的异常进行处理, 可以给线程设置非受检异常处理器.
// 设置全局的
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh);
// 设置单个线程的
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh);
public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler();
public UncaughtExceptionHandler getUncaughtExceptionHandler()
当线程出现异常时, JVM 会调用dispatchUncaughtException 方法,该方法会将对应的线程实例和异常信息传递给 回调接口.
public class TestUncaughtException {
public static void main(String[] args) {
// 只是简单输出异常
Thread.setDefaultUncaughtExceptionHandler((t,e)->{
System.out.println(t.getName() + ": " + e);
});
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 模拟抛出异常异常
Exception e = new ClassNotFoundException("class Test does not exist");
throw new RuntimeException(e);
}).start();
}
}
======================输出===========================
Thread-0: java.lang.RuntimeException: java.lang.ClassNotFoundException: class Test does not exist
如果不主动设置异常处理器, 它又是怎么样处理的呢
我们看Thread的源码,可以看到如果没有设置异常处理器, 那么会返回该线程的分组group, group实现了UncaughtExceptionHandler接口
从它的接口方法中,可以看到, 如果group的parent不为空 ,则调用parent的该方法, 直到system group, 它是c 代码创建的, 没有父线程组
那么会尝试调用全局的, 如果还是没有, 则在标准异常输出中打印异常堆栈信息.
所以, 如果我们没有设置, 默认使用的就是在标准异常输出中打印异常堆栈信息
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
// group 中的回调方法
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
注入钩子线程
Hook 线程会在JVM进程退出的时候,启动执行, 可以向JVM注册多个Hook线程 来在进程结束时,执行一些操作,比如释放数据库链接, 关闭文件句柄等
// 简单示例
public class TestHook {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(()->{
System.out.println("执行收尾工作....");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
}));
System.out.println("准备结束进程");
}
}
线程池
之后再补吧