创建和运行线程

1、进程和线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360安全卫士等)。

线程

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
  • Java中,线程作为最小调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器。

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集。
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享。
  • 进程间通信较为复杂。
    • 同一台计算机的进程通信称为IPC(Inter-process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP。
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量。
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低。

2、并行与并发

  • 单核cpu下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows 下时间片最小约为15毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。总结为一句话就是:微观串行,宏观并行, 一般会将这种线程轮流使用CPU的做法称为并发(concurrent)。

    1639315354263
  • 多核cpu下,每个核(core)都可以调度运行线程,这时候线程可以是并行的。

    1639315494264
  • 引用编程大师Rob Pike的一段描述:

    • 并发(concurrent)是同一时间应对(dealing with)多件事情的能力。
    • 并行(parallel)是同一时间动手做(doing)多件事情的能力。

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

  • 因为以下一些原因导致cpu不再执行当前的线程,转而执行另一个线程的代码:
    • 线程的cpu时间片用完。
    • 垃圾回收。
    • 有更高优先级的线程需要运行。
    • 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法。
  • 当Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条jvm指令的执行地址,是线程私有的。
    • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等。
    • Context Switch频繁发生会影响性能。

3、创建和使用线程

3.1 线程创建

3.1.1 直接使用Thread

  • 模板

    1
    2
    3
    4
    5
    6
    7
    8
    // 创建线程对象
    Thread t = new Thread() {
    public void run() {
    // 要执行的任务
    }
    };
    // 启动线程
    t.start();
  • 例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Slf4j
    public class TestThread {
    public static void main(String[] args) {
    // 构造方法的参数是给线程指定名字
    Thread t = new Thread("t1"){
    @Override
    // run方法内实现了要执行的任务
    public void run() {
    log.info("Hello Thread");
    }
    };
    t.start();
    }
    }

3.1.2 使用Runnable配合Thread

  • 线程任务(要执行的代码)分开。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Runnable runnable = new Runnable() {
    public void run(){
    // 要执行的任务
    }
    };
    // 创建线程对象
    Thread t = new Thread(runnable);
    // 启动线程
    t.start();
  • 例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Slf4j
    public class TestThread {
    public static void main(String[] args) {
    // 创建任务对象
    Runnable task = new Runnable() {
    @Override
    public void run() {
    log.info("Hello Thread");
    }
    };
    // 参数1是任务对象; 参数2是线程名字
    Thread t = new Thread(task, "t");
    t.start();
    }
    }
    • Java8以后可以使用lambda精简代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Slf4j
      public class TestThread {
      public static void main(String[] args) {
      // 创建任务对象
      Runnable task = () -> {
      log.info("Hello Thread");
      };
      // 参数1是任务对象; 参数2是线程名字
      Thread t = new Thread(task, "t");
      t.start();
      }
      }
  • Thread与Runnable的关系:

    • 使用带有构造器参数的方式传入Runnable的实例时通过源码可以看到实际上最终这个对象会被赋值给Thread类里一个名为target,类型为Runnable的成员变量。并且在执行Thread.run()方法时会判断target是否为null,如果不为null才优先执行target.run()。

    • 用Runnable更容易与线程池等高级API配合,让任务类脱离了Thread继承体系,更灵活。

3.1.3 FutureTask配合Thread

  • FutureTask能够接收Callable类型的参数,其间接实现了Runnable和Future接口,用来处理有返回结果的情况。

    1639318231435
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Slf4j
    public class TestThread {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    // 创建任务对象
    FutureTask<Integer> task = new FutureTask<>(() -> {
    log.info("Hello Thread");
    return 100;
    });
    // 参数1是任务对象; 参数2是线程名字
    new Thread(task, "t").start();
    // 主线程阻塞,同步等待task执行完毕的结果
    Integer result = task.get();
    log.debug("结果是:{}", result);
    }
    }

3.2 线程使用

