创建和运行线程

方法一:继承 Thread,重写 run() 方法

实际上是用 匿名内部类 实现了一个 Thread 的子类,override run() 方法

start() 让线程 runable,即让操作系统可以给它分配时间片

// 创建线程对象
Thread t = new Thread() {
 public void run() {
 // 要执行的任务
 }
};
// 启动线程
t.start();

方法二:implements Runnable 实现 run() 方法。 Thread(runnable)

把【线程】和【任务】(要执行的代码)分开

  • Runnable 可运行的任务(线程要执行的代码
  • Thread 代表线程 

对比:

  • 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
  • 用 Runnable 更容易与线程池等高级 API 配合
  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
Runnable runnable = new Runnable() {
 public void run(){
 // 要执行的任务
 }
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start(); 

java 8 以后可以使用 lambda 精简代码

一行代码可以不用大括号 {},多行时再加上

// 创建任务对象
Runnable task2 = () -> {log.debug("hello")};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

方法三:implements Callable<T> 实现 call() 方法 。FutureTask<T>(callable) Thread(futureTask)

Callable 与 Runnable 类似,只是 Callable 可以抛出异常,可以有返回值;而 Runnable 不行

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
 log.debug("hello");
 return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

 

查看进程与线程

linux

  • ps -ef 查看所有进程
  • ps -fT -p <PID> 查看某个进程(PID)的所有线程
  • top 按大写 H 切换是否显示线程
  • top -H -p <PID> 查看某个进程(PID)的所有线程

java

  • jps 查看所有 java 进程
  • jstack 查看 jvm 线程栈快照
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

 

栈与栈帧 Java Virtual Machine Stacks (Java 虚拟机栈)

  • 每个线程启动后,虚拟机就会为其分配一块栈内存,是线程私有的。
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存;每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

 

 

线程上下文切换(Thread Context Switch)

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统 保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等。
  • Context Switch 频繁发生会影响性能

 

start 与 run

调用 Run

public static void main(String[] args) {
   Thread t1 = new Thread("t1") {
     @Override
     public void run() {
       log.debug(Thread.currentThread().getName());
       // 读 MP4 文件,要花很多时间       FileReader.read(Constants.MP4_FULL_PATH);      }   };   t1.run();   log.debug(
"do other things ..."); }

输出

19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...

程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的,没有新起一个线程异步执行

调用 Start

将上述代码的 t1.run() 改为

t1.start();

输出

19:41:30 [main] c.TestStart - do other things ...
19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start ...
19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms

程序在 t1 线程运行, FileReader.read() 方法调用是异步的。

但 start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片可能还没分给它)

每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException。

System.out.printlin(t1.getState());
t1.start();
System.out.printlin(t1.getState());

NEW 状态 ——>   Runable 状态

 

sleep 与 yield sleep

sleep

(Thread.sleep(1000) 写在哪个线程中就对哪个睡眠)

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(因为 sleep 会设置一个超时时间,所以是有时限的阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行(要等待调度器把时间片分过来)
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性(有一个时间的单位,例 TimeUnit.SECONDS(3);)

用途:

单核 CPU 下,while (true) 里如果什么都不干, CPU 会空转占用会很快达到 100% 。这时 while(true) 里哪怕用 sleep(1) 也会大幅降低 cpu 占用

 

 

yield —— 让出 cpu 使用权

(英文意:让出,谦让)

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程(让出 cpu 使用权)
  2. 具体的实现依赖于操作系统的任务调度器(比如让出去之后,没有其它线程在运行,这时候 cpu 还是会让这个线程继续运行,也就是说想让但是没让出去

 

二者区别

sleep 后转为 Timed Waiting 状态,yield 后转位 Runable 状态,Runable 状态调度器还是可能会把时间片分过来,而  Timed Waiting 必须等到时间完成之后才会继续被调度

 

线程优先级

  • 线程优先级会提示(hint)操作系统调度器优先调度该线程

 

join —— 等待某线程结束

(底层原理就是 wait)

static int r = 0;
public static void main(String[] args) throws InterruptedException {
   test1();
}
private static void test1() throws InterruptedException {
   log.debug("开始");
   Thread t1 = new Thread(() -> {
     log.debug("开始");
     sleep(1);
     log.debug("结束");
     r = 10;
   });
   t1.start();
   // t1.join();   log.debug(
"结果为:{}", r);   log.debug("结束"); }

打印结果是 0

原因分析:

  • 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
  • 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0

解决:主线程调用 t1.join() 等 t1 线程运行结束后再打印 r 的结果 (上面那段代码注释的那一行)

注意:下面这段代码,虽然 t1 已经很大可能先于 t2 结束,但是主线程里,要等 t2 结束, t2.join() 执行完后, t1.join() 才会被执行,从而打印

 

 有时效的等待: join(long n)

最多等待 n 毫秒,如果大于 n 毫秒等待的线程还没有结束,就不会等待了,会继续往下运行

如果小于 n 毫秒等待的线程就已经结束了,这时候也不会继续等够 n 毫秒,会直接结束等待继续往下

 

interrupt 

打断后,会有一个 boolean 标志位标志是否线程被打断,sleep wait join 的线程被打断会恢复这个标志位为 false ,普通线程被打断后这个标志位为 true

t1.isInterrupted() 获取打断标记位的方法。调用后不会改变打断标记位

t1.interrupted() 也是一个获取打断标记位的方法,不同的是,它是静态方法,并且调用一次过后,就会清除打断标记位(置为 false)

1、打断状态为 sleep wait join 的处于阻塞线程 (抛出 InterruptException 的异常,打断标志位为 false)

 

2、打断正常运行状态的线程 (不抛异常,打断标志位为 true)

打断后,被打断的线程并不会停止运行,它只是知道有其它线程打断了它

 

要由 被打断的线程自己 来决定是否要继续运行

 

3、打断 park 状态的线程(不抛异常,打断标志位为 true)

 

打断标志位为 true 的线程,不可以再次被 park()

 

可以用 interrupted() 方法将打断标志位重置为 false,就可以再次被 park() 了

 

两阶段终止模式(Two phase termination)

在一个线程 T1 中如何优雅的终止线程 T2,这里的优雅是指给 T2 一个料理后事的机会

1、错误思路

  • 使用线程对象的 stop() 方法停止线程
    • stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
  • 使用 System.exit(int) 方法停止线程
    • 目的仅是停止一个线程,但这种做法会让整个程序都停止

2、两阶段终止

通常有监控线程,每隔几秒进行一次系统状态记录,会一直让它 while(true) 运行。但是必须有可以让它停止下来的方法

  • 情况1:如果在 sleep 时被打断了,那么就会有 InterruptException 异常,执行 catch(InterruptException e) 里的内容,这时打断标志位为 false,所以要重新打断一次,让它的标志位变为 true,这样下一次 while 循环,就会判断标志位然后继续走下面料理后事退出循环
  • 情况2:如果在正常执行时(如记录监控日志时)被打断了,那么打断标志位为 true,这样下一次 while 循环,就会判断标志位然后继续走下面的料理后事退出循环

 

 

不推荐使用的方法

还有一些不推荐使用的方法,这些方法已过时(Thread 源码已将其标记为 @Deprecated),容易破坏同步代码块,造成线程死锁

方法名 static 功能说明 替代
stop()   停止线程运行 interrupt 两阶段终止
suspend()   暂停,挂起线程运行 wait 暂停与 notify 唤醒
resume()   恢复线程运行 wait 暂停与 notify 唤醒

    

 

主线程与守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。

如下图,主线程已经结束,但是 t1 线程还未结束,所以 java 进程没有结束

 

有一种 特殊的线程 叫做 守护线程,只要 其它非守护线程 运行结束了,即使守护线程的代码没有执行完也会强制结束。

一般的线程默认都是 非守护线程,守护线程需要将线程 setDaemon(true)

如下图,将 t1 设为守护线程,所有非守护线程(这里只有一个主线程已结束)结束后,即使 t1 线程还在 while(true) 里面,也会结束

 

应用

  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求

 

 

五种状态

这是从 操作系统 层面来描述的

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行(等待获取时间片
  • 【运行状态】指 获取了 CPU 时间片运行中 的状态
    • CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】
    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】
    • 等 BIO 操作完毕,会由操作系统 唤醒 阻塞的线程,转换至【可运行状态】
    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器一直不会考虑 调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

 

六种状态

这是从 Java API 层面来描述的

根据 Thread.State 枚举,分为六种状态

 

 

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
  • BLOCKED (想获取 synchronized 的锁却获取不到), WAITING (join无时限的等待), TIMED_WAITING(sleep有时限的等待) 都是 Java API 层面对【阻塞状态】的细分(调度器同样不会分配时间片),后面会在状态转换一节 详述
  • TERMINATED 当线程代码运行结束