代码理解java多线程 (一) - 基础篇(2)

目录

 

第四章 Java线程的状态及主要转化方法

4.1 操作系统中的线程状态转换

4.2 Java线程的6个状态

4.2.1 NEW

4.2.2 RUNNABLE

4.2.3 BLOCKED

4.2.4 WAITING

4.2.5 TIMED_WAITING

4.2.6 TERMINATED

4.3 线程状态的转换

4.3.1 BLOCKED与RUNNABLE状态的转换

4.3.2 WAITING状态与RUNNABLE状态的转换

4.3.3 TIMED_WAITING与RUNNABLE状态转换

4.3.4 线程中断

第五章 Java线程间的通信

5.1 锁与同步

5.2 等待/通知机制

5.3 信号量

5.4 管道

5.5 其它通信相关

5.5.1 join方法

5.5.2 sleep方法

5.5.3 ThreadLocal类

5.5.4 InheritableThreadLocal


第四章 Java线程的状态及主要转化方法

4.1 操作系统中的线程状态转换

首先我们来看看操作系统中的线程状态转换。

在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态是一致的

ç³»ç»è¿ç¨/线ç¨è½¬æ¢å¾ 

 

操作系统线程主要有以下三个状态:

  • 就绪状态(ready):线程正在等待使用CPU,经调度程序调用之后可进入running状态。
  • 执行状态(running):线程正在使用CPU。
  • 等待状态(waiting): 线程经过等待事件的调用或者正在等待其他资源(如I/O)。

 

4.2 Java线程的6个状态

// Thread.State 源码
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

4.2.1 NEW

处于NEW状态的线程此时尚未启动。这里的尚未启动指的是还没调用Thread实例的start()方法。

 
  1. private void testStateNew() {
  2. Thread thread = new Thread(() -> {});
  3. System.out.println(thread.getState()); // 输出 NEW
  4. }

从上面可以看出,只是创建了线程而并没有调用start()方法,此时线程处于NEW状态。

关于start()的两个引申问题

  1. 反复调用同一个线程的start()方法是否可行?
  2. 假如一个线程执行完毕(此时处于TERMINATED状态),再次调用这个线程的start()方法是否可行?

要分析这两个问题,我们先来看看start()的源码:

 
  1. public synchronized void start() {
  2. if (threadStatus != 0)
  3. throw new IllegalThreadStateException();
  4.  
  5. group.add(this);
  6.  
  7. boolean started = false;
  8. try {
  9. start0();
  10. started = true;
  11. } finally {
  12. try {
  13. if (!started) {
  14. group.threadStartFailed(this);
  15. }
  16. } catch (Throwable ignore) {
  17.  
  18. }
  19. }
  20. }

我们可以看到,在start()内部,这里有一个threadStatus的变量。如果它不等于0,调用start()是会直接抛出异常的。

我们接着往下看,有一个native的start0()方法。这个方法里并没有对threadStatus的处理。到了这里我们仿佛就拿这个threadStatus没辙了,我们通过debug的方式再看一下:

 
  1. @Test
  2. public void testStartMethod() {
  3. Thread thread = new Thread(() -> {});
  4. thread.start(); // 第一次调用
  5. thread.start(); // 第二次调用
  6. }

我是在start()方法内部的最开始打的断点,叙述下在我这里打断点看到的结果:

  • 第一次调用时threadStatus的值是0。
  • 第二次调用时threadStatus的值不为0。

查看当前线程状态的源码:

 
  1. // Thread.getState方法源码:
  2. public State getState() {
  3. // get current thread state
  4. return sun.misc.VM.toThreadState(threadStatus);
  5. }
  6.  
  7. // sun.misc.VM 源码:
  8. public static State toThreadState(int var0) {
  9. if ((var0 & 4) != 0) {
  10. return State.RUNNABLE;
  11. } else if ((var0 & 1024) != 0) {
  12. return State.BLOCKED;
  13. } else if ((var0 & 16) != 0) {
  14. return State.WAITING;
  15. } else if ((var0 & 32) != 0) {
  16. return State.TIMED_WAITING;
  17. } else if ((var0 & 2) != 0) {
  18. return State.TERMINATED;
  19. } else {
  20. return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
  21. }
  22. }

