..

☕ Java Concurrency Thread

原文地址:https://docs.oracle.com/javase/tutorial/essential/concurrency/index.html

计算机的使用者理所当然地认为他们的系统可以同时处理多个事情。他们假设他们可以在其他应用下载文件的时候使用word,控制打印队列以及视频。即使是一个非常简单的应用,也被认为同时可以处理多个事情。比如:视频处理软件必须同时从网络上下载视频数据,解压,播放。即使是word也要同时处理键盘和鼠标的动作,无论它当前是正在处理文本格式化还是更新显示内容。这样的软件被称为并发(concurrent)软件。 Java语言生来就支持并发编程,在语言层面提供了基本的并发支持,以及并发库。自从5.0版本以来,Java引入了更高层次的并发API。本教程介绍Java提供的基础并发支持,并对java.util.concurrent包所提供的一些API进行介绍。

进程和线程

在并发编程中,与执行相关的有两个基本的单元:进程(process)和线程(thread)。 一个计算机系统通常有很多活跃的进程和线程,即使计算机只有一个核(core)也是如此。单核系统同时只有一个线程在执行,多个线程之间使用分时(time slicing)的方式来共享这个处理器核心。

目前,计算机系统多核架构已经成为主流,这极大地增强了一个系统的并发处理能力,但是并不是说单核系统不能搞并发,即使一个核也可以并发!

进程

一个进程是一个自包含的执行环境(self-contained environment)。一个进程通常有一个完成的,私有的运行时资源。特别地,每个进程都有它自己的内存空间。 进程通常被视为是和程序或者应用相同的概念。然而,用户眼中的一个应用可能实际上是由多个进程组成。为了处理进程间的通信,绝大多数操作系统支持进程内通信资源(Inner Process Communication,IPC),比如管道(pipe)和套接字(socket)。IPC不仅可以用于同一个系统内线程间的通信,也可以用于不同系统之间的通信。 绝大多数JVM都是作为一个进程来跑的。一个Java应用可以使用ProcessBuilder来创建进程。多进程应用超出了本教程的讲述范围。

线程

线程有时又被称为轻量级进程。进程和线程都提供了一种执行环境,但是创建线程比创建进程需要的资源更少。 线程在进程之内——每个进程最少有一个线程,线程之间共享进程的资源,包括内存和打开文件。这样使线程之间的通信变得更加简单,但是有时会引入一些问题。 多线程是Java程序的一个核心特性。每个应用至少有一个线程——如果你把系统线程,比如内存管理线程,信号处理线程也算上,那就有多个线程。但是从应用程序员的视角来看,开始的时候只有一个线程,被称作主线程(main thread)。这个线程可以创建其他的线程,稍后我们会介绍这一点。

Thread对象

每个线程都与Thread类的一个实例所对应。有两种使用Thread创建并发应用的基本方法:

  • 直接使用Thread类来进行线程的创建和管理,每当应用需要初始化一个异步任务时,实例化一个Thread。
  • 将线程的管理从应用中抽象出来,将应用的任务提交给执行器(executor) 本节介绍Thread类的使用方法,Executors在并发进阶中介绍。

线程的创建和启动

为了创建一个线程,你需要提供这个线程中运行的代码。可以有两种方式来做这件事:

  • 提供一个Runnable对象。Runnable接口定义了一个方法:run,它是这个线程中运行的代码,然后把Runnable结构体传递给Thread的构造函数,下面是一个例子:
public class HelloRunnable implements Runnable {  
  public static void main(String[] args) {  
    new Thread(new HelloRunnable()).start();  
  }  
  
  @Override  
  public void run() {  
    System.out.println("hello from a thread");  
  }  
}
  • 创建Thread的子类。Thread类本身实现了Runnable接口,尽管它的run方法什么也不干。所以我们可以继承Thread类,覆盖它的run方法。下面是一个例子:
public class HelloThread extends Thread {  
  @Override  public void run() {  
    System.out.println("hello from a thread");  
  }  
  
  public static void main(String[] args) {  
    new HelloThread().start();  
  }  
}

多一句,Thread类中run方法:

public  
class Thread implements Runnable {

	/* What will be run. */  
	private Runnable target;

	@Override  
	public void run() {  
	    if (target != null) {  
	        target.run();  
	    }  
	}
}

无论使用上述两种方法的哪一种,都是使用Thread.start来启动新的线程。 那么应该用哪一种方法呢?第一种方法,也就是实现Runnable接口,更加通用。因为还可以继承其他的类。第二种方法实现起来更加简单,但是因为已经继承自Thread类,所以不能继承自其他的类了。本课程更加推荐第一种做法,这样做可以把Runnable任务和Thread对象解耦,这样做更加灵活,并且对后面要介绍的并发高阶API也同样适用。 Thread类定义了线程管理相关的一些方法,这些方法包含静态方法,提供关于线程的信息、状态等。

线程的暂停

Thread.sleep方法会将当前正在执行的线程挂起一段时间。这样做可以将处理器时间让给应用内的其他线程,或者同一个系统内的其他应用。sleep方法也可以用于步调(pacing),就像下面这个例子展示的一样,也可以用来等待另外一个线程,SimpleThreads例子会展示这一点。 sleep方法有两个覆盖的版本:一个指定了休眠的时间,单位是毫秒。另一个单位是纳秒。然而,不能保证准确休眠指定的时间,因为这还取决于底层操作系统。同样,休眠周期可以被中断所终结,我们会在稍后介绍中断。不论在哪种情况下,你都不能假定调用sleep就会将当前的线程准确休眠指定的时间。 SleepMessages使用sleep来每隔4秒打印一个消息:

