Java多线程编程(6)--线程间通信(下)

  因为本文的内容大部分是以生产者/消费者模式来进行讲解和举例的,所以在开始学习本文介绍的几种线程间的通信方式之前,我们先来熟悉一下生产者/消费者模式。
  在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据(可能是消息、文件、任务等),这些数据由另一个模块来负责处理。产生数据的模块,就形象地被称为生产者;而处理数据的模块,就被称为消费者。
  单单抽象出生产者和消费者,还称不上是生产者/消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间来作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。因此,生产者/消费者模式大概的结构如下图:

  我们可以使用不同的线程来模拟生产者和消费者。对应的,生产数据的线程就被称为是生产者线程,而消费数据的线程就被称为是消费者。而缓冲区则可以选用那些线程安全的数据结构来模拟,因此,缓冲区在这里就起到了线程间通信工具的作用。本文将会通过生产者/消费者模式作为例子来介绍几种线程间的通信方式。

一.阻塞队列

1.BlockingQueue接口

  如何选择合适的数据结构来作为缓冲区呢?首先我们来分析一下我们的需求。首先,消费者应该是要按照生产者生产的顺序来消费数据的,那么我们脑海中浮现的一定是具有先进先出特性的队列了。其次,既然是在多线程之间进行传递,那么这个类一定是线程安全的。因此,缓冲区应该使用线程安全的队列。我们首先想到的应该是ConcurrentLinkedQueue,它是使用链表实现的线程安全的队列。但是,这个类是非阻塞的,这意味着当生产者向缓冲区中放入数据时,缓冲区是否已满时需要生产者自己去判断的;同理,当消费者去消费缓冲区中的数据时,缓冲区是否为空也是需要自己去判断的。由于这个类是非阻塞的,因此我们只能在线程中不断的去轮询缓冲区,这显然不是多线程编程该有的实现方式。
  那么,有没有可以阻塞线程的队列呢?答案是肯定的。java.util.concurrent包中的BlockingQueue接口定义了阻塞队列的行为。当阻塞队列中没有数据的时候,消费者端的线程会被挂起直到有数据被放入队列;当阻塞队列中填满数据的时候,生产者端的线程会被挂起直到队列中有空的位置。
  下面来介绍BlockingQueue接口中定义的方法。BlockingQueue接口时Queue接口的子接口,因此它继承了Queue接口中所有的方法,由于这些方法都比较简单,因此不再赘述。这里只介绍BlockingQueue接口中新增的方法。

(1)放入数据

  • void put​(E e) throws InterruptedException
    将指定元素e放入队列。如果队列没有空间,则当前线程会阻塞直到队列有空间。
  • boolean offer​(E e, long timeout, TimeUnit unit) throws InterruptedException
    将指定元素e放入队列。如果队列没有空间,则当前线程会阻塞直到队列有空间或等待超时。

(2)取出数据

  • E take() throws InterruptedException
    从队列中取出元素。如果队列中没有元素,则当前线程会阻塞直到队列中有元素。
  • E poll​(long timeout, TimeUnit unit) throws InterruptedException
    从队列中取出元素。如果队列中没有元素,则当前线程会阻塞直到队列中有元素或等待超时。
  • int drainTo​(Collection<? super E> c)
    一次性从队列中取出所有可用的数据对象放在指定的集合中。
  • int drainTo​(Collection<? super E> c, int maxElements)
    同上,maxElements可以限制最多获取的元素个数。

2.BlockingQueue接口的实现类

  java.util.concurrent包为BlockingQueue接口提供了7个实现类。

(1)ArrayBlockingQueue

  ArrayBlockingQueue是基于数组实现的有界的阻塞队列,其内部维护了一个定长数组来存放队列中的数据对象。由于其是有界的,因此在构造ArrayBlockingQueue实例时,必须提供队列的容量。这是一个非常常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还维护了两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

  阻塞队列按照其存储空间的容量是否受限制来划分,可以分为有界队列和无界队列。有界队列的存储容量限制是在构造实例的时候指定的,而无界队列实际上也有存储容量限制,其默认的最大存储容量为Integer.MAX_VALUE(即231-1)个元素。然而实际情况是,无界队列往往会在还没到达存储容量限制时就已经造成了OutOfMemoryError。
  ArrayBlockingQueue在写入数据和获取数据时,使用的是同一个锁对象,这就意味着两者无法达到真正的并行。其实按照实现原理来分析,ArrayBlockingQueue完全在两种操作上使用不同的锁,从而实现生产者和消费者操作的完全并行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,在性能上并不会有太大的提升。
  此外,在创建ArrayBlockingQueue实例时,我们还可以指定内部的锁是否采用公平锁,默认情况下采用非公平锁。