所以,我们结合上面的源码可以得到引申的两个问题的结果:

两个问题的答案都是不可行,在调用一次start()之后,threadStatus的值会改变(threadStatus !=0),此时再次调用start()方法会抛出IllegalThreadStateException异常。

比如,threadStatus为2代表当前线程状态为TERMINATED。

4.2.2 RUNNABLE

表示当前线程正在运行中。处于RUNNABLE状态的线程在Java虚拟机中运行,也有可能在等待其他系统资源(比如I/O)。

Java中线程的RUNNABLE状态

看了操作系统线程的几个状态之后我们来看看Thread源码里对RUNNABLE状态的定义:

 
  1. /**
  2. * Thread state for a runnable thread. A thread in the runnable
  3. * state is executing in the Java virtual machine but it may
  4. * be waiting for other resources from the operating system
  5. * such as processor.
  6. */

Java线程的RUNNABLE状态其实是包括了传统操作系统线程的readyrunning两个状态的。

4.2.3 BLOCKED

阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进入同步区。

我们用BLOCKED状态举个生活中的例子:

假如今天你下班后准备去食堂吃饭。你来到食堂仅有的一个窗口,发现前面已经有个人在窗口前了,此时你必须得等前面的人从窗口离开才行。
假设你是线程t2,你前面的那个人是线程t1。此时t1占有了锁(食堂唯一的窗口),t2正在等待锁的释放,所以此时t2就处于BLOCKED状态。

4.2.4 WAITING

等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。

调用如下3个方法会使线程进入等待状态:

  • Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
  • Thread.join():等待线程执行完毕,底层调用的是Object实例的wait方法;
  • LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。

我们延续上面的例子继续解释一下WAITING状态:

你等了好几分钟现在终于轮到你了,突然你们有一个“不懂事”的经理突然来了。你看到他你就有一种不祥的预感,果然,他是来找你的。

他把你拉到一旁叫你待会儿再吃饭,说他下午要去作报告,赶紧来找你了解一下项目的情况。你心里虽然有一万个不愿意但是你还是从食堂窗口走开了。

此时,假设你还是线程t2,你的经理是线程t1。虽然你此时都占有锁(窗口)了,“不速之客”来了你还是得释放掉锁。此时你t2的状态就是WAITING。然后经理t1获得锁,进入RUNNABLE状态。

要是经理t1不主动唤醒你t2(notify、notifyAll..),可以说你t2只能一直等待了。

4.2.5 TIMED_WAITING

超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。

调用如下方法会使线程进入超时等待状态:

  • Thread.sleep(long millis):使当前线程睡眠指定时间;
  • Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
  • Thread.join(long millis):等待当前线程最多执行millis毫秒,如果millis为0,则会一直执行;
  • LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
  • LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;

我们继续延续上面的例子来解释一下TIMED_WAITING状态:

到了第二天中午,又到了饭点,你还是到了窗口前。

突然间想起你的同事叫你等他一起,他说让你等他十分钟他改个bug。

好吧,你说那你就等等吧,你就离开了窗口。很快十分钟过去了,你见他还没来,你想都等了这么久了还不来,那你还是先去吃饭好了。

这时你还是线程t1,你改bug的同事是线程t2。t2让t1等待了指定时间,t1先主动释放了锁。此时t1等待期间就属于TIMED_WATING状态。

t1等待10分钟后,就自动唤醒,拥有了去争夺锁的资格。

4.2.6 TERMINATED

终止状态。此时线程已执行完毕。

4.3 线程状态的转换

根据上面关于线程状态的介绍我们可以得到下面的线程状态转换图
线程状态转换图

4.3.1 BLOCKED与RUNNABLE状态的转换

