《Java线程与并发编程实践》—— 1.2 操作更高级的线程任务

1.2 操作更高级的线程任务

之前的线程任务都和如何配置一个线程对象以及启动关联的线程相关。不过,Thread类也能支持更多高级的任务,包括中断其他线程、将线程join到另一条线程中以及致使线程睡眠。

1.2.1 中断线程

Thread类提供了一种线程可以中断其他线程的机制。当一个线程被中断时,它会抛出java.lang.InterruptedException。这一机制由下面的3种方法构成。

void interrupt():中断调用此方法的Thread对象所关联的线程。当一条线程由于调用了Thread的sleep()或者join()方法(这一章后面会讨论到)而被阻塞住时,该线程的中断状态就会被清除,同时抛出InterruptedException。否则,除了会设置中断状态,其他动作也会取决于当前线程的行为相应地发生(详情参见JDK文档)。
static boolean interrupted():验证当前线程是否已经中断,在这个例子中会返回true。该线程的中断状态会被这个方法清除掉。
boolean isInterrupted():验证线程是否已经中断,这个例子中会返回true。该线程的中断状态不受此方法的影响。
我创建了一个应用程序来演示线程中断,见清单1-2。

清单1-2 线程中断示例

public class ThreadDemo
{
   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         String name = Thread.currentThread().getName();
                         int count = 0;
                         while (!Thread.interrupted())
                            System.out.println(name + ": " + count++);
                      }
                   };
      Thread thdA = new Thread(r);
      Thread thdB = new Thread(r);
      thdA.start();
      thdB.start();
      while (true)
      {
         double n = Math.random();
         if (n >= 0.49999999 && n <= 0.50000001)
            break;
      }
      thdA.interrupt();
      thdB.interrupt();
   }
}

默认的主线程首先创建runnable对象,用于获取当前线程的名称。这个runnable对象随后声明了一个计数变量并且进入到while循环中,重复打印线程名称和计数变量的值,同时不断递增计数变量的值,直到该线程被中断。

接下来,默认的主线程创建了一对Thread对象,它们执行runnable并启动这些后台线程。

为了给这些后台线程一些时间以便在中断之前打印几条消息,默认主线程进入了一个基于while的忙循环,该循环语句就是拿来消耗一些时间的。这个循环会重复地获取随机数直到数字落入一段狭窄的区间内。

注意:
 

因为会浪费处理器时间,忙循环不是一个好主意。在本章后面我会展示一个更好的解决方案。
while循环终止之后,默认的主线程在每个后台的线程对象上执行interrupt()方法。每个后台线程在下一次执行Thread.interrupted()时,会返回true``并且同时终止循环。

编译清单1-2(javac ThreadDemo.java),并运行最终程序(java ThreadDemo)。你应该能看到包含递增计数变量的消息在Thread-0和Thread-1之间交替。示例如下:

Thread-1: 67
Thread-1: 68
Thread-0: 768
Thread-1: 69
Thread-0: 769
Thread-0: 770
Thread-1: 70
Thread-0: 771
Thread-0: 772
Thread-1: 71
Thread-0: 773
Thread-1: 72
Thread-0: 774
Thread-1: 73
Thread-0: 775
Thread-0: 776
Thread-0: 777
Thread-0: 778
Thread-1: 74
Thread-0: 779
Thread-1: 75

1.2.2 等待线程

线程(如默认的主线程)会偶尔启动另一个线程去操作单调的计算、下载大文件或者操作一些其他的耗时任务。在结束它自己的任务之后,这个启动工作线程的线程就准备着处理工作线程的结果,同时等待该工作线程“寿终正寝”。

