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万行。