public class SleepMessages {  
  public static void main(String[] args) throws InterruptedException {  
    String[] importantInfo = {  
            "Mares eat oats",  
            "Does eat oats",  
            "Little lambs eat ivy",  
            "A kid will eat ivy too"  
    };  
    for (String s : importantInfo) {  
      //Pause for 4 seconds  
      Thread.sleep(4000);  
      //Print a message  
      System.out.println(s);  
    }
  }
}

注意,main方法声明了抛出InterruptedException异常。前面我们提到另外一个线程可能会中断正在休眠的线程,但是这是一个例外。因为这个应用只有一个线程,所以它可以好好睡一觉,不用担心被其他人打扰,也就不需要catch这个异常。

线程的中断

中断是对线程的一个提示,它告诉这个线程应该停下手头正在做的事情,然后去做一些别的。线程如何响应中断由程序员决定,但是常用的一种处理方法是终结这个线程的执行。 一个线程通过在一个Thread对象上调用interrupt方法来给另一个线程发送一个中断信号。为了确保中断机制工作正常,被中断的线程需要支持被中断。 一个线程如何支持被中断呢?这取决于这个线程正在做什么。如果一个线程经常调用抛出InterruptedException异常的方法,在catch到中断异常之后,它简单地从run方法返回。比如:假设SleepMessages的for循环在一个run方法中,那么应该像下面这样修改,以支持中断。

for (String s : importantInfo) {  
  //Pause for 4 seconds
  try {
	  Thread.sleep(4000);
  } catch (InterruptedException e) {
	  // We've been interrupted: no more messages.
	  return;
  }
    
  //Print a message  
  System.out.println(s);  
}

很多抛出InterruptedException异常的方法,比如sleep,都是这样设计的:如果抛出了中断异常,那么就应该直接返回。

那么如果一个线程很长时间都没有调用一个抛出中断异常的方法呢?如果是这样的话,它应该调用Thread.interrupted,这个方法会返回true,如果接收到一个中断信号的话。下面是一个例子:

for (int i = 0; i < inputs.length; i++) {
    heavyCrunch(inputs[i]);
    if (Thread.interrupted()) {
        // We've been interrupted: no more crunching.
        return;
    }
}

在上面这个简单的例子中,仅仅是测试一下中断状态,如果收到了中断信号,就直接退出。在更加复杂的应用中,可能抛出一个InterruptedException会更有意义。

if (Thread.interrupted()) {
    throw new InterruptedException();
}

这样做可以让中断处理相关的代码都放在catch中。

中断机制是通过使用内部的一个flag——中断状态来实现的。调用Thread.interrupt会设置这个flag。当一个线程通过调用静态方法Thread.interrupted之后,这个flag会被清空。非静态方法isInterrupted不会清空中断状态flag。

一个简单的例子

下面这个简单的例子综合了本节所讲的知识。SimpleThreads包含两个线程。第一个是每个Java应用都会有的主线程。主线程通过Runnable对象创建了一个新线程MessageLoop,然后等待它执行结束。如果MessageLoop太墨迹,执行了太久,主线程会中断它。 MessageLoop线程打印一系列的消息。如果在打印所有消息之前被中断了,MessageLoop线程会打印一个消息然后退出。

public class SimpleThreads {  
  static void threadMessage(String message) {  
    String threadName = Thread.currentThread().getName();  
    System.out.format("%s: %s%n", threadName, message);  
  }  
  
  private static class MessageLoop implements Runnable {  
  
    @Override  
    public void run() {  
      String[] importantInfo = {  
              "Mares eat oats",  
              "Does eat oats",  
              "Little lambs eat ivy",  
              "A kid will eat ivy too"  
      };  
  
      try {  
        for (String s : importantInfo) {  
          // Pause for 4 seconds  
          Thread.sleep(4000);  
          // Print a message  
          threadMessage(s);  
        }  
      } catch (InterruptedException e) {  
        threadMessage("I wasn't done!");  
      }  
    }  
  }  
  
  public static void main(String[] args) throws InterruptedException {  
    // Delay, in milliseconds before  
    // we interrupt MessageLoop    // thread (default one hour).    long patience = 1000 * 60 * 60;  
  
    // If command line argument  
    // present, gives patience    // in seconds.    if (args.length > 0) {  
      try {  
        patience = Long.parseLong(args[0]) * 1000;  
      } catch (NumberFormatException e) {  
        System.err.println("Argument must be an integer.");  
        System.exit(1);  
      }  
    }  
  
    threadMessage("Starting MessageLoop thread");  
    long startTime = System.currentTimeMillis();  
    Thread t = new Thread(new MessageLoop());  
    t.start();  
  
    threadMessage("Waiting for MessageLoop thread to finish");  
    while (t.isAlive()) {  
      threadMessage("Still waiting...");  
      // Wait maximum of 1 second for MessageLoop thread to finish.  
      t.join(1000);  
      if (((System.currentTimeMillis() - startTime) > patience) && t.isAlive()) {  
        threadMessage("Tired of waiting!");  
        t.interrupt();  
        // Shouldn't be long now  
        // -- wait indefinitely        t.join();  
      }  
    }  
    threadMessage("Finally!");  
  }  
}