Thread类提供了3种join()方法,允许调用线程等待执行此方法的线程对象所关联的线程执行完毕。

  • void join():无限期地等待直至该线程死亡。当任意线程中断当前线程的时候,InterruptedException就会抛出。如果该异常被抛出,该线程的中断状态就会被清除。
  • void join(long millis):该线程死亡之前最多等待millis毫秒。如果传递0作为参数就会无限期地等待——``join()其实就调用了join(0)方法。如果millis是负数,那么就会导致IllegalArgument Exception被抛出。当任意线程中断了当前线程,就会导致InterruptedException被抛出,如果该异常被抛出,该线程的中断状态会被清除。
  • void join(long millis, int nanos):该线程死亡之前最多等待millis毫秒加nanos纳秒。当millis是负数、nanos是负数或者nanos大于999999的时候,会导致IllegalArgumentException被抛出。当任意线程中断了当前线程,就会导致InterruptedException被抛出,如果该异常被抛出,该线程的中断状态会被清除。
    为了演示不含参数的join()方法,我创建了一个应用程序来计算数学中的常量pi到小数点后50000位。它是根据17世纪早期的一位英国数学家John Machin发明的算法来计算的。这一算法首先计算pi/4 = 4 × arctan(1/5)−arctan(1/239),然后把结果乘以4得到pi的值。因为反正切函数使用了幂级的条件计算,条件越多,pi的值会越精确(从到小数点后多少位这方面来看)。清单1-3展示了源代码。

清单1-3 演示Thread Joining

import java.math.BigDecimal;

public class ThreadDemo
{
   // constant used in pi computation

   private static final BigDecimal FOUR = BigDecimal.valueOf(4);

   // rounding mode to use during pi computation

   private static final int roundingMode = BigDecimal.ROUND_HALF_EVEN;

   private static BigDecimal result;

   public static void main(String[] args)
   {
       Runnable r = () ->
                    {
                        result = computePi(50000);
                    };
       Thread t = new Thread(r);
       t.start();
       try
       {
            t.join();
       }
       catch (InterruptedException ie)
       {
           // Should never arrive here because interrupt() is never
           // called.
       }
       System.out.println(result);
    }
    /*
     * Compute the value of pi to the specified number of digits after the
     * decimal point. The value is computed using Machin's formula:
     *
      pi/4 = 4arctan(1/5)-arctan(1/239)
     *
     * and a power series expansion of arctan(x) to sufficient precision.
     */
    public static BigDecimal computePi(int digits)
    {
      int scale = digits + 5;
      BigDecimal arctan1_5 = arctan(5, scale);
      BigDecimal arctan1_239 = arctan(239, scale);
      BigDecimal pi = arctan1_5.multiply(FOUR).
                      subtract(arctan1_239).multiply(FOUR);
      return pi.setScale(digits, BigDecimal.ROUND_HALF_UP);
     }
    /*
     * Compute the value, in radians, of the arctangent of the inverse of
     * the supplied integer to the specified number of digits after the
     * decimal point. The value is computed using the power series
     * expansion for the arc tangent:
     *
     * arctan(x) = x-(x^3)/3+(x^5)/5-(x^7)/7+(x^9)/9 ...
     */

    public static BigDecimal arctan(int inverseX, int scale)
    {
       BigDecimal result, numer, term;
       BigDecimal invX = BigDecimal.valueOf(inverseX);
       BigDecimal invX2 = BigDecimal.valueOf(inverseX * inverseX);
       numer = BigDecimal.ONE.divide(invX, scale, roundingMode);
       result = numer;
       int i = 1;
       do
       {
           numer = numer.divide(invX2, scale, roundingMode);
           int denom = 2 * i + 1;
           term = numer.divide(BigDecimal.valueOf(denom), scale,
                               roundingMode);
           if ((i % 2) != 0)
                result = result.subtract(term);
           else
                result = result.add(term);
           i++;
       }
       while (term.compareTo(BigDecimal.ZERO) != 0);
       return result;
   }
}

默认的主线程首先创建了一个runnable去计算pi到小数点后50000位,然后把结果赋值给名为result的java.math.BigDecimal对象。为了代码简洁,这里使用了lambda表达式。

这个线程随后创建了一个Thread对象去执行runnable并启动了一个工作线程来执行操作。

这里,默认的主线程在该Thread对象上调用了join()方法等待工作线程死亡。当工作线程死亡了,默认主线程会打印出BigDecimal对象的值。

编译清单1-3的代码(javac ThreadDemo.java)并运行最终程序(java ThreadDemo)。我观察到的前段部分输出如下:

3.1415926535897932384626433832795028841971693993751058209749445923078164062
862089986280348253421170679821480865132823066470938446095505822317253594081
284811174502841027019385211055596446229489549303819644288109756659334461284
756482337867831652712019091456485669234603486104543266482133936072602491412
737245870066063155881748815209209628292540917153643678925903600113305305488
204665213841469519415116094330572703657595919530921861173819326117931051185
4807446237996274956735188575272489122793818301194912983367336244065664308
6021394946395224737190702179860943702770539217176293176752384674818467669
405132000568127

