线程创建与运行

Java 中有三种线程创建方式:

  • 实现 Runnable 接口的 run 方法
  • 继承 Thread 类并重写 run 的方法
  • 使用 FutureTask 方式。

首先看继承 Thread 类方式的实现。

  1. public class ThreadTest {
  2. //继承 Thread 类并重写 run 方法
  3. public static class MyThread extends Thread {
  4. @Override
  5. public void run() {
  6. System.out.println(「I am a child thread」);
  7. }
  8. }
  9. public static void mainString[] args {
  10. // 创建线程
  11. MyThread thread = new MyThread();
  12. // 启动线程
  13. thread.start();
  14. }
  15. }

使用继承方式的好处是,在 run()方法内获取当前线程直接使用 this 就可以了,无须使用 Thread.currentThread()方法;而如果使用 Runnable 方式,则只能使用主线程里面被声明为 final 的变量。

不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而 Runable 则没有这个限制。

下面看实现 Runnable 接口的 run 方法方式。

  1. public static class RunableTask implements Runnable{
  2. @Override
  3. public void run() {
  4. System.out.println("I am a child thread");
  5. }
  6. }
  7. public static void main(String[] args) throws InterruptedException{
  8. RunableTask task = new RunableTask();
  9. new Thread(task).start();
  10. new Thread(task).start();
  11. }

但是上面介绍的两种方式都有一个缺点,就是任务没有返回值。下面看最后一种,即使用 FutureTask 的方式。

  1. //创建任务类,类似 Runable
  2. public static class CallerTask implements Callable<String>{
  3. @Override
  4. public String call() throws Exception {
  5. return hello」;
  6. }
  7. }
  8. public static void mainString[] args throws InterruptedException {
  9. // 创建异步任务
  10. FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
  11. //启动线程
  12. new ThreadfutureTask).start();
  13. try {
  14. //等待任务执行完毕,并返回结果
  15. String result = futureTask.get();
  16. System.out.printlnresult);
  17. } catch ExecutionException e {
  18. e.printStackTrace();
  19. }
  20. }

线程通知与等待

1.wait()函数

当一个线程调用一个共享变量的 wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:(1)其他线程调用了该共享对象的 notify()或者 notifyAll()方法;(2)其他线程调用了该线程的 interrupt()方法,该线程抛出 InterruptedException 异常返回。

另外需要注意的是,如果调用 wait()方法的线程没有事先获取该对象的监视器锁,则调用 wait()方法时调用线程会抛出 IllegalMonitorStateException 异常。

那么一个线程如何才能获取一个共享变量的监视器锁呢?

(1)执行 synchronized 同步代码块时,使用该共享变量作为参数。

  1. synchronized(共享变量){
  2. //doSomething
  3. }

(2)调用该共享变量的方法,并且该方法使用了 synchronized 修饰。

  1. synchronized void add(int a, int b){
  2. //doSomething
  3. }

另外需要注意的是,一个线程可以从挂起状态变为可以运行状态(也就是被唤醒),即使该线程没有被其他线程调用 notify()、notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。

虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用 wait()方法进行防范。退出循环的条件是满足了唤醒该线程的条件。

  1. synchronized (obj) {
  2. while (条件不满足){
  3. obj.wait();
  4. }
  5. }

另外需要注意的是,当前线程调用共享变量的 wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。下面来看一个例子。

  1. // 创建资源
  2. private static volatile Object resourceA = new Object();
  3. private static volatile Object resourceB = new Object();
  4. public static void mainString[] args throws InterruptedException {
  5. // 创建线程
  6. Thread threadA = new Thread(new Runnable() {
  7. public void run() {
  8. try {
  9. // 获取 resourceA 共享资源的监视器锁
  10. synchronized resourceA {
  11. System.out.println(「threadA get resourceA lock」);
  12. // 获取 resourceB 共享资源的监视器锁
  13. synchronized resourceB {
  14. System.out.println(「threadA get resourceB lock」);
  15. // 线程 A 阻塞,并释放获取到的 resourceA 的锁
  16. System.out.println(「threadA release resourceA lock」);
  17. resourceA.wait();
  18. }
  19. }
  20. } catch InterruptedException e {
  21. e.printStackTrace();
  22. }
  23. }
  24. });
  25. // 创建线程
  26. Thread threadB = new Thread(new Runnable() {
  27. public void run() {
  28. try {
  29. //休眠 1s
  30. Thread.sleep(1000);
  31. // 获取 resourceA 共享资源的监视器锁
  32. synchronized resourceA {
  33. System.out.println(「threadB get resourceA lock」);
  34. System.out.println(「threadB try get resourceB lock...」);
  35. // 获取 resourceB 共享资源的监视器锁
  36. synchronized resourceB {
  37. System.out.println(「threadB get resourceB lock」);
  38. // 线程 B 阻塞,并释放获取到的 resourceA 的锁
  39. System.out.println(「threadB release resourceA lock」);
  40. resourceA.wait();
  41. }
  42. }
  43. } catch InterruptedException e {
  44. e.printStackTrace();
  45. }
  46. }
  47. });
  48. // 启动线程
  49. threadA.start();
  50. threadB.start();
  51. // 等待两个线程结束
  52. threadA.join();
  53. threadB.join();
  54. System.out.println(「main over」);
  55. }

