深入学习掌握JUC并发编程系列(二) -- 梳理Java线程的创建和状态
一、创建和运行线程
- 匿名内部类(Anonymous Classes):没有名字的在类内部定义的新的类(无需创建新的类,减少代码冗余)
- 条件:
- 必须继承一个父类或者实现一个接口
- 父类或者父接口当中的方法只使用一次
- 创建方式:通过创建子类对象
- 条件:
new 父类名或者父接口名() {
// 方法的重写
@Override
public void method(){
// 重写方法的语句
}
};
注意事项:
1. 匿名内部类在创建对象时,只能使用唯一一次(当需要多次创建对象,而且类的内容是一样的话,建议单独定义实现类)
2. 匿名对象,在调用方法时,只能使用唯一一次(当需要同一个对象,调用多次方法,建议给对象起个名字)
3. 匿名内部类省略了实现类/子类名称,匿名对象省略了对象名
- lambda表达式:创建只有一个抽象方法的接口的实例,省略new接口名和抽象方法的写法
- 条件:函数式接口(Functional Interface):接口中只有一个抽象方法
- 写法:(形式参数)->{ 执行语句};
- 可选:
- 参数圆括号可选:一个参数无需圆括号,多个参数需要圆括号
- 大括号可选:主体包含一个语句,不需要使用大括号
- 返回关键字可选:主体只有一个返回值则编译器会自动返回值,带大括号需要指定返回值
1. 创建线程方法
- 直接使用Thread:线程创建和执行任务合并在一起
- new一个Thread对象
- 使用匿名内部类创建Thread子类
- 重写run()方法
- 启动线程
// 创建线程对象
Thread t = new Thread("线程名字") {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
- 使用Runnable配合Thread:将创建线程和要执行的任务分开
- 创建Runnable对象
- 使用匿名内部类,实现Runnable接口
- 将Runnable对象作为参数传递给Thread构造方法
- 创建线程对象
- 启动线程
Runnable task = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( task, "线程名字" );
// 启动线程
t.start();
- 使用 lambda 表达式精简代码(Java8之后支持)
// 创建任务对象
Runnable task = () -> {log.debug("running"); };
// 参数1 :任务对象;参数2:线程名字
Thread t = new Thread(task, "t1");
t.start();
//直接将lambda表达式作为创建线程对象的参数传入
Thread t = new Thread(() -> {log.debug("running"); }, "t1");
t.start();
2. 原理
- Thread与Runnable的关系
- 方法一:在子类中重写了父类(Thread类)的run()方法
- 方法二:通过传入Runnable对象,调用父类中Runnable对象的run()方法
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
- 用 Runnable 更容易与线程池等高级 API 配合(推荐使用Runnable对象,而不是直接使用Thread对象)
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活(推荐使用组合的关系,而不是继承关系)
- 方法三(类似方法二)
- 使用FutureTask配合Thread:
- 创建Callable对象,重写call()方法(可通过lambda表达式简化)
- 以Callable对象为参数创建FutureTask对象
- 将FutureTask对象作为参数传递给Thread构造方法,创建线程对象
- 调用get()方法,获取返回结果
- 使用FutureTask配合Thread:
// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
//要执行的任务
return 100;
});
// task是任务对象
Thread t1 = new Thread(task, "t1");
t1.start();
// 主线程阻塞,同步等待task执行完毕的返回结果
Integer result = task.get();
log.debug("返回结果是:{}", result);
- 原理:
- FutureTask间接实现了Runnable接口,所以可以将任务对象(Callable)作为参数传入Thread构造方法中
- 还间接实现了Future接口,通过该接口中的get()方法可以获取任务执行结果
- 接收Callable类型的参数,相比Runnable,可以抛出异常,还带有返回结果(重写call()方法)
二、查看进程线程的方法
- Windows:
- 通过任务管理器查看
- 控制台(cmd)命令:tasklist(查看进程) taskkill(杀死进程)
- Linux:
- ps -fe:查看所有正在运行的进程( | grep java:筛选)
- ps -fT -p PID:查看某个进程的所有线程
- Kill PID:杀死进程
- top:动态显示所有正在运行的进程(按cpu占用率排序)
- top -H -p PID:动态显示某个进程的所有线程
- Java:
- jdk自带的命令:jps、jstack(查看所有进程、查看某个java进程的所有线程)
- 图形化工具:jconsole(连接java进程,查看所有线程信息)
三、线程运行的原理
1. 任务调度器
- 为操作系统服务(不受JVM控制),为Runnable状态的线程分配CPU时间片
- 基于线程优先级或者线程等待时间
2. 栈与栈帧
- 线程启动后,虚拟机就会分配一块栈内存
- 每个栈由多个栈帧(frame)组成,栈帧对应着方法调用
- 每个线程只能有一个活动栈帧,位于栈顶(当前栈帧,对应着正在运行的方法)
3. 线程上下文切换(Thread Context Switch)
- 定义:CPU不再执行当前线程,转而执行另一个线程
- 产生原因:
- 被动:
- 线程的CPU时间片用完(任务调度器)
- 垃圾回收(暂停用户线程,执行gc线程)
- 更高优先级的线程需要运行
- 主动:线程自己调用了sleep、yield、wait、join、pack、lock、synchronized等方法
- 被动:
- 程序计数器:当发生线程上下文切换时,可以保存当前线程的状态(记录行号指示器)
- 避免频繁的线程上下文切换(影响性能)
四、Thread类常见方法
五、start()与run()
- 相同:都是线程类的方法
- 不同:
- start方法表示启动线程,run方法表示线程启动后执行的代码(执行任务)
- start方法只能执行一次,run方法可以重复调用
- 不start,直接调用run方法是在主线程中执行了run方法,没有启动新的线程(无法实现异步处理)
六、sleep()与yield()
- sleep:放弃当前线程的CPU时间片
- 让当前线程状态从Running进入Timed Waiting状态(阻塞状态)
- 其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException异常
- 睡眠结束后的线程未必会立刻被执行(任务调度器分配)
- 建议用TimeUnit类的sleep方法(包含时间单位,可读性更好)
- yield:让出当前线程的CPU使用权
- 让当前线程状态从Running进入Runnable状态(就绪状态)
- 任务调度器重新分配时间片,调度线程执行
- 区别:
- 优先级:
- sleep方法放弃CPU使用权,会给低优先级的线程执行机会
- yield方法让出CPU使用权,只会给相同或更高优先级线程执行机会
- 状态:
- sleep方法让线程进入Timed Waiting状态(阻塞 blocked)
- yield方法让线程进入Runnable状态(就绪 ,有机会重新被任务调度器分配时间片)
- 异常:
- sleep方法被打断(interrupt)后抛出InterruptedException异常
- yield方法没有声明异常
- 不推荐:yield依赖任务调度器(操作系统层面)实现,可移植性差
- 优先级:
七、线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程(仅仅是一个提示,调度器可以忽略)
- CPU比较忙,优先级高的线程会获得更多的时间片;CPU比较闲时,优先级几乎没作用
- setPriority,优先级范围[1, 10],默认为5
- yield和线程优先级只是给任务调度器一个提示,真正CPU时间片的分配还是由任务调度器实现
八、join()方法(同步)
- 主线程同步等待t1线程
- 等待多个线程结果(t1,t2),join(long n):有时效的等待(最多等待n毫秒)
九、interrupt()方法
- 打断标记:线程是否被其它线程打断过(打断过:True),通过isInterrupted()方法获取
- 打断阻塞状态的线程(sleep,wait,join),清空打断标记(为False),并抛出异常
- 打断正常运行的线程,不会清空打断标记(为True),不会终止线程运行(通过打断标记,由被打断的线程自己决定是否继续运行)
private static void test2() throws InterruptedException {
Thread t2 = new Thread(()->{
while(true) {
Thread current = Thread.currentThread(); //获取当前线程
boolean interrupted = current.isInterrupted(); //获取打断标记
if(interrupted) {
log.debug(" 打断状态: {}", interrupted);
break;
}
}
}, "t2");
t2.start();
sleep(0.5);
t2.interrupt();
}
- 打断park线程:不会清空打断标记(为True),线程会继续运行
- 打断标记为True时,park会失效,可以使用interrupted方法重新实现park
- Thread.interrupted() 方法会返回打断标记,并清除打断标记(为False)
十、不推荐使用的方法
方法已过时(@Deprecated),容易破坏同步代码块,造成线程死锁
十一、主线程与守护线程
- Java进程结束运行,需要等待所有线程执行完毕
- 主线程:main线程
- 守护线程(Daemon):其它非守护线程运行结束后,守护线程强制结束(即使未运行完)
- t1.setDaemon(True):将t1线程设为守护线程(必须在start()方法前)
垃圾回收线程就是一种守护线程 - Tomcat 中的 Acceptor 和 Poller 线程都是守护线程
- t1.setDaemon(True):将t1线程设为守护线程(必须在start()方法前)
十二、线程的状态
- 五种状态(操作系统层面):
- 初始状态:创建线程对象,还未与操作系统线程关联
- 可运行状态(就绪状态):线程已经被创建(与操作系统线程关联),准备分到CPU时间片
- 运行状态:获取CPU时间片,正在运行(当时间片用完,运行状态转为可运行状态,导致线程上下文切换)
- 阻塞状态:调用了阻塞API,线程不会分到CPU时间片(任务调度器不考虑),导致线程上下文切换
- 终止状态:线程执行完毕,生命周期结束
- 六种状态(Java API层面 Thread类中的State枚举类)
- NEW:线程刚被创建,还未调用start方法运行(对应初始状态)
- RUNNABLE:调用start方法之后(对应可运行状态、运行状态、阻塞状态)
- BLOCKED、WAITING、TIMED_WAITING(对应阻塞状态)
- TERMINATED:线程执行完毕(对应终止状态)
- t1.getState()方法:获取线程状态
总结
- 线程创建的三种方法:Thread子类、Runnable对象、FutureTask
- 线程重要api:start、run、sleep、join(同步等待)、interrupt(打断标记)
- 线程状态:操作系统层面(5种)、java层面(6种)
- 原理:
- 线程运行流程(虚拟机栈、栈帧、线程上下文切换、程序计数器)
- 线程创建方式(源码)