一、线程的创建
Java中主要有继承Thread类、实现Runnable接口、实现Callable接口、线程池ThreadPoolExecutor来进行创建
1.继承Thread类创建线程
package panw.base;import lombok.extern.slf4j.Slf4j;@Slf4jpublic class ThreadTest extends Thread {private int a;@Overridepublic void run() {while (true) {sleep(1000);log.debug("runTimes: " + a);}}public static void sleep(long time){try {Thread.sleep(time);} catch (InterruptedException e) {throw new RuntimeException(e);}}public static void main(String[] args) {ThreadTest threadTest = new ThreadTest();threadTest.start();}}
2.实现Runnable接口
package panw.base;import lombok.extern.slf4j.Slf4j;@Slf4jpublic class RunnableTest implements Runnable {private int id;@Overridepublic void run() {while (true) {if (id<=10){log.debug("runTimes: {}", id);id++;}}}public static void main(String[] args) {new Thread(new RunnableTest()).start();}}
3.实现Callable接口
在某些情况下,我们需要线程运行结束提供给主线程一个返回值,主线程拿到值之后进行后续的处理,那么我们就需要用到Callable接口,通常Callable接口配合线程池来进行使用,Java中提供了如下的实现方式:
package panw.base;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.Future;import java.util.concurrent.FutureTask;@Slf4jpublic class CallableTest implements Callable<String> {@Overridepublic String call() throws Exception {return "123";}public static void main(String[] args) {FutureTask<String> future = new FutureTask<>(new CallableTest());try {new Thread(future).start();while (!future.isDone()) {}log.debug(future.get());} catch (Exception e) {e.printStackTrace();}}}
总结:
使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递。不好的地方是Java不支持多继承,如果继承了Thread类,那么子类不能再继承其他类,而Runable则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是Futuretask方式可以
二、线程的生命周期
此处在网上有两种说法,五种生命周期是在操作系统层面,六种生命周期则是Java定义的。
1.五种生命周期
包括 新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。
- 新建状态(New):
创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,与其他Java对象一样,仅仅由Java虚拟机为其分配了内存。
- 就绪状态(Runnable)
当线程对象调用了start()方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度。
- 运行状态(Running)
如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会让出该线程占用的CPU资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。
- 阻塞状态(Blocked)
一个正在执行的线程在某些特殊情况下,如被人为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进人阻塞状态。线程进人阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
- 死亡状态(Terminated)
如果线程调用stop()方法或nun()方法正常执行完毕,或者线程抛出一个未捕获的异常(Exception)错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。
2.六种生命周期

- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,如sleep就位TIMED_WAITING, join为WAITING状态。后面会在状态转换一节详述。
-
三、线程常用方法
1.线程启动
启动一个线程为啥是
start(),而不是run()?
调用start()实际上是调用了一个native方法start0()来启动一个线程,而run()方法只是单纯调用了Runnable中的run()方法;public class Thread implements Runnable {/* Make sure registerNatives is the first thing <clinit> does. */private static native void registerNatives();...static {registerNatives();}@Overridepublic void run() {if (target != null) {target.run();}}public synchronized void start() {if (threadStatus != 0)throw new IllegalThreadStateException();group.add(this);boolean started = false;try {start0();started = true;} finally {try {if (!started) {group.threadStartFailed(this);}} catch (Throwable ignore) {}}}private native void start0();}
2.sleep()与yield()
sleep (使线程阻塞)
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),可通过state()方法查看
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 。如:
//休眠一秒TimeUnit.SECONDS.sleep(1);//休眠一分钟TimeUnit.MINUTES.sleep(1);
yield (让出当前线程)
调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态(仍然有可能被执行),然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
- 设置方法:
thread1.setPriority(Thread.MAX_PRIORITY); //设置为优先级最高
3.join()方法
用于等待某个线程结束。哪个线程内调用join()方法,就等待哪个线程结束,然后再去执行其他线程。
如在主线程中调用ti.join(),则是主线程等待t1线程结束 ```java package panw.base;
import lombok.extern.slf4j.Slf4j;
@Slf4j public class JoinTest { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 100; i++) { log.debug(“t1: “ + i); } }); t1.start(); Thread.sleep(10); t1.join(); log.debug(“main run”);
}
}
<a name="eYGxj"></a>## 4.线程终止线程终止不是简单的调用`stop()`方法,和其他控制线程方法如`suspend()、resume()`,这些方法已经过时,就拿stop来说,在结束一个线程时并不会保证线程的资源正常释放,因此可能导致程序出现一些不确定的状态。<br />因此要中断一个线程,在线程中提供了一个`interrupt()`方法:<a name="rhb5e"></a>### 1)interrupt()其他线程通过调用当前线程的`interrupt()`方法,表示向当前线程打了招呼,告诉该线程可以中断线程的执行了,至于什么时候中断,取决于自己。<br />线程也可以通过检查自身 通过 `isInterrupted()`方法来判断是否被中断。```javapackage panw.base;import lombok.extern.slf4j.Slf4j;@Slf4jpublic class InterruptTest {private static volatile int count;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {count++;log.debug("count: {}", count);}}, "interrupt");t1.start();try {Thread.sleep(1000);} catch (InterruptedException e) {}t1.interrupt();}}
2)中断标识复位
上面的interrupt()方法,通过设置一个中断标识告诉线程可以停止,线程中还提供了一个静态方法Thread.interrupted()来对中断标识线程复位;
package panw.base;import lombok.extern.slf4j.Slf4j;@Slf4jpublic class InterruptedTest {public static void main(String[] args) {Thread t1 = new Thread(() -> {while (true) {if (Thread.currentThread().isInterrupted()) {log.debug("-----before:interrupted is " + Thread.currentThread().isInterrupted());Thread.interrupted();log.debug("-----after:interrupted is " + Thread.currentThread().isInterrupted());}}}, "Interrupted");t1.start();try {Thread.sleep(50);} catch (InterruptedException e) {}t1.interrupt();}}
3)异常复位
除了上面的Thread.interrupted()方法对线程中断标识进行复位以外,还有一个被动的复位方式,就是当抛出InterruptedException之前,JVM会先把线程的中断标识为清除,然后才抛出InterruptedException,这时候调用isInterrupted(),将会返回false。
package panw.base;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;@Slf4jpublic class InterruptedTest {public static void main(String[] args) {Thread t1 = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {try {log.debug("未被阻塞");TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {log.debug("isInterrupted:"+Thread.currentThread().isInterrupted());e.printStackTrace();}}}, "Interrupt");t1.start();try {Thread.sleep(2000);} catch (InterruptedException e) {}t1.interrupt();}}
Object.wait、Thread.sleep 和 Thread.join 在被打断时 都 会 抛 出InterruptedException,打断park线程则直接会直接unpark,并且再次unpark不会起效。
4)两阶段终止模式-interrupt
两阶段终止模式不是23种传统设计模式中的,它是由 黄文海在《Java多线程编程实战指南 设计模式》中所提到的模式,具体思路如下图:
这里使用interrupt来进行简单实现:
package panw.model;import lombok.extern.slf4j.Slf4j;@Slf4jpublic class TwoStageTermination {private Thread monitor;public void start(){this.monitor = new Thread(() -> {while (true) {if (Thread.currentThread().isInterrupted()) {log.debug("after interrupted--------------do something");break;}try {Thread.sleep(2000);log.debug("TwoStageTermination: Thread sleeping");} catch (InterruptedException e) {e.printStackTrace();Thread.currentThread().interrupt();}}});monitor.start();}public void stop(){monitor.interrupt();}public static void main(String[] args) {TwoStageTermination twoStageTermination = new TwoStageTermination();twoStageTermination.start();try {Thread.sleep(3500);} catch (InterruptedException e) {e.printStackTrace();}twoStageTermination.stop();}}
5.守护线程
当JAVA进程中有多个线程在执行时,只有当所有非守护线程都执行完毕后,JAVA进程才会结束。但当非守护线程全部执行完毕后,守护线程无论是否执行完毕,也会一同结束。
//将线程设置为守护线程, 默认为falsemonitor.setDaemon(true);
守护线程的应用
- 垃圾回收器线程就是一种守护线程
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求
四、线程执行原理
JVM中由堆、栈、方法区所组成,其中栈内存就是分配给线程使用的,每个线程启动后,虚拟机都会为其分配一块栈内存。
每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的方法
这里就不深入研究,等到JVM章再进行讲解。