(2)LinkedBlockingQueue

  LinkedBlockingQueue是基于链表实现的阻塞队列,其既可以是有界的,也可以是无界的。如果在构造LinkedBlockingQueue实例时没有提供队列的容量,则会构造出一个无界的队列,反之则会构造出一个有界的队列。
  LinkedBlockingQueue也是一个非常常用的阻塞队列,其内部维护了一个链表,对于数据的写入操作是在链表头部进行的,而对于数据的获取操作是在链表尾部进行的。LinkedBlockingQueue内部对于数据的写入和读取采用了两个锁来控制,即putLock和takeLock,它们都是非公平锁。两种操作对应了两把锁意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,这可以有效地提高整个队列的并发性能。但是,相较于ArrayBlockingQueue,LinkedBlockingQueue在写入和读取数据时,需要动态地创建和删除链表节点,在高并发和数据量大的时候,GC压力很大。
  ArrayBlockingQueue和LinkedBlockingQueue是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题时,使用这两个类足以解决大部分问题。

(3)DelayQueue

  DelayQueue是一个存放延时元素的无界阻塞队列,它对队列中的元素做出了限制,即E extends Delayed,这意味着队列中的元素必须实现Delayed接口。Delayed接口用于标记具有延时功能的对象,即只有在给定的延迟时间结束之后才能对该对象进行操作。Delayed接口中只定义了一个方法getDelay​(TimeUnit unit),但是由于它是Comparable接口的子接口,因此它还继承了compareTo方法,这个方法在实现时需要根据getDelay的结果来进行排序。
  DelayQueue内部维护了一个优先级队列,即PriorityQueue,该队列是以延时结束的时间做为优先级来存放元素的,延时结束时间越早,优先级越高。当试图从延时队列中取出元素时,会先从优先级队列中取出优先级最高的元素,若该元素延时时间已经结束,则直接返回;否则将会阻塞当前线程直到延时结束。向队列中放入元素时,除了获取操作优先级队列的锁之外没有其他限制。该队列的读取和写入使用的是同一把锁(非公平锁),因此该队列的消费者和生产者无法并行操作。

(4)PriorityBlockingQueue

  PriorityBlockingQueue很好理解,可以将其看作线程安全的、具有阻塞功能的无界优先级队列。PriorityBlockingQueue的put和take操作都加了锁,并且它们使用的是同一把非公平锁,这意味着该队列上的消费者和生产者无法并行操作。由于该队列是无界的,因此该队列不会阻塞生产者,但是当队列中没有元素的时候会阻塞消费者。虽然该队列是无界的,但是它仍然提供了可以指定队列初始化大小的构造方法,这是因为该队列会在队列已满的情况下进行自动扩容。

(5)SynchronousQueue

  SynchronousQueue是一种较为特殊的阻塞队列,其内部并没有存储队列元素的空间。当生产者线程执行put操作时,如果没有消费者线程在执行take操作,那么该生产者线程会被阻塞;当消费者线程在执行take操作时,如果没有生产者线程在执行put操作,那么该消费者线程也会被阻塞。
  SynchronousQueue类提供了两个构造方法,分别是SynchronousQueue()和SynchronousQueue(boolean fair)。第一种构造器默认采用了非公平策略(实际上是LIFO),第二种构造器则可以指定队列采用非公平策略还是公平策略。
  此外,由于SynchronousQueue本身并不存储元素,因此该队列对于Queue接口中定义的大部分方法都具有固定的返回值,例如peek()总是返回null,size()总是返回0等。

(6)LinkedTransferQueue

  LinkedTransferQueue是一种用链表实现的无界阻塞队列,它实现了TransferQueue接口,而TransferQueue接口是BlockingQueue接口的子接口,因此它也是阻塞队列的一种。
  下面是TransferQueue接口定义的方法:

  除了这几个方法外,该队列其他方法的行为和LinkedBlockingQueue类似,这里不再过多赘述。

