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

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

本节书摘来异步社区《Java线程与并发编程实践》一书中的第1章,第1.2节,作者: 【美】Jeff Friesen,更多章节内容可以访问云栖社区“异步社区”公众号查看。

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 = 4*arctan(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万行。

相关文章
|
21小时前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第13天】 在Java开发中,并发编程是一个复杂且重要的领域。它不仅关系到程序的线程安全性,也直接影响到系统的性能表现。本文将探讨Java并发编程的核心概念,包括线程同步机制、锁优化技术以及如何平衡线程安全和性能。通过分析具体案例,我们将提供实用的编程技巧和最佳实践,帮助开发者在确保线程安全的同时,提升应用性能。
8 1
|
21小时前
|
Java 编译器 开发者
Java并发编程中的锁优化策略
【5月更文挑战第13天】在Java并发编程中,锁是一种重要的同步机制,用于保证多线程环境下数据的一致性。然而,不当的使用锁可能会导致性能下降,甚至产生死锁等问题。本文将介绍Java中锁的优化策略,包括锁粗化、锁消除、锁降级等,帮助开发者提高程序的性能。
|
1天前
|
安全 调度 Python
探索Python中的并发编程:协程与多线程的比较
本文将深入探讨Python中的并发编程技术,重点比较协程与多线程的特点和应用场景。通过对协程和多线程的原理解析,以及在实际项目中的应用案例分析,读者将能够更好地理解两种并发编程模型的异同,并在实践中选择合适的方案来提升Python程序的性能和效率。
|
1天前
|
安全 Java 数据安全/隐私保护
Java一分钟之-Java反射机制:动态操作类与对象
【5月更文挑战第12天】本文介绍了Java反射机制的基本用法,包括获取Class对象、创建对象、访问字段和调用方法。同时,讨论了常见的问题和易错点,如忽略访问权限检查、未捕获异常以及性能损耗,并提供了相应的避免策略。理解反射的工作原理和合理使用有助于提升代码灵活性,但需注意其带来的安全风险和性能影响。
15 4
|
1天前
|
Java 调度
Java一分钟之线程池:ExecutorService与Future
【5月更文挑战第12天】Java并发编程中,`ExecutorService`和`Future`是关键组件,简化多线程并提供异步执行能力。`ExecutorService`是线程池接口,用于提交任务到线程池,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。通过`submit()`提交任务并返回`Future`对象,可检查任务状态、获取结果或取消任务。注意处理`ExecutionException`和避免无限等待。实战示例展示了如何异步执行任务并获取结果。理解这些概念对提升并发性能至关重要。
15 5
|
1天前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第12天】 在现代软件开发中,多线程编程是提升应用程序性能和响应能力的关键手段之一。特别是在Java语言中,由于其内置的跨平台线程支持,开发者可以轻松地创建和管理线程。然而,随之而来的并发问题也不容小觑。本文将探讨Java并发编程的核心概念,包括线程安全策略、锁机制以及性能优化技巧。通过实例分析与性能比较,我们旨在为读者提供一套既确保线程安全又兼顾性能的编程指导。
|
2天前
|
Java
Java一分钟:线程协作:wait(), notify(), notifyAll()
【5月更文挑战第11天】本文介绍了Java多线程编程中的`wait()`, `notify()`, `notifyAll()`方法,它们用于线程间通信和同步。这些方法在`synchronized`代码块中使用,控制线程执行和资源访问。文章讨论了常见问题,如死锁、未捕获异常、同步使用错误及通知错误,并提供了生产者-消费者模型的示例代码,强调理解并正确使用这些方法对实现线程协作的重要性。
11 3
|
2天前
|
安全 算法 Java
Java一分钟:线程同步:synchronized关键字
【5月更文挑战第11天】Java中的`synchronized`关键字用于线程同步,防止竞态条件,确保数据一致性。本文介绍了其工作原理、常见问题及避免策略。同步方法和同步代码块是两种使用形式,需注意避免死锁、过度使用导致的性能影响以及理解锁的可重入性和升级降级机制。示例展示了同步方法和代码块的运用,以及如何避免死锁。正确使用`synchronized`是编写多线程安全代码的核心。
54 2
|
2天前
|
安全 Java 调度
Java一分钟:多线程编程初步:Thread类与Runnable接口
【5月更文挑战第11天】本文介绍了Java中创建线程的两种方式:继承Thread类和实现Runnable接口,并讨论了多线程编程中的常见问题,如资源浪费、线程安全、死锁和优先级问题,提出了解决策略。示例展示了线程通信的生产者-消费者模型,强调理解和掌握线程操作对编写高效并发程序的重要性。
41 3
|
2天前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第11天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个方面,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。我们将通过实例和代码片段来说明这些概念和技术。
3 0