输出结果如下:

线程基础操作 - 图1

如上代码中,在 main 函数里面启动了线程 A 和线程 B,为了让线程 A 先获取到锁,这里让线程 B 先休眠了 1s,线程 A 先后获取到共享变量 resourceA 和共享变量 resourceB 上的锁,然后调用了 resourceA 的 wait()方法阻塞自己,阻塞自己后线程 A 释放掉获取的 resourceA 上的锁。

线程 B 休眠结束后会首先尝试获取 resourceA 上的锁,如果当时线程 A 还没有调用 wait()方法释放该锁,那么线程 B 会被阻塞,当线程 A 释放了 resourceA 上的锁后,线程 B 就会获取到 resourceA 上的锁,然后尝试获取 resourceB 上的锁。由于线程 A 调用的是 resourceA 上的 wait()方法,所以线程 A 挂起自己后并没有释放获取到的 resourceB 上的锁,所以线程 B 尝试获取 resourceB 上的锁时会被阻塞。

这就证明了当线程调用共享对象的 wait()方法时,当前线程只会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。

当一个线程调用共享对象的 wait()方法被阻塞挂起后,如果其他线程中断了该线程,则该线程会抛出 InterruptedException 异常并返回。

  1. public class WaitNotifyInterupt {
  2. static Object obj = new Object();
  3. public static void mainString[] args throws InterruptedException {
  4. //创建线程
  5. Thread threadA = new Thread(new Runnable() {
  6. public void run() {
  7. try {
  8. System.out.println(「---begin---」);
  9. //阻塞当前线程
  10. synchronized (obj) {
  11. obj.wait();
  12. }
  13. System.out.println("---end---");
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. });
  19. threadA.start();
  20. Thread.sleep(1000);
  21. System.out.println("---begin interrupt threadA---");
  22. threadA.interrupt();
  23. System.out.println("---end interrupt threadA---");
  24. }
  25. }

输出如下。

线程基础操作 - 图2

在如上代码中,threadA 调用共享对象 obj 的 wait()方法后阻塞挂起了自己,然后主线程在休眠 1s 后中断了 threadA 线程,中断后 threadA 在 obj.wait()处抛出 java.lang. InterruptedException 异常而返回并终止。

2.wait(long timeout)函数

该方法相比 wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的 timeout ms 时间内被其他线程调用该共享变量的 notify()或者 notifyAll()方法唤醒,那么该函数还是会因为超时而返回。如果将 timeout 设置为 0 则和 wait 方法效果一样,因为在 wait 方法内部就是调用了 wait(0)。需要注意的是,如果在调用该函数时,传递了一个负的 timeout 则会抛出 IllegalArgumentException 异常。

3.wait(long timeout, int nanos)函数

在其内部调用的是 wait(long timeout)函数,如下代码只有在 nanos>0 时才使参数 timeout 递增 1。

  1. public final void wait(long timeout, int nanos) throws InterruptedException {
  2. if (timeout < 0) {
  3. throw new IllegalArgumentException("timeout value is negative");
  4. }
  5. if (nanos < 0 || nanos > 999999) {
  6. throw new IllegalArgumentException(
  7. "nanosecond timeout value out of range");
  8. }
  9. if (nanos > 0) {
  10. timeout++;
  11. }
  12. wait(timeout);
  13. }

4.notify() 函数

一个线程调用共享对象的 notify()方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的

此外,被唤醒的线程不能马上从 wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。

类似 wait 系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的 notify()方法,否则会抛出 IllegalMonitorStateException 异常。

5.notifyAll() 函数

不同于在共享变量上调用 notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。

下面举一个例子来说明 notify()和 notifyAll()方法的具体含义及一些需要注意的地方,代码如下。

  1. // 创建资源
  2. private static volatile Object resourceA = new Object();
  3. public static void mainString[] args throws InterruptedException {
  4. // 创建线程
  5. Thread threadA = new Thread(new Runnable() {
  6. public void run() {
  7. // 获取 resourceA 共享资源的监视器锁
  8. synchronized resourceA {
  9. System.out.println(「threadA get resourceA lock」);
  10. try {
  11. System.out.println(「threadA begin wait」);
  12. resourceA.wait();
  13. System.out.println(「threadA end wait」);
  14. } catch InterruptedException e {
  15. // TODO Auto-generated catch block
  16. e.printStackTrace();
  17. }
  18. }
  19. }
  20. });
  21. // 创建线程
  22. Thread threadB = new Thread(new Runnable() {
  23. public void run() {
  24. synchronized resourceA {
  25. System.out.println(「threadB get resourceA lock」);
  26. try {
  27. System.out.println(「threadB begin wait」);
  28. resourceA.wait();
  29. System.out.println(「threadB end wait」);
  30. } catch InterruptedException e {
  31. // TODO Auto-generated catch block
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  36. });
  37. // 创建线程
  38. Thread threadC = new Thread(new Runnable() {
  39. public void run() {
  40. synchronized resourceA {
  41. System.out.println(「threadC begin notify」);
  42. resourceA.notify();
  43. }
  44. }
  45. });
  46. // 启动线程
  47. threadA.start();
  48. threadB.start();
  49. Thread.sleep(1000);
  50. threadC.start();
  51. // 等待线程结束
  52. threadA.join();
  53. threadB.join();
  54. threadC.join();
  55. System.out.println(「main over」);
  56. }

输出结果如下。

线程基础操作 - 图3

从输出结果可知线程调度器这次先调度了线程 A 占用 CPU 来运行,线程 A 首先获取 resourceA 上面的锁,然后调用 resourceA 的 wait()方法挂起当前线程并释放获取到的锁,然后线程 B 获取到 resourceA 上的锁并调用 resourceA 的 wait()方法,此时线程 B 也被阻塞挂起并释放了 resourceA 上的锁,到这里线程 A 和线程 B 都被放到了 resourceA 的阻塞集合里面。线程 C 休眠结束后在共享资源 resourceA 上调用了 notify()方法,这会激活 resourceA 的阻塞集合里面的一个线程,这里激活了线程 A,所以线程 A 调用的 wait()方法返回了,线程 A 执行完毕。而线程 B 还处于阻塞状态。如果把线程 C 调用的 notify()方法改为调用 notifyAll()方法,则执行结果如下。

线程基础操作 - 图4

从输入结果可知线程 A 和线程 B 被挂起后,线程 C 调用 notifyAll()方法会唤醒 resourceA 的等待集合里面的所有线程,这里线程 A 和线程 B 都会被唤醒,只是线程 B 先获取到 resourceA 上的锁,然后从 wait()方法返回。线程 B 执行完毕后,线程 A 又获取了 resourceA 上的锁,然后从 wait()方法返回。线程 A 执行完毕后,主线程返回,然后打印输出。

一个需要注意的地方是,在共享变量上调用 notifyAll()方法只会唤醒调用这个方法前调用了 wait 系列函数而被放入共享变量等待集合里面的线程。

等待线程执行终止的 join 方法

在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。Thread 类中有一个 join 方法就可以做这个事情,join 方法是 Thread 类直接提供的。join 是无参且返回值为 void 的方法。

  1. public static void main(String[] args) throws InterruptedException {
  2. Thread threadOne = new Thread(new Runnable() {
  3. @Override
  4. public void run() {
  5. try {
  6. Thread.sleep(1000);
  7. } catch InterruptedException e {
  8. e.printStackTrace();
  9. }
  10. System.out.println(「child threadOne over 」);
  11. }
  12. });
  13. Thread threadTwo = new Thread(new Runnable() {
  14. @Override
  15. public void run() {
  16. try {
  17. Thread.sleep(1000);
  18. } catch InterruptedException e {
  19. e.printStackTrace();
  20. }
  21. System.out.println(「child threadTwo over 」);
  22. }
  23. });
  24. //启动子线程
  25. threadOne.start();
  26. threadTwo.start();
  27. System.out.println(「wait all child thread over 」);
  28. //等待子线程执行完毕,返回
  29. threadOne.join();
  30. threadTwo.join();
  31. System.out.println("all child thread over! ");
  32. }

另外,线程 A 调用线程 B 的 join 方法后会被阻塞,当其他线程调用了线程 A 的 interrupt()方法中断了线程 A 时,线程 A 会抛出 InterruptedException 异常而返回。下面通过一个例子来加深理解。

  1. public static void mainString[] args throws InterruptedException {
  2. //线程 one
  3. Thread threadOne = new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. System.out.println(「threadOne begin run 」);
  7. for (; {
  8. }
  9. }
  10. });
  11. //获取主线程
  12. final Thread mainThread = Thread.currentThread();
  13. //线程 two
  14. Thread threadTwo = new Thread(new Runnable() {
  15. @Override
  16. public void run() {
  17. //休眠 1s
  18. try {
  19. Thread.sleep(1000);
  20. } catch InterruptedException e {
  21. e.printStackTrace();
  22. }
  23. //中断主线程
  24. mainThread.interrupt();
  25. }
  26. });
  27. // 启动子线程
  28. threadOne.start();
  29. //延迟 1s 启动线程
  30. threadTwo.start();
  31. try{//等待线程 one 执行结束
  32. threadOne.join();
  33. }catchInterruptedException e){
  34. System.out.println(「main thread:」 + e);
  35. }
  36. }

输出结果如下。

线程基础操作 - 图5

这里需要注意的是,在 threadTwo 里面调用的是主线程的 interrupt()方法,而不是线程 threadOne 的。