3.2.1 start()与run()

  • 调用run():程序仍在main线程运行,t1线程内的方法调用还是同步的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Slf4j
    public class TestThread {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    Thread t1 = new Thread("t1") {
    @Override
    public void run() {
    try {
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    log.debug("running...");
    }
    };

    t1.run();
    log.debug("do other things...");
    }
    }

    运行结果:

    1
    2
    16:09:34.019 org.example.TestThread [main] - running...
    16:09:34.019 org.example.TestThread [main] - do other things...
  • 调用start():程序在t1线程运行,t1线程内的方法调用是异步的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Slf4j
    public class TestThread {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    Thread t1 = new Thread("t1") {
    @Override
    public void run() {
    try {
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    log.debug("running...");
    }
    };

    t1.start();
    log.debug("do other things...");
    }
    }

    运行结果:

    1
    2
    16:12:04.089 org.example.TestThread [main] - do other things...
    16:12:07.089 org.example.TestThread [t1] - running...
  • 结论:

    • 直接调用run是在主线程中执行了run,没有启动新的线程。
    • 使用start是启动新的线程,通过新的线程间接执行run中的代码。

2.2 sleep()与yield()

  • 调用sleep():会让当前线程从Running进入Timed Waiting状态(阻塞)。

    • 睡眠结束后的线程未必会立刻得到执行。
    • 建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性。(例如TimeUnit.SECONDS.sleep(1);)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Slf4j
    public class TestThread {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    Thread t1 = new Thread("t1") {
    @Override
    public void run() {
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    };

    t1.start();
    log.debug("t1 state: {}", t1.getState());// t1 state: RUNNABLE

    try {
    Thread.sleep(500);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    log.debug("t1 state: {}", t1.getState());// t1 state: TIMED_WAITING
    }
    }
    • 其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      @Slf4j
      public class TestThread {
      public static void main(String[] args) throws ExecutionException, InterruptedException {
      Thread t1 = new Thread("t1") {
      @Override
      public void run() {
      log.debug("enter sleep...");
      try {
      Thread.sleep(2000);
      } catch (InterruptedException e) {
      log.debug("wake up...");
      e.printStackTrace();
      }
      }
      };
      t1.start();

      Thread.sleep(1000);
      log.debug("interrupt...");
      t1.interrupt();
      }
      }

      运行结果:

      1
      2
      3
      4
      5
      6
      16:22:22.726 org.example.TestThread [t1] - enter sleep...
      16:22:23.735 org.example.TestThread [main] - interrupt...
      16:22:23.735 org.example.TestThread [t1] - wake up...
      java.lang.InterruptedException: sleep interrupted
      at java.lang.Thread.sleep(Native Method)
      at org.example.TestThread$1.run(TestThread.java:23)
  • 调用yield()

    • 调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其它线程。(让出去后还可能重新被分到CPU的时间片继续执行)
    • 具体的实现依赖于操作系统的任务调度器。

2.3 join()

  • t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
public class TestThread {
static int r = 0;
public static void main(String[] args) throws ExecutionException, InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
},"t1");
t1.start();
t1.join();
log.debug("结果为:{}", r);//结果为:10
log.debug("结束");
}
}

因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10。通过使用t1线程的join()使得主线程等待其结束并成功打印出修改后的值。运行结果:

1
2
3
4
5
20:59:03.203 org.example.TestThread [main] - 开始
20:59:03.235 org.example.TestThread [t1] - 开始
20:59:04.241 org.example.TestThread [t1] - 结束
20:59:04.241 org.example.TestThread [main] - 结果为:10
20:59:04.241 org.example.TestThread [main] - 结束
  • 有时效的join()规定最多等待的时间,如果超出所等待的时间就会放弃等待继续运行。如下方t1线程执行时间超过2秒,而主线程最多等待1.5秒,所以主线程没有等待t1线程完全执行结束(只等待了1.5秒)就接着继续运行了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Slf4j
    public class TestThread {
    static int r1 = 0;
    static int r2 = 0;
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    Thread t1 = new Thread(() -> {
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    r1 = 10;
    });

    long start = System.currentTimeMillis();
    t1.start();

    log.debug("join begin");
    t1.join(1500);
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
    }
    }

    运行结果:

    1
    2
    21:15:49.100 org.example.TestThread [main] - join begin
    21:15:50.608 org.example.TestThread [main] - r1: 0 r2: 0 cost: 1508