1.2.3 线程睡眠

Thread类声明了一对静态方法致使线程睡眠(暂时性地停止执行)。

void sleep(long millis):睡眠millis毫秒数。线程睡眠的实际的毫秒数取决于系统定时器和调度器的精度。如果millis是负数,那么就会导致IllegalArgumentException+被抛出。当任意线程中断了当前线程,就会导致javascript InterruptedException被 抛出,如果该异常被抛出,该线程的中断状态会被清除。
void sleep(long millis, int nanos):睡眠millis``毫秒数和nanos纳秒数。实际睡眠的毫秒数和纳秒数取决于系统定时器和调度器的精度。当millis是负数,nanos是负数或者nanos大于999999的时候,会导致IllegalArgumentException被抛出。当任意线程中断了当前线程,就会导致InterruptedException被抛出,如果该异常被抛出,该线程的中断状态会被清除。
sleep()方法相较于忙循环更好,因为它们不会浪费处理器周期。

我已经重构了清单1-2的应用程序来展示线程睡眠。请看清单1-4。

清单1-4 线程睡眠示例

public class ThreadDemo
{
    public static void main(String[] args)
    {
        Runnable r = new Runnable()
                     {
                        @Override
                        public void run()
                        {
                           String name = Thread.currentThread().getName();
                            int count = 0;
                            while (!Thread.interrupted())
                               System.out.println(name + ":" + count++);
                        }
                     };
       Thread thdA = new Thread(r);
       Thread thdB = new Thread(r);
       thdA.start();
       thdB.start();
       try
       {
           Thread.sleep(2000);
       }
       catch (InterruptedException ie)
       {
       }
       thdA.interrupt();
       thdB.interrupt();
    }
}

清单1-2和清单1-4唯一的不同之处就是使用Thread.sleep(2000)替代了忙循环,睡眠了2秒。

编译清单1-4 (javac ThreadDemo.java),运行最终程序(java ThreadDemo)。由于睡眠时间是大概的时间,所以在多次运行中打印出的行数会有差异。但是,这种差异不会特别大。举个例子,你不会在某次运行看到10行,而在另外一次运行中看到1000万行。

时间: 2025-01-21 03:03:50

《Java线程与并发编程实践》—— 1.2 操作更高级的线程任务的相关文章

《Java线程与并发编程实践》—— 导读

前言 Java线程与并发编程实践 线程和并发工具并非尤物,但是它们是正式应用的重要部分.本书会向你介绍Java 8 Update 60中线程特性以及并发工具的大部分内容. 第1章介绍了类Thread和接口Runnable.你会学习如何创建Thread以及Runnable对象,获取和设置线程状态.启动线程.中断线程,将一条线程插入另外一条线程以及触发线程睡眠. 第2章关注同步.学习后你会解决一些问题,如没有同步就无法解决的竞态条件.你也能学到如何创建同步方法.块,以及如何使用忽略互斥访问的轻量级同

《Java线程与并发编程实践》—— 第2章 同步 2.1 线程中的问题

第2章 同步 Java线程与并发编程实践 线程交互通常是通过共享变量完成的,当线程之间没有交互时,开发多线程的应用程序会变得简单许多.一旦发生了交互,很多诱发线程不安全(在多线程环境下不正确)的因素就会暴露出来.在这一章中,你将会认识到这些问题,同时也会学习如何正确地使用Java面向同步的特性来克服它们. 2.1 线程中的问题 Java对线程的支持促进了响应式.可扩展应用程序的发展.不过,这样的支持是以增加复杂性作为代价的.如果不多加小心,你的代码就会到处充斥着极难以察觉的bug,而这些bug多

《Java线程与并发编程实践》- 第1章 Thread和Runnable

第1章 Thread和Runnable Java线程与并发编程实践 Java程序是通过线程执行的,线程在程序中具有独立的执行路径.当多条线程执行时,它们彼此之间的路径可以不同.举个例子,一条线程可能在执行switch语句的某个case分支,另一条线程很可能在执行其他case分支. 每个Java应用程序都有一个执行main()函数的默认主线程.应用程序也可以创建线程在后台操作时间密集型任务,以确保对用户的响应.这些封装了代码执行序列的线程对象就被称为runnable. Java虚拟机给每条线程分配

