线程的创建

  • 继承Thread类
public class Test1 {
    public static void main(String[] args) {
        var t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running");
            }
        };
        t1.start();
        log.debug("running");
    }
}
  • 实现Runnable接口
@Slf4j(topic = "c.Test2")
public class Test2 {
    public static void main(String[] args) {
        Runnable t2 = () -> {
            log.debug("running");
        };
        new Thread(t2, "t2").start();
        log.debug("running");
    }

}
  • 通过FutureTask和Callable接口
@Slf4j(topic = "c.Test3")
public class Test3 {
    public static void main(String[] args) {
        var task = new FutureTask<>(() -> {
            log.debug("running");
            return 1;
        });
        new Thread(task, "t1").start();
        try {
            log.debug("result:{}", task.get());
        } catch (InterruptedException | ExecutionException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
        log.debug("running");
    }
}

Thread与Runnable接口的关系

image-1666708245552
在Thread的成员中有一个名为target的Runnable成员,二者为组合的关系,同时Thread也实现了Runable接口
image-1666708320846
当我们调用Thread的run()方法时实际上是在调用内部Runnable接口的run()方法,这实际是一种代理模式,实际执行run的都是传入的Runable接口,接着我们查看传入的FutureTask是个什么样子的
image-1666708499753
FutureTask继承于RunnableFuture,我们继续查看RunnableFuture的结构,
image-1666708608182
RunnableFuture也是继承了Runnable接口,也就是说三种方法都实现了Runnable任务与Thread体系的解耦

查看正在运行线程的方法

  • windows
    • tasklist 查看线程
    • taskkill 杀死线程
  • linux
    • ps -fe 查看所有线程
    • ps -fT -p 查看某个进程(pid)的所有线程
    • kill -9 直接杀死线程
    • kill -15 优雅杀死线程
    • top 按大写H切换是否显示线程
    • top -H -p 查看某个进程(pid)的所有线程
  • java
    • jpa 查看所有java线程
    • jstack 查看某个 Java 进程(PID)的所有线程状态
    • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

run()与start()

直接调用run()方法实际只是调用了这个类的方法,并没有将线程启动,start()方法则是启动新的线程启动执行run()中的方法

@Slf4j(topic = "c.Test4")
public class Test4 {
  public static void main(String[] args) {
      var t1 = new Thread(() -> {
          log.debug("running");
          log.debug(Thread.currentThread().getName());
      }, "t1");
      log.debug("t1 state:{}", t1.getState());
      t1.start();
      log.debug("t1 state:{}", t1.getState());
      log.debug("do something");
      var t2 = new Thread(() -> {
          log.debug("running");
          log.debug(Thread.currentThread().getName());
      }, "t2");
      log.debug("t2 state:{}", t2.getState());
      t2.run();
      log.debug("t2 state:{}", t2.getState());
  }
}

image-1666709431557

sleep()与yield()

sleep

  1. sleep()调用sleep会让当前线程从Runnig进入Timed Waiting状态(阻塞)
  2. 其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议使用TimeUnit的sleep代替Thread的sleep来获得更好的可读性
@Slf4j(topic = "c.Test5")
public class Test5 {
  public static void main(String[] args) {
      var t1 = new Thread(() -> {
          try {
              Thread.sleep(2000);
          } catch (InterruptedException e) {
              throw new RuntimeException(e);
          }
      });
      log.debug("t1 state:{}", t1.getState());
      t1.start();
      try {
          Thread.sleep(500);
      } catch (InterruptedException e) {
          throw new RuntimeException(e);
      }
      log.debug("t1 state:{}", t1.getState());
  }
}

image-1666709665177
这里可以验证第一点

@Slf4j(topic = "c.Test6")
public class Test6 {
  public static void main(String[] args) {
      var t1 = new Thread(() -> {
          log.debug("enter sleeping");
          try {
              Thread.sleep(2000);
          } catch (InterruptedException e) {
              log.debug("wake up!");
              throw new RuntimeException(e);
          }
      }, "t1");
      t1.start();
      try {
          Thread.sleep(1000);
      } catch (InterruptedException e) {
          throw new RuntimeException(e);
      }
      log.debug("interrupt t1");
      t1.interrupt();
  }

}

这里可以验证第二点
image-1666709743222
测试下使用TimeUnit的sleep方法

@Slf4j(topic = "c.Test7")
public class Test7 {
  public static void main(String[] args) {

      new Thread(() -> {
          log.debug("enter");
          try {
              TimeUnit.SECONDS.sleep(5);
          } catch (InterruptedException e) {
              throw new RuntimeException(e);
          }
          log.debug("end");
      }).start();
  }
}

yield

  1. 调用yield会让当前线程从Runnig进入Runnable就绪状态,然后调度执行其他线程
  2. 具体的实现依赖于操作系统的任务调度器

join方法

这个方法在kolin的协程中也有,和kotlin中的协程其一样的作用,
等待这个线程完成,然后再执行下文的代码

@Slf4j(topic = "c.Test10")
public class Test10 {
  private static int r = 0;

