目录
[sleep()、 yield()](#sleep()、 yield())
线程是CPU调度的最小单位,它可以和属于同一个进程的其他线程共享这个进程的全部资源
创建线程的4种方式
1.创建继承于Thread类的子类,并重写Thread类的run()方法。
java
@Test
public void test4(){
System.out.println("主线程"+Thread.currentThread().getName()+"输出");
MyThread thread = new MyThread();
thread.start();
}
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("子线程"+Thread.currentThread().getName()+"输出");
}
}
-
优点:编码简单
-
缺点:线程类已经继承了 Thread 类无法继承其他类了,功能不能通过继承拓展(单继承的局限性)
2.创建一个实现了Runnable接口的类,并实现run()方法
java
@Test
public void test4(){
System.out.println("主线程"+Thread.currentThread().getName()+"输出");
MyRunnable thread = new MyRunnable();
new Thread(thread,"1号线程").start();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("子线程"+Thread.currentThread().getName()+"输出");
}
}
-
缺点:代码复杂一点。
-
优点:
-
线程任务类只是实现了 Runnable 接口,可以继续继承其他类,避免了单继承的局限性
-
同一个线程任务对象可以被包装成多个线程对象
-
适合多个多个线程去共享同一个资源
-
实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立
-
线程池可以放入实现 Runnable 或 Callable 线程任务对象
-
3.通过Callable和FutureTask创建线程
java
@Test
public void test4(){
System.out.println("主线程"+Thread.currentThread().getName()+"输出");
MyCallable thread = new MyCallable();
FutureTask<String> task = new FutureTask<>(thread);
Thread t = new Thread(task);
t.start();
try {
String s = task.get(); // 获取call方法返回的结果(正常/异常结果)
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
}
static class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return Thread.currentThread().getName() + "->" + "Hello World";
}
}
实现 Callable 接口:
-
定义一个线程任务类实现 Callable 接口,申明线程执行的结果类型
-
重写线程任务类的 call 方法,这个方法可以直接返回执行的结果
-
创建一个 Callable 的线程任务对象
-
把 Callable 的线程任务对象包装成一个未来任务对象
-
把未来任务对象包装成线程对象
-
调用线程的 start() 方法启动线程
public FutureTask(Callable<V> callable)
:未来任务对象,在线程执行完后得到线程的执行结果
- FutureTask 就是 Runnable 对象,因为 Thread 类只能执行 Runnable 实例的任务对象,所以把 Callable 包装成未来任务对象
public V get()
:同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步
-
get() 线程会阻塞等待任务执行完成
-
run() 执行完后会把结果设置到 FutureTask 的一个成员变量,get() 线程可以获取到该变量的值
优缺点:
-
优点:同 Runnable,并且能得到线程执行的结果
-
缺点:编码复杂
线程的生命周期
从操作系统层面:
- 新建(new):新创建了一个线程对象。
- 就绪(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
- 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
- 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被 CPU 调用以进入到运行状态。阻塞的情况分三种:
- 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
- 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入可运行状态。
- 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
从JavaAPI层面,Java中的线程状态可以参考Thread类中的一个枚举类state。
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征 |
Runnable(可运行) | 线程可以在 Java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法) |
Blocked(阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒 |
Timed Waiting (限期等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait |
Teminated(结束) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 |
线程状态之间的转换
现在有线程t
1、NEW --> RUNNABLE
当调用t.start()方法时, NEW --> RUNNABLE
2、RUNNABLE <--> WAITING:
(1)t线程用synchronized(obj)获取了对象锁后,调用 obj.wait()方法时,t 线程进入waitSet中, 从RUNNABLE --> WAITING。
(2)调用obj.notify(),obj.notifyAll(),t.interrupt()时, 唤醒的线程都到entrySet阻塞队列成为BLOCKED状态, 在阻塞队列,和其他线程再进行竞争锁,竞争锁成功,t 线程从 WAITING --> RUNNABLE,竞争锁失败,t 线程从 WAITING --> BLOCKED。
3、RUNNABLE <--> WAITING:
(1)当前线程(不是t线程)调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING。
(2)t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE。
4、RUNNABLE <--> WAITING
(1)当前线程调用 LockSupport.park() 方法会让当前线程从RUNNABLE --> WAITING
(2)调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从WAITING --> RUNNABLE
5、RUNNABLE <--> TIMED_WAITING
(1)t 线程用synchronized(obj)获取了对象锁后调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
(2)t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时; 唤醒的线程都到entrySet阻塞队列成为BLOCKED状态, 在阻塞队列,和其他线程再进行竞争锁,竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE,竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
6、RUNNABLE <--> TIMED_WAITING
(1)当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING (注意是当前线程在t 线程对象的waitSet等待)
(2)当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE
7、RUNNABLE <--> TIMED_WAITING
(1)当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
(2)当前线程等待时间超过了 n 毫秒或调用了线程的 interrupt() ,当前线程从 TIMED_WAITING --> RUNNABLE
8、RUNNABLE <--> TIMED_WAITING
(1)当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
(2)调用LockSupport.unpark(目标线程) 或调用了线程的interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE
9、RUNNABLE <--> BLOCKED
(1)t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
(2)持有 obj 锁的线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
10、RUNNABLE --> TERMINATED
当前线程所有代码运行完毕,进入 TERMINATED
线程方法
Thread 类 API:
方法 | 说明 |
---|---|
public void start() | 启动一个新线程,Java虚拟机调用此线程的 run 方法 |
public void run() | 线程启动后调用该方法 |
public void setName(String name) | 给当前线程取名字 |
public void getName() | 获取当前线程的名字 线程存在默认名称:子线程是 Thread-索引,主线程是 main |
public static Thread currentThread() | 获取当前线程对象,代码在哪个线程中执行 |
public static void sleep(long time) | 让当前线程休眠多少毫秒再继续执行 Thread.sleep(0) : 让操作系统立刻重新进行一次 CPU 竞争 |
public static native void yield() | 提示线程调度器让出当前线程对 CPU 的使用 |
public final int getPriority() | 返回此线程的优先级 |
public final void setPriority(int priority) | 更改此线程的优先级,常用 1 5 10 |
public void interrupt() | 中断这个线程,异常处理机制 |
public static boolean interrupted() | 判断当前线程是否被打断,清除打断标记 |
public boolean isInterrupted() | 判断当前线程是否被打断,不清除打断标记 |
public final void join() | 等待这个线程结束 |
public final void join(long millis) | 等待这个线程死亡 millis 毫秒,0 意味着永远等待 |
public final native boolean isAlive() | 线程是否存活(还没有运行完毕) |
public final void setDaemon(boolean on) | 将此线程标记为守护线程或用户线程 |
run()、start()
run:称为线程体,包含了要执行的这个线程的内容,方法运行结束,此线程随即终止。直接调用 run 是在主线程中执行了 run,没有启动新的线程,需要顺序执行
start:使用 start 是启动新的线程,此线程处于就绪(可运行)状态,通过新的线程间接执行 run 中的代码
run() 方法中的异常不能抛出,只能 try/catch
-
因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常
-
异常不能跨线程传播回 main() 中,因此必须在本地进行处理
sleep()、 yield()
sleep:
-
调用 sleep 会让当前线程从
Running
进入Timed Waiting
状态(阻塞) -
sleep() 方法的过程中,线程不会释放对象锁
-
其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
-
睡眠结束后的线程未必会立刻得到执行,需要抢占 CPU
-
建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield:
-
调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用
-
具体的实现依赖于操作系统的任务调度器
-
会放弃 CPU 资源,不会释放锁资源
join
public final void join():等待这个线程结束
原理:调用者轮询检查线程 alive 状态,thread.join() 等价于:
java
public final synchronized void join(long millis) throws InterruptedException {
// 调用者线程进入 thread 的 waitSet 等待, 直到当前线程运行结束
while (isAlive()) {
wait(0);
}
}
-
join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是释放的是当前的线程对象锁,而不是外面的锁
-
当调用某个线程(thread)的 join 方法后,该线程(thread)抢占到 CPU 资源,就不再释放,直到线程执行完毕
线程同步:
-
join 实现线程同步,因为会阻塞等待另一个线程的结束,才能继续向下运行
-
需要外部共享变量,不符合面向对象封装的思想
-
必须等待线程结束,不能配合线程池使用
-
-
Future 实现(同步):get() 方法阻塞等待执行结果
-
main 线程接收结果
-
get 方法是让调用线程同步等待
-
java
public static int r = 0;
@Test
public void test5() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
});
t1.start();
t1.join();//不等待线程执行结束,输出的10
System.out.println(r);
}
interrupt
打断线程
public void interrupt()
:打断这个线程,异常处理机制
public static boolean interrupted()
:判断当前线程是否被打断,打断返回 true,清除打断标记,连续调用两次一定返回 false
public boolean isInterrupted()
:判断当前线程是否被打断,不清除打断标记
打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(打断不是停止)
-
sleep、wait、join 方法都会让线程进入等待状态,打断线程会清空打断状态(false)
java@Test public void test5() throws InterruptedException { 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 }
-
打断正常运行的线程:不会清空打断状态(true)
java
@Test
public void test5() throws InterruptedException {
Thread t2 = new Thread(()->{
while(true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if(interrupted) {
break;
}
}
}, "t2");
t2.start();
Thread.sleep(500);
t2.interrupt();
System.out.println("打断状态: "+t2.isInterrupted());
}
打断 park
park 作用类似 sleep,打断 park 线程,不会清空打断状态(true)
java
@Test
public void test5() throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("park...");
LockSupport.park();
System.out.println("unpark...");
}, "t1");
t1.start();
Thread.sleep(2000);
t1.interrupt();
System.out.println("打断状态:" + Thread.currentThread().isInterrupted());//打断状态:true
}
如果打断标记已经是 true, 则 park 会失效
java
LockSupport.park();
System.out.println("unpark...");
LockSupport.park();//失效,不会阻塞
System.out.println("unpark...");//和上一个unpark同时执行
可以修改获取打断状态方法,使用 Thread.interrupted()
,清除打断标记
终止模式
终止模式之两阶段终止模式:Two Phase Termination
目标:在一个线程 T1 中如何优雅终止线程 T2?优雅指的是给 T2 一个后置处理器
错误思想:
-
使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
-
使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止
两阶段终止模式图示:
打断线程可能在任何时间,所以需要考虑在任何时刻被打断的处理方法:
java
public class Test {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
Thread.sleep(3500);
tpt.stop();
}
}
class TwoPhaseTermination {
private Thread monitor;
// 启动监控线程
public void start() {
monitor = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
Thread thread = Thread.currentThread();
if (thread.isInterrupted()) {
System.out.println("后置处理");
break;
}
try {
Thread.sleep(1000); // 睡眠
System.out.println("执行监控记录"); // 在此被打断不会异常
} catch (InterruptedException e) { // 在睡眠期间被打断,进入异常处理的逻辑
e.printStackTrace();
// 重新设置打断标记,打断 sleep 会清除打断状态
thread.interrupt();
}
}
}
});
monitor.start();
}
// 停止监控线程
public void stop() {
monitor.interrupt();
}
}
daemon
public final void setDaemon(boolean on)
:如果是 true ,将此线程标记为守护线程
线程启动前调用此方法:
java
Thread t = new Thread() {
@Override
public void run() {
System.out.println("running");
}
};
// 设置该线程为守护线程
t.setDaemon(true);
t.start();
用户线程:平常创建的普通线程
守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示
说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,JVM 是守护线程,不会继续运行下去
常见的守护线程:
-
垃圾回收器线程就是一种守护线程
-
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
线程原理
运行机制
Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚拟机就会为其分配一块栈内存
-
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
-
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch):一些原因导致 CPU 不再执行当前线程,转而执行另一个线程
-
线程的 CPU 时间片用完
-
垃圾回收
-
有更高优先级的线程需要运行
-
线程自己调用了 sleep、yield、wait、join、park 等方法
程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
JVM 规范并没有限定线程模型,以 HotSopot 为例:
-
Java 的线程是内核级线程(1:1 线程模型),每个 Java 线程都映射到一个操作系统原生线程,需要消耗一定的内核资源(堆栈)
-
线程的调度是在内核态运行的,而线程中的代码是在用户态运行,所以线程切换(状态改变)会导致用户与内核态转换进行系统调用,这是非常消耗性能
Java 中 main 方法启动的是一个进程也是一个主线程,main 方法里面的其他线程均为子线程,main 线程是这些线程的父线程
线程调度
线程调度指系统为线程分配处理器使用权的过程,方式有两种:协同式线程调度 、抢占式线程调度(Java 选择)
协同式线程调度:线程的执行时间由线程本身控制
-
优点:线程做完任务才通知系统切换到其他线程,相当于所有线程串行执行,不会出现线程同步问题
-
缺点:线程执行时间不可控,如果代码编写出现问题,可能导致程序一直阻塞,引起系统的奔溃
抢占式线程调度:线程的执行时间由系统分配
-
优点:线程执行时间可控,不会因为一个线程的问题而导致整体系统不可用
-
缺点:无法主动为某个线程多分配时间
Java 提供了线程优先级的机制,优先级会提示调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它。在线程的就绪状态时,如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但 CPU 闲时,优先级几乎没作用。
说明:并不能通过优先级来判断线程执行的先后顺序