一、线程的创建

Java中主要有继承Thread类、实现Runnable接口、实现Callable接口、线程池ThreadPoolExecutor来进行创建

1.继承Thread类创建线程

  1. package panw.base;
  2. import lombok.extern.slf4j.Slf4j;
  3. @Slf4j
  4. public class ThreadTest extends Thread {
  5. private int a;
  6. @Override
  7. public void run() {
  8. while (true) {
  9. sleep(1000);
  10. log.debug("runTimes: " + a);
  11. }
  12. }
  13. public static void sleep(long time){
  14. try {
  15. Thread.sleep(time);
  16. } catch (InterruptedException e) {
  17. throw new RuntimeException(e);
  18. }
  19. }
  20. public static void main(String[] args) {
  21. ThreadTest threadTest = new ThreadTest();
  22. threadTest.start();
  23. }
  24. }

2.实现Runnable接口

  1. package panw.base;
  2. import lombok.extern.slf4j.Slf4j;
  3. @Slf4j
  4. public class RunnableTest implements Runnable {
  5. private int id;
  6. @Override
  7. public void run() {
  8. while (true) {
  9. if (id<=10){
  10. log.debug("runTimes: {}", id);
  11. id++;
  12. }
  13. }
  14. }
  15. public static void main(String[] args) {
  16. new Thread(new RunnableTest()).start();
  17. }
  18. }

3.实现Callable接口