(7)LinkedBlockingDeque

  顾名思义,这个类是用链表实现的阻塞双端队列,和LinkedBlockingQueue类似,它既可以是有界的,也可以是无界的。实际上,除了具有双端队列的特性外,该类与LinkedBlockingQueue十分相似,可以参照上面对LinkedBlockingQueue的介绍来理解它,这里不再详细介绍。

二.信号量Semaphore

  Semaphore类是一个计数信号量。为了便于讨论,我们把代码所访问的特定资源或者执行特定操作的机会同意看作是一种资源,可以将其称之为虚拟资源。Semaphore相当于虚拟资源访问许可管理器,它可以用来控制同一时间内对虚拟资源的访问次数。为了对虚拟资源的访问进行流量控制,我们必须使相应代码只有在获得许可的情况下才能够访问这些资源。基于这种思想,在访问虚拟资源前应该先申请许可,在访问后应该释放许可。
  Semaphore的acquire和release方法分别用于申请和释放许可。如果当前可用的许可数等于0或小于0(在构造Semaphore实例的时候可以指定许可数为0或负数),那么acquire方法会使执行线程暂停。Semaphore内部维护了一个队列来存储这些被暂停的线程,默认情况下,Semaphore使用非公平策略,当然也可以在构造方法中显式指定Semaphore实例使用公平策略还是非公平策略。
  下面是Semaphore类提供的所有方法:

三.管道流

  Java语言提供了各种各样的输入/输出流,使我们能够很方便地对数据进行操作,其中管道流是一种特殊的流,用于在不同的线程间直接传送数据。一个线程发送数据到管道,另一个线程从管道中读取数据。通过使用管道,可以实现不同线程间的通信,而无需借助临时文件等数据中介。
  和其他流类似,管道流也分为字节流和字符流,其中字节流对应的输入流和输出流分别是PipedInputStream和PipedOutputStream,字符流对应的输入流和输出流分别是PipedReader和PipedWriter。
  管道流实际上是使用一个循环缓冲数组来实现的,输入流从这个数组中读数据,输出流向这个数组中写数据。当缓冲区满时,输出流所在的线程将会阻塞,当缓冲区空时,输入流所在的线程将会阻塞。这个数组位于输入流内部,默认大小为1024,也可以通过管道输入流的构造方法来指定缓冲数组大小。
  管道输入流和输出流在使用之前必须先建立连接,可以通过输入流或输出流的构造方法或connect方法来使两个流建立连接。假设in是线程A的输入流,out是线程B的输出流,那么可以通过以下几种方法来建立连接:

PipedInputStream in = new PipedInputStream(out);   //方法1

PipedInputStream in = new PipedInputStream();      //方法2
in.connect(out);

PipedOutputStream out = new PipedOutputStream(in); //方法3

PipedOutputStream out = new PipedOutputStream();   //方法4
out.connect(in);

  不要在同一个线程中同时使用管道输入流和管道输出流,这样有可能会引起死锁。因为当缓冲区满时,如果继续向输出流中写入数据,则会阻塞当前线程,从而造成从输入流中读取数据的代码永远不会被执行到,造成死锁;缓冲区空时,如果继续从输出流中读取数据,也会阻塞当前线程,从而造成向输出流中写入数据的代码永远不会被执行到,造成死锁。
  下面是使用管道字符流编写的一个demo:

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;

public class PipedStreamDemo {
    private static final String content = "Hello world";
    