2.4 interrupt()

  • 如果线程被Object.wait,Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。下面的isInterrupted()是线程的一个实例方法,作用是返回当前线程是否被打断,不会清除打断状态;与其区别的是Thread类的interrupted()方法是一个静态方法,作用是返回当前线程的被打断状态,同时清除打断状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Slf4j
    public class TestThread {
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
    log.debug("sleep...");
    try {
    Thread.sleep(5000); // wait, join
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    },"t1");

    t1.start();
    // 主线程等待以便确保t1线程先进入睡眠状态
    Thread.sleep(1000);
    log.debug("interrupt");
    t1.interrupt();
    // 主线程等待以便t1线程抛出InterruptedException异常
    Thread.sleep(500);
    log.debug("打断标记:{}", t1.isInterrupted());// 标记被清空
    }
    }

    运行结果:

    1
    2
    3
    4
    5
    6
    7
    21:32:39.648 org.example.TestThread [t1] - sleep...
    21:32:40.661 org.example.TestThread [main] - interrupt
    java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at org.example.TestThread.lambda$main$0(TestThread.java:21)
    at java.lang.Thread.run(Thread.java:748)
    21:32:41.174 org.example.TestThread [main] - 打断标记:false
  • 打断正常运行的线程打断正常运行的线程, 不会清空打断状态,此时被打断的线程可根据打断标记来决定自己是否要停止运行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Slf4j
    public class TestThread {
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
    while(true) {
    boolean interrupted = Thread.currentThread().isInterrupted();
    if(interrupted) {
    log.debug("被打断了, 退出循环");
    break;
    }
    }
    }, "t1");
    t1.start();

    Thread.sleep(1000);
    log.debug("interrupt");
    t1.interrupt();
    }
    }

    执行结果:

    1
    2
    21:51:21.970 org.example.TestThread [main] - interrupt
    21:51:21.972 org.example.TestThread [t1] - 被打断了, 退出循环
  • 打断park线程, 不会清空打断状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Slf4j
    public class TestThread {
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
    log.debug("park...");
    LockSupport.park();
    log.debug("unpark...");
    log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
    }, "t1");
    t1.start();

    Thread.sleep(1000);
    t1.interrupt();
    }
    }

    运行结果:

    1
    2
    3
    10:40:39.969 org.example.TestThread [t1] - park...
    10:40:40.969 org.example.TestThread [t1] - unpark...
    10:40:40.969 org.example.TestThread [t1] - 打断状态:true

    打断状态为true后如果继续执行LockSupport.park()则失效,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Slf4j
    public class TestThread {
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
    log.debug("park...");
    LockSupport.park();
    log.debug("unpark...");
    log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
    LockSupport.park();
    log.debug("unpark...");
    }, "t1");
    t1.start();

    Thread.sleep(1000);
    t1.interrupt();
    }
    }

    运行结果:

    1
    2
    3
    4
      10:41:31.009 org.example.TestThread [t1] - park...
    10:41:32.010 org.example.TestThread [t1] - unpark...
    10:41:32.010 org.example.TestThread [t1] - 打断状态:true
    10:41:32.010 org.example.TestThread [t1] - unpark...
  • 在一个线程t1中“优雅”终止线程t2(即给线程t2一个料理后事的机会)的方式。

    • 错误的方式:

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

    • 此案例的监控线程被打断有两种情况,第一种情况是监控线程在正常监控时被打断,但此时打断标记不会被清空,在通过下一次while循环时即可结束循环;第二种情况是监控线程在睡眠时被打断,这时由于打断标记会被清除,所以要手动设置打断标记为true以便在下次while循环时能结束循环。

    • 方式一:利用isInterrupted。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
        @Slf4j
      public class MonitorThreadTest {
      private Thread thread;

      public static void main(String[] args) throws InterruptedException {
      MonitorThreadTest monitorThreadTest = new MonitorThreadTest();
      monitorThreadTest.start();
      Thread.sleep(3500);
      // 在监控线程睡眠过程中打断
      monitorThreadTest.stop();
      }

      public void start(){
      thread = new Thread(() -> {
      while(true) {
      Thread current = Thread.currentThread();
      if(current.isInterrupted()) {
      log.debug("料理后事");
      break;
      }
      try {
      // 情况一
      Thread.sleep(1000);
      // 情况二
      log.debug("执行监控操作");
      } catch (InterruptedException e) {
      e.printStackTrace();
      // 重新设置打断标记
      current.interrupt();
      }
      }
      },"监控线程");
      thread.start();
      }

      public void stop() {
      thread.interrupt();
      }
      }

      运行结果:

      1
      2
      3
      4
      5
      6
      7
      8
        22:14:37.595 org.example.MonitorThreadTest [监控线程] - 执行监控操作
      22:14:38.600 org.example.MonitorThreadTest [监控线程] - 执行监控操作
      22:14:39.610 org.example.MonitorThreadTest [监控线程] - 执行监控操作
      java.lang.InterruptedException: sleep interrupted
      at java.lang.Thread.sleep(Native Method)
      at org.example.MonitorThreadTest.lambda$start$0(MonitorThreadTest.java:32)
      at java.lang.Thread.run(Thread.java:748)
      22:14:40.104 org.example.MonitorThreadTest [监控线程] - 料理后事
    • 方式二:利用打断标记。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
        @Slf4j
      public class MonitorThreadTest {
      private Thread thread;
      // 停止标记
      private volatile boolean stop = false;

      public static void main(String[] args) throws InterruptedException {
      MonitorThreadTest monitorThreadTest = new MonitorThreadTest();
      monitorThreadTest.start();
      Thread.sleep(3500);
      // 在监控线程睡眠过程中打断
      monitorThreadTest.stop();
      }

      public void start(){
      thread = new Thread(() -> {
      while (true) {
      // 是否被打断
      if (stop) {
      log.debug("料理后事");
      break;
      }
      try {
      Thread.sleep(1000);
      log.debug("执行监控记录");
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      }
      },"监控线程");
      thread.start();
      }

      public void stop() {
      stop = true;
      thread.interrupt();
      }
      }

      运行结果:

      1
      2
      3
      4
      5
      6
      7
      8
      22:27:10.648 org.example.MonitorThreadTest [监控线程] - 执行监控记录
      22:27:11.667 org.example.MonitorThreadTest [监控线程] - 执行监控记录
      22:27:12.667 org.example.MonitorThreadTest [监控线程] - 执行监控记录
      java.lang.InterruptedException: sleep interrupted
      at java.lang.Thread.sleep(Native Method)
      at org.example.MonitorThreadTest.lambda$start$0(MonitorThreadTest.java:33)
      at java.lang.Thread.run(Thread.java:748)
      22:27:13.139 org.example.MonitorThreadTest [监控线程] - 料理后事

2.5 wait()和notify()

  • 操作系统的Monitor对象结构如下图:

    • 已经成为Monitor的Owner如果发现条件不满足,可以调用wait方法,即可进入WaitSet变为WAITING状态。
    • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片。
    • BLOCKED线程会在Owner线程释放锁时唤醒。
    • WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList重新竞争。
  • obj.wait()让进入object监视器的线程到waitSet等待。

    • wait()方法会释放对象的锁,进入WaitSet等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify为止。
    • wait(long n)是有时限的等待, 到n毫秒后结束等待,或是被notify。
  • obj.notify()在object上正在waitSet等待的线程中挑一个唤醒。

  • obj.notifyAll()让object上正在waitSet等待的线程全部唤醒。

  • 它们都是线程之间进行协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    @Slf4j
    public class TestWaitNotify {
    final static Object obj = new Object();

    public static void main(String[] args) {

    new Thread(() -> {
    synchronized (obj) {
    log.debug("执行....");
    try {
    obj.wait(); // 让线程在obj上一直等待下去
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    log.debug("其它代码....");
    }
    },"t1").start();

    new Thread(() -> {
    synchronized (obj) {
    log.debug("执行....");
    try {
    obj.wait(); // 让线程在obj上一直等待下去
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    log.debug("其它代码....");
    }
    },"t2").start();

    // 主线程两秒后执行
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    log.debug("唤醒obj上其它线程");
    synchronized (obj) {
    // obj.notify(); // 唤醒obj上一个线程
    obj.notifyAll(); // 唤醒obj上所有等待线程
    }
    }
    }
    • 当调用obj.notify()时随机唤醒某一个在waitSet中等待的线程,执行结果:

    • obj.notifyAll()时唤醒所有在waitSet中等待的线程,执行结果:

  • sleep(long n)和wait(long n)的区别。

    • sleep是Thread类的静态方法,而wait是Object的实例方法。
    • sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用。
    • sleep在睡眠的同时,不会释放对象锁的,但wait在等待的时候会释放对象锁。
    • 它们状态都是TIMED_WAITING。

2.6 park()和unpark()

  • 它们是LockSupport类中的方法。

    1
    2
    3
    4
    // 暂停当前线程
    LockSupport.park();
    // 恢复某个线程的运行
    LockSupport.unpark(暂停线程对象)
  • 先park再unpark:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Slf4j
    public class TestParkAndUnPark {
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
    log.debug("start...");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    log.debug("park...");
    LockSupport.park();
    log.debug("resume...");
    },"t1");
    t1.start();
    Thread.sleep(2000);
    log.debug("unpark...");
    LockSupport.unpark(t1);
    }
    }

    运行结果:

  • 先unpark再park则可以恢复未来暂停进程的运行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class TestParkAndUnPark {
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
    log.debug("start...");
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    log.debug("park...");
    LockSupport.park();
    log.debug("resume...");
    }, "t1");
    t1.start();
    Thread.sleep(1000);
    log.debug("unpark...");
    LockSupport.unpark(t1);
    }
    }

    运行结果:

  • 与Object的wait&notify相比:

    • wait,notify和notifyAll必须配合Object Monitor一起使用,而park,unpark不必。
    • park,unpark是以线程为单位来阻塞和唤醒线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么精确。
    • park&unpark可以先unpark,而wait&notify不能先notify。
  • 原理。

    • 每个线程都有自己的一个Parker对象,由三部分组成_counter ,_cond和_mutex,其中_mutex中包含等待队列_cond。

    • 打个比喻:线程就像一个旅人,Parker就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0为耗尽,1为充足)。

      • 调用park就是要看需不需要停下来歇息。
  • 如果备用干粮耗尽,那么钻进帐篷歇息。
    + 如果备用干粮充足,那么不需停留,继续前进。

    • 调用unpark,就好比令干粮充足。

      • 如果这时线程还在帐篷,就唤醒让他继续前进。
      • 如果这时线程还在运行,那么下次他调用park时,仅是消耗掉备用干粮,不需停留继续前进。
      • 因为背包空间有限,多次调用unpark仅会补充一份备用干粮。
      • ①当前线程调用Unsafe.park()方法。
    • ②检查_counter,本情况为0,这时,获得_mutex互斥锁。

      • ③线程进入_cond条件变量阻塞。
  • ④设置_counter=0。

    • ①调用Unsafe.unpark(Thread_0)方法,设置_counter为1。
    • ②唤醒_cond条件变量中的Thread_0。
    • ③Thread_0恢复运行。
    • ④设置_counter为0。
    • ①调用Unsafe.unpark(Thread_0)方法,设置_counter为1。
    • ②当前线程调用Unsafe.park()方法。
    • ③检查_counter,本情况为1,这时线程无需阻塞,继续运行。
    • ④设置_counter为0。