我们在上面说到:处于BLOCKED状态的线程是因为在等待锁的释放。假如这里有两个线程a和b,a线程提前获得了锁并且暂未释放锁,此时b就处于BLOCKED状态。我们先来看一个例子:

 
  1. @Test
  2. public void blockedTest() {
  3.  
  4. Thread a = new Thread(new Runnable() {
  5. @Override
  6. public void run() {
  7. testMethod();
  8. }
  9. }, "a");
  10. Thread b = new Thread(new Runnable() {
  11. @Override
  12. public void run() {
  13. testMethod();
  14. }
  15. }, "b");
  16.  
  17. a.start();
  18. b.start();
  19. System.out.println(a.getName() + ":" + a.getState()); // 输出?
  20. System.out.println(b.getName() + ":" + b.getState()); // 输出?
  21. }
  22.  
  23. // 同步方法争夺锁
  24. private synchronized void testMethod() {
  25. try {
  26. Thread.sleep(2000L);
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. }

初看之下,大家可能会觉得线程a会先调用同步方法,同步方法内又调用了Thread.sleep()方法,必然会输出TIMED_WAITING,而线程b因为等待线程a释放锁所以必然会输出BLOCKED。

其实不然,有两点需要值得大家注意,一是在测试方法blockedTest()内还有一个main线程,二是启动线程后执行run方法还是需要消耗一定时间的。不打断点的情况下,上面代码中都应该输出RUNNABLE

测试方法的main线程只保证了a,b两个线程调用start()方法(转化为RUNNABLE状态),还没等两个线程真正开始争夺锁,就已经打印此时两个线程的状态(RUNNABLE)了。

这时你可能又会问了,要是我想要打印出BLOCKED状态我该怎么处理呢?其实就处理下测试方法里的main线程就可以了,你让它“休息一会儿”,打断点或者调用Thread.sleep方法就行。

这里需要注意的是main线程休息的时间,要保证在线程争夺锁的时间内,不要等到前一个线程锁都释放了你再去争夺锁,此时还是得不到BLOCKED状态的。

我们把上面的测试方法blockedTest()改动一下:

 
  1. public void blockedTest() throws InterruptedException {
  2. ······
  3. a.start();
  4. Thread.sleep(1000L); // 需要注意这里main线程休眠了1000毫秒,而testMethod()里休眠了2000毫秒
  5. b.start();
  6. System.out.println(a.getName() + ":" + a.getState()); // 输出?
  7. System.out.println(b.getName() + ":" + b.getState()); // 输出?
  8. }

在这个例子中,由于main线程休眠,所以线程a的run()方法跟着执行,线程b再接着执行。

在线程a执行run()调用testMethod()之后,线程a休眠了2000ms(注意这里是没有释放锁的),main线程休眠完毕,接着b线程执行的时候是争夺不到锁的,所以这里输出:

 
  1. a:TIMED_WAITING
  2. b:BLOCKED

4.3.2 WAITING状态与RUNNABLE状态的转换

根据转换图我们知道有3个方法可以使线程从RUNNABLE状态转为WAITING状态。我们主要介绍下Object.wait()Thread.join()
Object.wait()

调用wait()方法前线程必须持有对象的锁。

线程调用wait()方法时,会释放当前的锁,直到有其他线程调用notify()/notifyAll()方法唤醒等待锁的线程。

需要注意的是,其他线程调用notify()方法只会唤醒单个等待锁的线程,如有有多个线程都在等待这个锁的话不一定会唤醒到之前调用wait()方法的线程。

同样,调用notifyAll()方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。

Thread.join()

调用join()方法不会释放锁,会一直等待当前线程执行完毕(转换为TERMINATED状态)。

我们再把上面的例子线程启动那里改变一下:

 
  1. public void blockedTest() {
  2. ······
  3. a.start();
  4. a.join();
  5. b.start();
  6. System.out.println(a.getName() + ":" + a.getState()); // 输出 TERMINATED
  7. System.out.println(b.getName() + ":" + b.getState());
  8. }

要是没有调用join方法,main线程不管a线程是否执行完毕都会继续往下走。

a线程启动之后马上调用了join方法,这里main线程就会等到a线程执行完毕,所以这里a线程打印的状态固定是TERMIATED

至于b线程的状态,有可能打印RUNNABLE(尚未进入同步方法),也有可能打印TIMED_WAITING(进入了同步方法)。

4.3.3 TIMED_WAITING与RUNNABLE状态转换

TIMED_WAITING与WAITING状态类似,只是TIMED_WAITING状态等待的时间是指定的。

Thread.sleep(long)

使当前线程睡眠指定时间。需要注意这里的“睡眠”只是暂时使线程停止执行,并不会释放锁。时间到后,线程会重新进入RUNNABLE状态。

Object.wait(long)

wait(long)方法使线程进入TIMED_WAITING状态。这里的wait(long)方法与无参方法wait()相同的地方是,都可以通过其他线程调用notify()或notifyAll()方法来唤醒。

不同的地方是,有参方法wait(long)就算其他线程不来唤醒它,经过指定时间long之后它会自动唤醒,拥有去争夺锁的资格。

Thread.join(long)

join(long)使当前线程执行指定时间,并且使线程进入TIMED_WAITING状态。

我们再来改一改刚才的示例:

 
  1. public void blockedTest() {
  2. ······
  3. a.start();
  4. a.join(1000L);
  5. b.start();
  6. System.out.println(a.getName() + ":" + a.getState()); // 输出 TIEMD_WAITING
  7. System.out.println(b.getName() + ":" + b.getState());
  8. }

这里调用a.join(1000L),因为是指定了具体a线程执行的时间的,并且执行时间是小于a线程sleep的时间,所以a线程状态输出TIMED_WAITING。

b线程状态仍然不固定(RUNNABLE或BLOCKED)。

4.3.4 线程中断

在某些情况下,我们在线程启动后发现并不需要它继续执行下去时,需要中断线程。目前在Java里还没有安全直接的方法来停止线程,但是Java提供了线程中断机制来处理需要中断线程的情况。

线程中断机制是一种协作机制。需要注意,通过中断操作并不能直接终止一个线程,而是通知需要被中断的线程自行处理。

简单介绍下Thread类里提供的关于线程中断的几个方法:

  • Thread.interrupt():中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为true(默认是flase);
  • Thread.interrupted():测试当前线程是否被中断。线程的中断状态受这个方法的影响,意思是调用一次使线程中断状态设置为true,连续调用两次会使得这个线程的中断状态重新转为false;
  • Thread.isInterrupted():测试当前线程是否被中断。与上面方法不同的是调用这个方法并不会影响线程的中断状态。

在线程中断机制里,当其他线程通知需要被中断的线程后,线程中断的状态被设置为true,但是具体被要求中断的线程要怎么处理,完全由被中断线程自己而定,可以在合适的实际处理中断请求,也可以完全不处理继续执行下去。

 

 

第五章 Java线程间的通信

合理的使用Java多线程可以更好地利用服务器资源。一般来讲,线程内部有自己私有的线程上下文,互不干扰。但是当我们需要多个线程之间相互协作的时候,就需要我们掌握Java线程的通信方式。本文将介绍Java线程之间的几种通信原理。

5.1 锁与同步

在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁。线程和锁的关系,我们可以用婚姻关系来理解。一个锁同一时间只能被一个线程持有。也就是说,一个锁如果和一个线程“结婚”(持有),那其他线程如果需要得到这个锁,就得等这个线程和这个锁“离婚”(释放)。

在我们的线程之间,有一个同步的概念。什么是同步呢,假如我们现在有2位正在抄暑假作业答案的同学:线程A和线程B。当他们正在抄的时候,老师突然来修改了一些答案,可能A和B最后写出的暑假作业就不一样。我们为了A,B能写出2本相同的暑假作业,我们就需要让老师先修改答案,然后A,B同学再抄。或者A,B同学先抄完,老师再修改答案。这就是线程A,线程B的线程同步。

可以以解释为:线程同步是线程之间按照一定的顺序执行。

为了达到线程同步,我们可以使用锁来实现它。

我们先来看看一个无锁的程序:

 
  1. public class NoneLock {
  2.  
  3. static class ThreadA implements Runnable {
  4. @Override
  5. public void run() {
  6. for (int i = 0; i < 100; i++) {
  7. System.out.println("Thread A " + i);
  8. }
  9. }
  10. }
  11.  
  12. static class ThreadB implements Runnable {
  13. @Override
  14. public void run() {
  15. for (int i = 0; i < 100; i++) {
  16. System.out.println("Thread B " + i);
  17. }
  18. }
  19. }
  20.  
  21. public static void main(String[] args) {
  22. new Thread(new ThreadA()).start();
  23. new Thread(new ThreadB()).start();
  24. }
  25. }

执行这个程序,你会在控制台看到,线程A和线程B各自独立工作,输出自己的打印值。如下是我的电脑上某一次运行的结果。每一次运行结果都会不一样。

 
  1. ....
  2. Thread A 48
  3. Thread A 49
  4. Thread B 0
  5. Thread A 50
  6. Thread B 1
  7. Thread A 51
  8. Thread A 52
  9. ....

那我现在有一个需求,我想等A先执行完之后,再由B去执行,怎么办呢?最简单的方式就是使用一个“对象锁”:

 
  1. public class ObjectLock {
  2. private static Object lock = new Object();
  3.  
  4. static class ThreadA implements Runnable {
  5. @Override
  6. public void run() {
  7. synchronized (lock) {
  8. for (int i = 0; i < 100; i++) {
  9. System.out.println("Thread A " + i);
  10. }
  11. }
  12. }
  13. }
  14.  
  15. static class ThreadB implements Runnable {
  16. @Override
  17. public void run() {
  18. synchronized (lock) {
  19. for (int i = 0; i < 100; i++) {
  20. System.out.println("Thread B " + i);
  21. }
  22. }
  23. }
  24. }
  25.  
  26. public static void main(String[] args) throws InterruptedException {
  27. new Thread(new ThreadA()).start();
  28. Thread.sleep(10);
  29. new Thread(new ThreadB()).start();
  30. }
  31. }

这里声明了一个名字为lock的对象锁。我们在ThreadAThreadB内需要同步的代码块里,都是用synchronized关键字加上了同一个对象锁lock

上文我们说到了,根据线程和锁的关系,同一时间只有一个线程持有一个锁,那么线程B就会等线程A执行完成后释放lock,线程B才能获得锁lock

这里在主线程里使用sleep方法睡眠了10毫秒,是为了防止线程B先得到锁。因为如果同时start,线程A和线程B都是出于就绪状态,操作系统可能会先让B运行。这样就会先输出B的内容,然后B执行完成之后自动释放锁,线程A再执行。

5.2 等待/通知机制

上面一种基于“锁”的方式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会耗费服务器资源。

而等待/通知机制是另一种方式。

Java多线程的等待/通知机制是基于Object类的wait()方法和notify()notifyAll()方法来实现的。

notify()方法会随机叫醒一个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程。

前面我们讲到,一个锁同一时刻只能被一个线程持有。而假如线程A现在持有了一个锁lock并开始执行,它可以使用lock.wait()让自己进入等待状态。这个时候,lock这个锁是被释放了的。

这时,线程B获得了lock这个锁并开始执行,它可以在某一时刻,使用lock.notify(),通知之前持有lock锁并进入等待状态的线程A,说“线程A你不用等了,可以往下执行了”。

需要注意的是,这个时候线程B并没有释放锁lock,除非线程B这个时候使用lock.wait()释放锁,或者线程B执行结束自行释放锁,线程A才能得到lock锁。

我们用代码来实现一下:

 
  1. public class WaitAndNotify {
  2. private static Object lock = new Object();
  3.  
  4. static class ThreadA implements Runnable {
  5. @Override
  6. public void run() {
  7. synchronized (lock) {
  8. for (int i = 0; i < 5; i++) {
  9. try {
  10. System.out.println("ThreadA: " + i);
  11. lock.notify();
  12. lock.wait();
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. lock.notify();
  18. }
  19. }
  20. }
  21.  
  22. static class ThreadB implements Runnable {
  23. @Override
  24. public void run() {
  25. synchronized (lock) {
  26. for (int i = 0; i < 5; i++) {
  27. try {
  28. System.out.println("ThreadB: " + i);
  29. lock.notify();
  30. lock.wait();
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. lock.notify();
  36. }
  37. }
  38. }
  39.  
  40. public static void main(String[] args) throws InterruptedException {
  41. new Thread(new ThreadA()).start();
  42. Thread.sleep(1000);
  43. new Thread(new ThreadB()).start();
  44. }
  45. }
  46.  
  47. // 输出:
  48. ThreadA: 0
  49. ThreadB: 0
  50. ThreadA: 1
  51. ThreadB: 1
  52. ThreadA: 2
  53. ThreadB: 2
  54. ThreadA: 3
  55. ThreadB: 3
  56. ThreadA: 4
  57. ThreadB: 4

在这个Demo里,线程A和线程B首先打印出自己需要的东西,然后使用notify()方法叫醒另一个正在等待的线程,然后自己使用wait()方法陷入等待并释放lock锁。

需要注意的是等待/通知机制使用的是使用同一个对象锁,如果你两个线程使用的是不同的对象锁,那它们之间是不能用等待/通知机制通信的。

5.3 信号量

JDK提供了一个类似于“信号量”功能的类Semaphore。但本文不是要介绍这个类,而是介绍一种基于volatile关键字的自己实现的信号量通信。

后面会有专门的章节介绍volatile关键字,这里只是做一个简单的介绍。

volitile关键字能够保证内存的可见性,如果用volitile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的。

比如我现在有一个需求,我想让线程A输出0,然后线程B输出1,再然后线程A输出2…以此类推。我应该怎样实现呢?

代码:

 
  1. public class Signal {
  2. private static volatile int signal = 0;
  3.  
  4. static class ThreadA implements Runnable {
  5. @Override
  6. public void run() {
  7. while (signal < 5) {
  8. if (signal % 2 == 0) {
  9. System.out.println("threadA: " + signal);
  10. synchronized (this) {
  11. signal++;
  12. }
  13. }
  14. }
  15. }
  16. }
  17.  
  18. static class ThreadB implements Runnable {
  19. @Override
  20. public void run() {
  21. while (signal < 5) {
  22. if (signal % 2 == 1) {
  23. System.out.println("threadB: " + signal);
  24. synchronized (this) {
  25. signal = signal + 1;
  26. }
  27. }
  28. }
  29. }
  30. }
  31.  
  32. public static void main(String[] args) throws InterruptedException {
  33. new Thread(new ThreadA()).start();
  34. Thread.sleep(1000);
  35. new Thread(new ThreadB()).start();
  36. }
  37. }
  38.  
  39. // 输出:
  40. threadA: 0
  41. threadB: 1
  42. threadA: 2
  43. threadB: 3
  44. threadA: 4

我们可以看到,使用了一个volatile变量signal来实现了“信号量”的模型。这里需要注意的是,volatile变量需要进行原子操作。signal++并不是一个原子操作,所以我们需要使用synchronized给它“上锁”。

这种实现方式并不一定高效,本例只是演示信号量

信号量的应用场景:

假如在一个停车场中,车位是我们的公共资源,线程就如同车辆,而看门的管理员就是起的“信号量”的作用。

因为在这种场景下,多个线程(超过2个)需要相互合作,我们用简单的“锁”和“等待通知机制”就不那么方便了。这个时候就可以用到信号量。

其实JDK中提供的很多多线程通信工具类都是基于信号量模型的。我们会在后面第三篇的文章中介绍一些常用的通信工具类。

5.4 管道

管道是基于“管道流”的通信方式。JDK提供了PipedWriter、 PipedReader、 PipedOutputStream、 PipedInputStream。其中,前面两个是基于字符的,后面两个是基于字节流的。

这里的示例代码使用的是基于字符的:

 
  1. public class Pipe {
  2. static class ReaderThread implements Runnable {
  3. private PipedReader reader;
  4.  
  5. public ReaderThread(PipedReader reader) {
  6. this.reader = reader;
  7. }
  8.  
  9. @Override
  10. public void run() {
  11. System.out.println("this is reader");
  12. int receive = 0;
  13. try {
  14. while ((receive = reader.read()) != -1) {
  15. System.out.print((char)receive);
  16. }
  17. } catch (IOException e) {
  18. e.printStackTrace();
  19. }
  20. }
  21. }
  22.  
  23. static class WriterThread implements Runnable {
  24.  
  25. private PipedWriter writer;
  26.  
  27. public WriterThread(PipedWriter writer) {
  28. this.writer = writer;
  29. }
  30.  
  31. @Override
  32. public void run() {
  33. System.out.println("this is writer");
  34. int receive = 0;
  35. try {
  36. writer.write("test");
  37. } catch (IOException e) {
  38. e.printStackTrace();
  39. } finally {
  40. try {
  41. writer.close();
  42. } catch (IOException e) {
  43. e.printStackTrace();
  44. }
  45. }
  46. }
  47. }
  48.  
  49. public static void main(String[] args) throws IOException, InterruptedException {
  50. PipedWriter writer = new PipedWriter();
  51. PipedReader reader = new PipedReader();
  52. writer.connect(reader); // 这里注意一定要连接,才能通信
  53.  
  54. new Thread(new ReaderThread(reader)).start();
  55. Thread.sleep(1000);
  56. new Thread(new WriterThread(writer)).start();
  57. }
  58. }
  59.  
  60. // 输出:
  61. this is reader
  62. this is writer
  63. test

我们通过线程的构造函数,传入了PipedWritePipedReader对象。可以简单分析一下这个示例代码的执行流程:

  1. 线程ReaderThread开始执行,
  2. 线程ReaderThread使用管道reader.read()进入”阻塞“,
  3. 线程WriterThread开始执行,
  4. 线程WriterThread用writer.write(“test”)往管道写入字符串,
  5. 线程WriterThread使用writer.close()结束管道写入,并执行完毕,
  6. 线程ReaderThread接受到管道输出的字符串并打印,
  7. 线程ReaderThread执行完毕。

管道通信的应用场景:

这个很好理解。使用管道多半与I/O流相关。当我们一个线程需要先另一个线程发送一个信息(比如字符串)或者文件等等时,就需要使用管道通信了。

5.5 其它通信相关

以上介绍了一些线程间通信的基本原理和方法。除此以外,还有一些与线程通信相关的知识点,这里一并介绍。

5.5.1 join方法

join()方法是Thread类的一个实例方法。它的作用是让当前线程陷入“等待”状态,等join的这个线程执行完成后,再继续执行当前线程。

有时候,主线程创建并启动了子线程,如果子线程中需要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。

如果主线程想等待子线程执行完毕后,获得子线程中的处理完的某个数据,就要用到join方法了。

示例代码:

 
  1. public class Join {
  2. static class ThreadA implements Runnable {
  3.  
  4. @Override
  5. public void run() {
  6. try {
  7. System.out.println("我是子线程,我先睡一秒");
  8. Thread.sleep(1000);
  9. System.out.println("我是子线程,我睡完了一秒");
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. }
  15.  
  16. public static void main(String[] args) throws InterruptedException {
  17. Thread thread = new Thread(new ThreadA());
  18. thread.start();
  19. thread.join();
  20. System.out.println("如果不加join方法,我会先被打出来,加了就不一样了");
  21. }
  22. }

注意join()方法有两个重载方法,一个是join(long), 一个是join(long, int)。

实际上,通过源码你会发现,join()方法及其重载方法底层都是利用了wait(long)这个方法。

对于join(long, int),通过查看源码(JDK 1.8)发现,底层并没有精确到纳秒,而是对第二个参数做了简单的判断和处理。

5.5.2 sleep方法

sleep方法是Thread类的一个静态方法。它的作用是让当前线程睡眠一段时间。它有这样两个方法:

  • Thread.sleep(long)
  • Thread.sleep(long, int)

同样,查看源码(JDK 1.8)发现,第二个方法貌似只对第二个参数做了简单的处理,没有精确到纳秒。实际上还是调用的第一个方法。

这里需要强调一下:sleep方法是不会释放当前的锁的,而wait方法会。这也是最常见的一个多线程面试题。

它们还有这些区别:

  • wait可以指定时间,也可以不指定;而sleep必须指定时间。
  • wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易死锁。
  • wait必须放在同步块或同步方法中,而sleep可以再任意位置

5.5.3 ThreadLocal类

ThreadLocal是一个本地线程副本变量工具类。内部是一个弱引用的Map来维护。这里不详细介绍它的原理,而是只是介绍它的使用,以后有独立章节来介绍ThreadLocal类的原理。

有些朋友称ThreadLocal为线程本地变量线程本地存储。严格来说,ThreadLocal类并不属于多线程间的通信,而是让每个线程有自己”独立“的变量,线程之间互不影响。它为每个线程都创建一个副本,每个线程可以访问自己内部的副本变量。

ThreadLocal类最常用的就是set方法和get方法。示例代码:

 
  1. public class ThreadLocalDemo {
  2. static class ThreadA implements Runnable {
  3. private ThreadLocal<String> threadLocal;
  4.  
  5. public ThreadA(ThreadLocal<String> threadLocal) {
  6. this.threadLocal = threadLocal;
  7. }
  8.  
  9. @Override
  10. public void run() {
  11. threadLocal.set("A");
  12. try {
  13. Thread.sleep(1000);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println("ThreadA输出:" + threadLocal.get());
  18. }
  19.  
  20. static class ThreadB implements Runnable {
  21. private ThreadLocal<String> threadLocal;
  22.  
  23. public ThreadB(ThreadLocal<String> threadLocal) {
  24. this.threadLocal = threadLocal;
  25. }
  26.  
  27. @Override
  28. public void run() {
  29. threadLocal.set("B");
  30. try {
  31. Thread.sleep(1000);
  32. } catch (InterruptedException e) {
  33. e.printStackTrace();
  34. }
  35. System.out.println("ThreadB输出:" + threadLocal.get());
  36. }
  37. }
  38.  
  39. public static void main(String[] args) {
  40. ThreadLocal<String> threadLocal = new ThreadLocal<>();
  41. new Thread(new ThreadA(threadLocal)).start();
  42. new Thread(new ThreadB(threadLocal)).start();
  43. }
  44. }
  45. }
  46.  
  47. // 输出:
  48. ThreadA输出:A
  49. ThreadB输出:B

可以看到,虽然两个线程使用的同一个ThreadLocal实例(通过构造方法传入),但是它们各自可以存取自己当前线程的一个值。

那ThreadLocal有什么作用呢?如果只是单纯的想要线程隔离,在每个线程中声明一个私有变量就好了呀,为什么要使用ThreadLocal?

如果开发者希望将类的某个静态变量(user ID或者transaction ID)与线程状态关联,则可以考虑使用ThreadLocal。

最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。数据库连接和Session管理涉及多个复杂对象的初始化和关闭。如果在每个线程中声明一些私有变量来进行操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭连接。

5.5.4 InheritableThreadLocal

InheritableThreadLocal类与ThreadLocal类稍有不同,Inheritable是继承的意思。它不仅仅是当前线程可以存取副本值,而且它的子线程也可以存取这个副本值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值