    public static void main(String[] args) {
        try {
            PipedReader reader = new PipedReader();
            PipedWriter writer = new PipedWriter(reader);

            Receiver receiver = new Receiver(reader);
            Sender sender = new Sender(writer);
            receiver.start();
            sender.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static class Sender extends Thread {
        private PipedWriter writer;
        
        public Sender(PipedWriter writer) {
            this.writer = writer;
        }
        
        @Override
        public void run() {
            try {
                System.out.println("Send : " + content);
                char[] chars = content.toCharArray();
                writer.write(chars, 0, chars.length);
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static class Receiver extends Thread {
        private PipedReader reader;
        
        public Receiver(PipedReader reader) {
            this.reader = reader;
        }

        @Override
        public void run() {
            try {
                int ch;
                while ((ch = reader.read()) != -1) {
                    System.out.println("Received : " + (char) ch);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

  该程序输出如下:

Send : Hello world
Received : H
Received : e
Received : l
Received : l
Received : o
Received :  
Received : w
Received : o
Received : r
Received : l
Received : d

四.交换器Exchanger

  Exchanger<V>是一个用于在两个线程之间交换数据的工具类。两个线程可以通过同一个Exchanger实例的exchange方法来交换数据。当一个线程先执行exchange方法时,它会被阻塞并等待另一个线程的到来;当另一个线程也执行exchange方法时,前一个线程会被唤醒,两个线程完成数据交换并继续执行。
  Exchanger提供了两个exchange方法:

  下面是一个使用Exchanger的例子:

import java.util.concurrent.Exchanger;

public class ExchangerDemo {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();
        new Buyer(exchanger).start();
        new Seller(exchanger).start();
    }

    private static class Buyer extends Thread {
        private Exchanger<String> exchanger;

        Buyer(Exchanger<String> exchanger) {
            this.exchanger = exchanger;
        }

        @Override
        public void run() {
            try {
                String money = "10元";
                Thread.sleep(2000);
                System.out.println("买家:拿出" + money);
                String good = exchanger.exchange(money);
                System.out.println("买家:得到" + good);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static class Seller extends Thread {
        private Exchanger<String> exchanger;

        Seller(Exchanger<String> exchanger) {
            this.exchanger = exchanger;
        }

        @Override
        public void run() {
            try {
                String good = "10斤大白菜";
                System.out.println("卖家:拿出" + good);
                System.out.println("卖家:等待买家...");
                String money = exchanger.exchange(good);
                System.out.println("卖家:得到" + money);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

  该程序的输出如下:

卖家:拿出10斤大白菜
卖家:等待买家...
买家:拿出10元
买家:得到10斤大白菜
卖家:得到10元

五.线程中断机制

  有时候我们需要停止一个线程,例如一个下载线程,该线程在没有下载成功之前不会退出,若此时用户觉得下载速度慢,不想下载而点击了取消按钮,此时我们应该停止这个下载线程并释放资源。但是,由于Thread.stop、Thread.suspend和Thread.resume过于暴力而被废弃,那么我们应该如何优雅地停止一个线程呢?
  Java为我们提供了线程中断机制。中断机制的思想是:一个线程不应该由其他线程来强制中断,而是应该由线程自己自行判断。中断可以看作是由一个线程发送给另外一个线程的一种指示,该指示用于表示发起线程希望目标线程停止其正在执行的操作。但是,中断一个线程并不代表马上停止该线程,而只是通知该线程应该中断了,是否应该中断以及如何响应中断则应该交给该线程来自行处理。

  在Java的API或语言规范中,并没有将中断与任何取消语义关联起来。但实际上,如果在取消之外的其他操作中使用中断,那么都是不合适的,并且很难支持起更大的应用。

1.API

  实际上,每个线程内部都有一个boolean类型的中断标记,当中断一个线程时,该线程内部的中断标记将会被设置为true。以下是Thread类中与中断有关的三个方法:

void interrupt()

boolean isInterrupted()

static boolean interrupted()

  下面将分别对这三个方法进行介绍。

(1)interrupt

  调用一个线程的interrupt方法会将该线程的中断标记设置为true。

  1. 如果该线程由于调用Object.wait()、Object.wait(long)、Object.wait(long, int)、Thread.join()、Thread.join(long)、Thread.join(long, int)、Thread.sleep()或Thread.sleep(long, int)而进入等待状态,该线程的中断标记将会被清除并收到一个InterruptedException。
  2. 如果该线程阻塞在一个基于InterruptibleChannel的I/O操作上,这个channel将会被关闭并收到一个ClosedByInterruptException。
  3. 如果该线程被阻塞在一个Selector里,则该线程会马上从选择操作中返回,返回值可能是非0值,就好像Selector的wakeup方法被调用过一样。
  4. 如果以上条件均不成立,那么该线程仅仅只是中断标记被设置为true,并不会表现出其他行为。

  上面的2、3两个条件与NIO有关,这里只是顺便提到而已。后续会推出关于NIO的系列教程。

(2)isInterrupted

  该方法较为简单,只是返回该线程的中断标记,并不会影响该中断标记和产生其他行为。

(3)interrupted

  该方法是一个静态方法,也会返回该线程的中断标记。但是该方法与isInterrupted最大的区别在于该方法会清除线程的中断状态。例如,如果当前线程已经被中断,那么调用interrupted方法将会返回true并同时清除中断状态;如果当前线程未被中断,则会返回false。这个方法的好处在于返回了线程中断标记的同时还清除了中断标记。

2.线程在不同状态下对中断的反应

  线程一共有6种状态,分别是NEW、RUNNABLE、WAITING、TIMED_WAITING、BLOCKED和TERMINATED。在不同状态下,线程可能会对中断产生不同的反应。

(1)NEW/TERMINATED

  由于处于NEW状态的线程还没有启动,而处于TERMINATED状态的线程已经终止,Java认为对处于这两种状态下的线程进行中断毫无意义,所以并不会将线程的中断标识设置为true,也不会产生其他的行为。

public class NewAndTerminatedDemo {
    public static void main(String[] args) {
        Thread thread = new Thread();
        System.out.println(thread.getState());
        thread.interrupt();
        System.out.println(thread.isInterrupted());
        thread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(thread.getState());
        thread.interrupt();
        System.out.println(thread.isInterrupted());
    }
}

  上面的例子输出如下:

NEW
false
TERMINATED
false

(2)RUNNABLE

  处于RUNNABLE状态下的线程被中断后,除了中断标记被设置为true外不会产生其他行为,下面我们来做个实验:

public class RunnableDemo {
    public static void main(String[] args) {
        TimeWasteThread timeWasteThread = new TimeWasteThread();
        timeWasteThread.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        timeWasteThread.interrupt();
        System.out.println("The interrupt flag of timeWasteThread is " + timeWasteThread.isInterrupted());
    }

    private static class TimeWasteThread extends Thread {
        @Override
        public void run() {
            while (true) {
                System.out.println("The 40th fibonacci number is " + fibonacci(40));
            }
        }

        private int fibonacci(int n) {
            if (n == 1 || n == 2) {
                return 1;
            }
            return fibonacci(n - 1) + fibonacci(n - 2);
        }
    }
}

  在上面的例子中,TimeWasteThread内部执行了一个非常耗时的操作——计算第40个斐波那契数(这种写法在每次计算时都需要重新递归,因此非常耗时),这样做的目的是使它一直处于RUNNABLE状态,我们既不希望它太快结束,也不希望使用sleep方法,因为这是下一小节要讨论的内容。主线程在启动TimeWasteThread两秒后中断了该线程。该程序的输出如下:

The 40th fibonacci number is 102334155
The 40th fibonacci number is 102334155
The 40th fibonacci number is 102334155
The 40th fibonacci number is 102334155
The 40th fibonacci number is 102334155
The 40th fibonacci number is 102334155
The interrupt flag of timeWasteThread is true
The 40th fibonacci number is 102334155
The 40th fibonacci number is 102334155
The 40th fibonacci number is 102334155
The 40th fibonacci number is 102334155
...

  可以看到,在主线程对TimeWasteThread发出了中断信号后,TimeWasteThread的中断标记确实变成了true,可以程序仍然在继续执行,没有受到任何影响。虽然主线程已经调用了TimeWasteThread的interrupt方法,可该线程并没有中断运行。既然如此,那我要这中断机制有何用?
  我们在上面提到过,中断机制的思想是一个线程是否中断应该由该线程来判断,而不应该由其他线程来控制。因此,我们可以在线程内部来判断当前线程是否需要被中断,然后做出相应的决策。
  基于这种思想,我们将TimeWasteThread的run方法修改如下:

@Override
public void run() {
    while (!Thread.interrupted()) {
        System.out.println("The 40th fibonacci number is " + fibonacci(40));
    }
}

  重新运行该程序,输出如下:

The 40th fibonacci number is 102334155
The 40th fibonacci number is 102334155
The 40th fibonacci number is 102334155
The 40th fibonacci number is 102334155
The 40th fibonacci number is 102334155
The 40th fibonacci number is 102334155
The interrupt flag of timeWasteThread is true
The 40th fibonacci number is 102334155

  可以看到,TimeWasteThread在收到中断信号后很快就停了下来,达到了中断的目的。

(3)WAITING/TIMED_WAITING

  这两种状态本质上可以看作是等待状态,只不过一个是无限期等待,而另一个是有时间限制的等待,因此放在一起讨论。
  下面的例子中,我们让SleepingThread进入TIMED_WAITING状态后将其中断:

public class WaitingDemo {
    public static void main(String[] args) {
        Thread sleepingThread = new SleepingThread();
        sleepingThread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        sleepingThread.interrupt();
    }

    private static class SleepingThread extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                System.out.println("Thread has been interrupted.");
                System.out.println("The interrupt flag of sleepingThread is " + Thread.currentThread().isInterrupted());
            }
        }
    }
}

  该程序的输出如下:

Thread has been interrupted.
The interrupt flag of sleepingThread is false

  在上面的例子中,主线程在SleepingThread处于TIMED_WAITING状态时将其中断,此时SleepingThread被唤醒并抛出了一个InterruptedException异常。也就是说,处于WAITING或者TIMED_WAITING状态的线程被中断时往往是通过InterruptedException异常(有时也会通过其他异常,例如ClosedByInterruptException异常)来进行通知的。
  不过,当SleepingThread由于中断而被唤醒时,它的中断标记却是false,而我们确确实实在主线程中已经调用了它的interrupt方法,这是为什么呢?实际上,按照惯例,抛出InterruptedException异常的方法,通常会在抛出该异常时将当前线程的中断标记重置为false。这是因为,当捕获到InterruptedException异常时,我们已经知道线程被中断了,那么此时的中断标记对于我们来说已经没用了,但是我们还需要手动将它再设置为false,方便下次使用。因此,为了使用方便,方法在抛出InterruptedException异常之前应该将当前线程的终端标记重置为false。

(4)BLOCKED

  只有在等待一个对象的监视器的线程才会处于BLOCKED状态。下面我们通过一个例子来演示对处于BLOCKED状态的线程调用interrupt方法会发生什么。

public class BlockedDemo {
    private static final Integer foo = 1;

    public static void main(String[] args) {
        Thread thread1 = new Thread1();
        thread1.start();
        Thread thread2 = new Thread2();
        thread2.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("The state of thread2 is " + thread2.getState());
        thread2.interrupt();
        System.out.println("The interrupt flag of thread2 is " + thread2.isInterrupted());
        System.out.println("The state of thread2 is " + thread2.getState());
    }

    private static class Thread1 extends Thread {
        @Override
        public void run() {
            synchronized (foo) {
                for (int i = 0; i < 5; i++) {
                    fibonacci(40);
                }
            }
        }

        private int fibonacci(int n) {
            if (n == 1 || n == 2) {
                return 1;
            }
            return fibonacci(n - 1) + fibonacci(n - 2);
        }
    }

    private static class Thread2 extends Thread {
        @Override
        public void run() {
            System.out.println("Thread2 tries to get the monitor of object foo.");
            synchronized (foo) {
                System.out.println("Thread2 got the monitor of object foo.");
                System.out.println("The interrupt flag of thread2 is " + Thread.currentThread().isInterrupted());
            }
        }
    }
}

  该程序的输出如下:

Thread2 tries to get the monitor of object foo      (1)
The state of thread2 is BLOCKED                     (2)
The interrupt flag of thread2 is true               (3)
The state of thread2 is BLOCKED                     (4)
Thread2 got the monitor of object foo               (5)
The interrupt flag of thread2 is true               (6)

  下面依次分析每一条输出:
  (1)Thread2启动,尝试获取foo对象的监视器;
  (2)由于Thread1先获取到了foo对象的监视器且持有较长时间,Thread2需要等待Thread1释放foo对象的监视器,因此Thread2进入BLOCKED状态;
  (3)对处于BLOCKED状态的Thread2执行interrupt方法,该线程的中断标记变成true;
  (4)对Thread2执行interrupt方法后,该线程仍然处于BLOCKED状态;
  (5)因为Thread1释放了foo对象的监视器,所以Thread2获取到了该监视器,继续执行下面的代码;
  (6)Thread2重新进入RUNNABLE状态后,中断标记仍然为true,此时可以根据中断标记来决定之后的逻辑。
  综上,对处于BLOCKED状态的线程调用interrupt方法,仅仅只是将该线程的中断标记设置为true,除此之外没有任何变化。

posted @ 2020-08-11 14:40  maconn  阅读(538)  评论(0编辑  收藏  举报