《Java线程与并发编程实践》—— 2.2 同步临界区的访问

2.2 同步临界区的访问 你会看到,同步能够解决之前的线程问题.同步是JVM的一个特性,旨在保证两个或者多个并发的线程不会同时执行同一块临界区,临界区就是必须以串行方式(一次一条线程)访问的一段代码块. 因为其他线程在临界区中的时候每条线程对该临界区的访问都会互斥地执行,这种同步属性就称为互斥.由于这个原因,线程获取到的锁经常称为互斥锁. 同步也表现出可见性,该属性能够保证一条线程在临界区执行的时候总是能看到共享变量最近的修改.当进入临界区时,它从主存中读入这些变量,离开时把这些变量的值写入主存

《Java线程与并发编程实践》—— 2.3 谨防活跃性问题

2.3 谨防活跃性问题 活跃性这个词代表着某件正确的事情最终会发生.活跃性失败发生在应用程序触及一种无法继续执行的状态.在单线程的应用程序中,无限循环就是一个例子.多线程应用程序面临着诸如死锁.活锁和饿死的额外挑战. 死锁:线程1等待线程2互斥持有的资源,而线程2也在等待线程1互斥持有的资源.两条线程都无法继续执行. 活锁:线程x持续重试一个总是失败的操作,以致于无法继续执行. 饿死:线程x一直被(调度器)延迟访问其赖以执行的资源.或许是调度器先于低优先级的线程执行高优先级的线程,而总是有一个高

《Java线程与并发编程实践》—— 1.4 小结

1.4 小结 Java应用程序通过线程执行,线程在程序代码中具有独立的执行路径.每个Java应用程序都有一个执行main()函数的默认主线程.应用程序也可以创建线程在后台操作时间密集型任务以确保对用户的响应.这些封装了代码执行序列的线程对象就称为runnables. Thread类为底层操作系统的线程体系架构提供一套统一接口(操作系统通常负责创建和管理线程).单个的操作系统线程关联为一个Thread对象. Runnable接口为关联Thread对象的线程提供执行代码.这些代码放在Runnable

《Java线程与并发编程实践》—— 1.3 练习

1.3 练习 接下来的练习旨在测试你对第1章内容的掌握程度. 1.给出线程的定义. 2.给出runnable的定义. 3.Thread类和Runnable接口完成了什么? 4.指出创建一个Runnable对象的两种方式. 5.指出关联一个runnable到一个Thread对象的两种方式. 6.指出5种Thread的状态. 7.判断对错:默认线程的名字会以Thd-作为前缀. 8.如何给线程赋予非默认的名称? 9.如何确定线程是死是活? 10.指出Thread.State枚举的所有常量. 11.如何

《Java线程与并发编程实践》—— 2.6 小结

2.6 小结 线程交互通常是通过共享变量完成的,当线程之间没有交互,开发多线程的应用程序会变得简单许多.一旦发生了交互,竞态条件.数据竞争以及缓存变量等诱发线程不安全(在多线程环境下不正确)的因素就会暴露出来. 你可以使用同步解决之前的线程问题.同步是JVM的一个特性,旨在保证两条或者两条以上并发的线程不会同时进入同一块临界区.临界区就是必须以串行方式访问的一段代码块. 活跃性这个词代表着某件正确的事情最终会发生.活跃性失败发生在应用程序触及一种无法继续执行的状态.在单线程的应用程序中,无限循环

《Java线程与并发编程实践》—— 2.5 练习

2.5 练习 下面的练习用来测试你对第2章内容的掌握程度. 1.指出与线程相关的三个问题. 2.判断对错:当程序计算的正确性取决于相对时间或者调度器所控制的多线程交叉时,你会遇到数据竞争问题. 3.给出同步的定义. 4.指出同步的两种属性. 5.同步是如何实现出来的? 6.判断对错:当一个已经获取锁的线程调用任意Thread的任意sleep()方法时不会释放锁. 7.如何指定一个同步方法? 8.如何指定一个同步块? 9.给出活跃性的定义. 10.指出三种活跃性挑战. 11.如何区分volatil