在某些情况下,我们需要线程运行结束提供给主线程一个返回值,主线程拿到值之后进行后续的处理,那么我们就需要用到Callable接口,通常Callable接口配合线程池来进行使用,Java中提供了如下的实现方式:

  1. package panw.base;
  2. import lombok.extern.slf4j.Slf4j;
  3. import java.util.concurrent.Callable;
  4. import java.util.concurrent.ExecutionException;
  5. import java.util.concurrent.Future;
  6. import java.util.concurrent.FutureTask;
  7. @Slf4j
  8. public class CallableTest implements Callable<String> {
  9. @Override
  10. public String call() throws Exception {
  11. return "123";
  12. }
  13. public static void main(String[] args) {
  14. FutureTask<String> future = new FutureTask<>(new CallableTest());
  15. try {
  16. new Thread(future).start();
  17. while (!future.isDone()) {
  18. }
  19. log.debug(future.get());
  20. } catch (Exception e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. }

总结:

使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递。不好的地方是Java不支持多继承,如果继承了Thread类,那么子类不能再继承其他类,而Runable则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是Futuretask方式可以

二、线程的生命周期

此处在网上有两种说法,五种生命周期是在操作系统层面,六种生命周期则是Java定义的。

1.五种生命周期

包括 新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。
并发编程基础 - 图1

  • 新建状态(New):

创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,与其他Java对象一样,仅仅由Java虚拟机为其分配了内存。

  • 就绪状态(Runnable)

当线程对象调用了start()方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度。

  • 运行状态(Running)

如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会让出该线程占用的CPU资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。

  • 阻塞状态(Blocked)

一个正在执行的线程在某些特殊情况下,如被人为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进人阻塞状态。线程进人阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

  • 死亡状态(Terminated)

如果线程调用stop()方法或nun()方法正常执行完毕,或者线程抛出一个未捕获的异常(Exception)错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。

2.六种生命周期

6种线程状态.png

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,如sleep就位TIMED_WAITING, join为WAITING状态。后面会在状态转换一节详述。
  • TERMINATED 当线程代码运行结束

    三、线程常用方法

    1.线程启动

    启动一个线程为啥是start(),而不是run()
    调用start()实际上是调用了一个native方法start0()来启动一个线程,而run()方法只是单纯调用了Runnable中的run()方法;

    1. public class Thread implements Runnable {
    2. /* Make sure registerNatives is the first thing <clinit> does. */
    3. private static native void registerNatives();
    4. ...
    5. static {
    6. registerNatives();
    7. }
    8. @Override
    9. public void run() {
    10. if (target != null) {
    11. target.run();
    12. }
    13. }
    14. public synchronized void start() {
    15. if (threadStatus != 0)
    16. throw new IllegalThreadStateException();
    17. group.add(this);
    18. boolean started = false;
    19. try {
    20. start0();
    21. started = true;
    22. } finally {
    23. try {
    24. if (!started) {
    25. group.threadStartFailed(this);
    26. }
    27. } catch (Throwable ignore) {
    28. }
    29. }
    30. }
    31. private native void start0();
    32. }

    2.sleep()与yield()

    sleep (使线程阻塞)

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),可通过state()方法查看
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 。如:

    1. //休眠一秒
    2. TimeUnit.SECONDS.sleep(1);
    3. //休眠一分钟
    4. TimeUnit.MINUTES.sleep(1);

    yield (让出当前线程)

  5. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态(仍然有可能被执行),然后调度执行其它线程

  6. 具体的实现依赖于操作系统的任务调度器

    线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
  • 设置方法:
    1. 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”);

  1. }

}

  1. <a name="eYGxj"></a>
  2. ## 4.线程终止
  3. 线程终止不是简单的调用`stop()`方法,和其他控制线程方法如`suspend()、resume()`,这些方法已经过时,就拿stop来说,在结束一个线程时并不会保证线程的资源正常释放,因此可能导致程序出现一些不确定的状态。<br />因此要中断一个线程,在线程中提供了一个`interrupt()`方法:
  4. <a name="rhb5e"></a>
  5. ### 1)interrupt()
  6. 其他线程通过调用当前线程的`interrupt()`方法,表示向当前线程打了招呼,告诉该线程可以中断线程的执行了,至于什么时候中断,取决于自己。<br />线程也可以通过检查自身 通过 `isInterrupted()`方法来判断是否被中断。
  7. ```java
  8. package panw.base;
  9. import lombok.extern.slf4j.Slf4j;
  10. @Slf4j
  11. public class InterruptTest {
  12. private static volatile int count;
  13. public static void main(String[] args) {
  14. Thread t1 = new Thread(() -> {
  15. while (!Thread.currentThread().isInterrupted()) {
  16. count++;
  17. log.debug("count: {}", count);
  18. }
  19. }, "interrupt");
  20. t1.start();
  21. try {
  22. Thread.sleep(1000);
  23. } catch (InterruptedException e) {
  24. }
  25. t1.interrupt();
  26. }
  27. }

2)中断标识复位

上面的interrupt()方法,通过设置一个中断标识告诉线程可以停止,线程中还提供了一个静态方法Thread.interrupted()来对中断标识线程复位;

  1. package panw.base;
  2. import lombok.extern.slf4j.Slf4j;
  3. @Slf4j
  4. public class InterruptedTest {
  5. public static void main(String[] args) {
  6. Thread t1 = new Thread(() -> {
  7. while (true) {
  8. if (Thread.currentThread().isInterrupted()) {
  9. log.debug("-----before:interrupted is " + Thread.currentThread().isInterrupted());
  10. Thread.interrupted();
  11. log.debug("-----after:interrupted is " + Thread.currentThread().isInterrupted());
  12. }
  13. }
  14. }, "Interrupted");
  15. t1.start();
  16. try {
  17. Thread.sleep(50);
  18. } catch (InterruptedException e) {
  19. }
  20. t1.interrupt();
  21. }
  22. }

3)异常复位

除了上面的Thread.interrupted()方法对线程中断标识进行复位以外,还有一个被动的复位方式,就是当抛出InterruptedException之前,JVM会先把线程的中断标识为清除,然后才抛出InterruptedException,这时候调用isInterrupted(),将会返回false。

  1. package panw.base;
  2. import lombok.extern.slf4j.Slf4j;
  3. import java.util.concurrent.TimeUnit;
  4. @Slf4j
  5. public class InterruptedTest {
  6. public static void main(String[] args) {
  7. Thread t1 = new Thread(() -> {
  8. while (!Thread.currentThread().isInterrupted()) {
  9. try {
  10. log.debug("未被阻塞");
  11. TimeUnit.SECONDS.sleep(1);
  12. } catch (InterruptedException e) {
  13. log.debug("isInterrupted:"+Thread.currentThread().isInterrupted());
  14. e.printStackTrace();
  15. }
  16. }
  17. }, "Interrupt");
  18. t1.start();
  19. try {
  20. Thread.sleep(2000);
  21. } catch (InterruptedException e) {
  22. }
  23. t1.interrupt();
  24. }
  25. }

Object.waitThread.sleepThread.join 在被打断时 都 会 抛 出InterruptedException,打断park线程则直接会直接unpark,并且再次unpark不会起效。

4)两阶段终止模式-interrupt

两阶段终止模式不是23种传统设计模式中的,它是由 黄文海在《Java多线程编程实战指南 设计模式》中所提到的模式,具体思路如下图:
并发编程基础 - 图3
这里使用interrupt来进行简单实现:

  1. package panw.model;
  2. import lombok.extern.slf4j.Slf4j;
  3. @Slf4j
  4. public class TwoStageTermination {
  5. private Thread monitor;
  6. public void start(){
  7. this.monitor = new Thread(() -> {
  8. while (true) {
  9. if (Thread.currentThread().isInterrupted()) {
  10. log.debug("after interrupted--------------do something");
  11. break;
  12. }
  13. try {
  14. Thread.sleep(2000);
  15. log.debug("TwoStageTermination: Thread sleeping");
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. Thread.currentThread().interrupt();
  19. }
  20. }
  21. });
  22. monitor.start();
  23. }
  24. public void stop(){
  25. monitor.interrupt();
  26. }
  27. public static void main(String[] args) {
  28. TwoStageTermination twoStageTermination = new TwoStageTermination();
  29. twoStageTermination.start();
  30. try {
  31. Thread.sleep(3500);
  32. } catch (InterruptedException e) {
  33. e.printStackTrace();
  34. }
  35. twoStageTermination.stop();
  36. }
  37. }

5.守护线程

当JAVA进程中有多个线程在执行时,只有当所有非守护线程都执行完毕后,JAVA进程才会结束。但当非守护线程全部执行完毕后,守护线程无论是否执行完毕,也会一同结束。

  1. //将线程设置为守护线程, 默认为false
  2. monitor.setDaemon(true);

守护线程的应用

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

    四、线程执行原理

    JVM中由堆、栈、方法区所组成,其中栈内存就是分配给线程使用的,每个线程启动后,虚拟机都会为其分配一块栈内存。

  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

这里就不深入研究,等到JVM章再进行讲解。