  public static void main(String[] args) {
      log.debug("开始");
      var t1 = new Thread(() -> {
          log.debug("开始");
          try {
              TimeUnit.SECONDS.sleep(1);
          } catch (InterruptedException e) {
              throw new RuntimeException(e);
          }

          log.debug("结束");
          r = 10;
      });
      t1.start();
      try {
          t1.join();
      } catch (InterruptedException e) {
          throw new RuntimeException(e);
      }
      log.debug("结果为:{}", r);
      log.debug("结束");
  }
}

image-1666709963526
如果这个join是个有时限的,那么并不会等时限结束,才进行下文代码,而是线程结束后就进行下文代码的执行

@Slf4j(topic = "c.Test12")
public class Test12 {
  private static int r1 = 0;
  private static int r2 = 0;

  public static void main(String[] args) {
      var t1 = new Thread(() -> {
          try {
              TimeUnit.SECONDS.sleep(2);
          } catch (InterruptedException e) {
              throw new RuntimeException(e);
          }
          r1 = 10;
      });


      var start = System.currentTimeMillis();
      t1.start();
      log.debug("join begin");
      try {
          t1.join(3000);
      } catch (InterruptedException e) {
          throw new RuntimeException(e);
      }
      var end = System.currentTimeMillis();
      log.debug("结果为:{},{},cost:{}", r1, r2, end - start);
  }
}

如果把这里的t1.join()中的参数修改为1,那么将无法得到t1中r1的变化,就进行了下文代码的执行

interrupt()方法

打断sleep,wait,join的线程,这里测试下sleep的方法

@Slf4j(topic = "c.Test13")
public class Test13 {
  public static void main(String[] args) {
      var t1 = new Thread(() -> {
          log.debug("sleep...");
          try {
              TimeUnit.SECONDS.sleep(5);
          } catch (InterruptedException e) {
              throw new RuntimeException(e);
          }

      }, "t1");
      t1.start();
      log.debug("interrupt t1");
      t1.interrupt();
      try {
          TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
          throw new RuntimeException(e);
      }
      log.debug("打断标记为:{}", t1.isInterrupted());
  }
}

image-1666711322431
这里interrupt标记被重置了
如果我们试图打断一个正常没有sleep等方法的线程则

@Slf4j(topic = "c.Test14")
public class Test14 {
  public static void main(String[] args) {
      var t1 = new Thread(() -> {
          while (true) {
              if (Thread.currentThread().isInterrupted()) {
                  log.debug("interrupted");
                  break;
              }
          }
      }, "t1");
      t1.start();
      try {
          TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
          throw new RuntimeException(e);
      }
      t1.interrupt();
      log.debug("打断标记为:{}", t1.isInterrupted());
  }
}

image-1666711409932
这里interrupt并没有被重置

还有一个方法的打断表现是很奇怪的
LockSupport的park()方法

@Slf4j(topic = "c.Test15")
public class Test15 {
  public static void main(String[] args) {
      var t1 = new Thread(() -> {
          log.debug("park");
          LockSupport.park();
          log.debug("unpark");
          log.debug("打断标记为:{}", Thread.currentThread().isInterrupted());
          //重置打断标记
//           Thread.currentThread().interrupt();
          LockSupport.park();
          log.debug("unpark");
      });
      t1.start();
      try {
          TimeUnit.SECONDS.sleep(1);
          t1.interrupt();
      } catch (InterruptedException e) {
          throw new RuntimeException(e);
      }
  }
}

会执行到打印park后,就出现阻塞的情况
在这里我们调用interrupt()方法后,线程会中断park()方法,且不会出现之前的异常,而且也不会重置interrupt状态,同时如果继续调用park()方法并不会起到中断状态,而是会继续执行,park()方法失效了
如果我们想继续调用下一个park方法的话,需要将线程的interrupt状态自行重置

守护线程与主线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守
护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

@Slf4j(topic = "c.Test16")
public class Test16 {
  public static void main(String[] args) {
      var t1 = new Thread(() -> {
          while (true) {
              if (Thread.currentThread().isInterrupted()) {
                  log.debug("interrupted");
                  break;
              }
          }
          log.debug("结束");
      }, "t1");

      t1.setDaemon(true);
      t1.start();
      try {
          Thread.sleep(1000);
      } catch (InterruptedException e) {
          throw new RuntimeException(e);
      }
      log.debug("结束");
  }
}

image-1666713067102
这里并没有打印守护线程中的结束语句

线程的状态

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

而在java中,线程的状态被分为NEW,RUNNABLE,BLOCKED,WAITING,TIME_WAITING,TERMINATED
只不过Java的线程无法区分操作系统的几种状态,所以可以认为,RUNNABLE包含操作系统中

Q.E.D.