@lambeta
2016-08-17T01:01:20.000000Z
字数 13869
阅读 356
translation
Java应用程序通过线程执行,线程在程序代码中具有独立的执行路径。当多条线程执行的时候,每条线程走的路径可以不同。举个例子,一条线程可能在执行switch语句的一个case分支,另一条线程很可能在执行其它case分支。
每个Java应用程序都有一个执行main()函数的默认主线程。应用程序也可以创建线程在后台操作时间密集型任务以确保对用户的响应。这些封装了代码执行序列的线程对象就被称为runnables。
Java虚拟机给每条线程分配独立的JVM栈空间以免彼此干扰。独立的栈使得线程可以追踪它们自己下条要执行的指令,这些指令会依线程不同而有所区别。栈空间也为每条线程单独准备了一份方法参数,局部变量以及返回值的拷贝。
Java主要是通过java.lang.Thread 类以及java.lang.Runnable接口来支持线程的。本章将介绍这些类型。
Thread类为底层操作系统的线程体系架构提供一套统一接口。(操作系统通常负责创建和管理线程。)单个的操作系统线程对应关联一个Thread对象。
Runnable接口为关联Thread对象的线程提供执行代码。这些代码被放在Runnable的void run()方法中,尽管它不接收任何参数且没有返回值,但有可能抛出异常,这点我们会在第4章讨论。
除了默认的主线程,线程都是通过创建合适的Thread和Runnable对象介入应用程序的。Thread类声明了几个构造方法来初始化Thread对象。其中有几个需要接收Runnable对象作为参数。
我们有两种方式创建Runnable对象。第一种方式是创建一个实现了Runnable接口的匿名类,如下:
Runnable r = new Runnable(){@Overridepublic void run(){// perform some workSystem.out.println("Hello from thread");}};
在Java 8之前,这是唯一一种创建runnable的方式。不过,Java 8引入lambda表达式更为快捷地创建runnable:
Runnable r = () -> System.out.println("Hello from thread");
lambda确实比匿名类更简洁。我会在本章及随后的章节继续使用这些语言特性。
注意 一个lambda表达式(lambda)是一个被传递到构造函数或者普通方法以供后续执行的匿名函数。和Runnable类似,lambda表达式以functional interfaces(声明单个抽象方法的接口)的形式工作。
创建Runnable对象之后,你可以把它传递到Thread类接收Runnable作为参数的构造函数中。举个例子,Thread(Runnable runnable)方法由特定的runnable初始化了一个新的Thread对象。下面的代码片段示范了这一做法:
Thread t = new Thread(r);
少数构造函数不会接收Runnable作为参数。比如,Thread()构造方法就不会接收它来初始化线程。你必须继承Thread类继而重写它的run()方法(Thread类实现了Runnable接口)并提供运行代码。如下面的代码片段所示:
{@Overridepublic void run(){// perform some workSystem.out.println("Hello from thread");}}// ...MyThread mt = new MyThread();
一个Thread对象关联着一条线程的状态。这个状态是由线程名称、线程存活的标识、线程的执行状态(是否正在执行?)、线程的优先级以及线程是否为守护线程等标识构成。
每个Thread对象都会被赋予一个名称,这样有利于调试。这个名称如果不是显式指定的,那么默认会以一个Thread-作为前缀。你可以通过调用Thread的String getName()方法来获取这个名称。若要设置名称,则得把名称传递给一个合适的构造函数,比如Thread(Runnable r, String name),或者调用Thread的void setName(String name)方法。如下面的代码片段所示:
Thread t1 = new Thread(r, "thread t1");System.out.println(t1.getName()); // Output: thread t1Thread t2 = new Thread(r);t2.setName("thread t2");System.out.println(t2.getName()); // Output: thread t2
注意 Thread的long getId()方法会返回一个唯一的基于长整型的名称。这个数字在线程的生命周期内不会改变。
你可通过调用Thread的boolean isAlive()方法判断一条线程是死是活。当线程是活的,该方法返回true,反之,返回false。一条线程的寿命仅仅起始于它真正在start()方法(后面会讨论)中被启动起来,而结束于它刚刚离开run()方法,此时线程死亡。下面的代码片段打印了一条新创建线程的存活状态:
Thread t = new Thread(r);System.out.println(t.isAlive()); // Output: false
线程的执行状态被Thread.State枚举常量所标识:
Thread通过提供Thread.State getState()方法得以让应用程序判断线程的当前状态。示例如下:
Thread t = new Thread(r);System.out.println(t.getState()); // Output: NEW
当计算机有足够的处理器或处理内核,操作系统就会为每个处理器或核心分配单独的线程,这些线程可以同时执行。一旦计算机没有足够的处理器或核心的时候,多条线程只能轮转着使用共享的处理器和核心了。
注意 你可以调用java.lang.Runtime类的int availableProcessors()方法确定JVM上可用的处理器或处理器核心的数量。方法的返回值可能会在JVM执行时发生变化但是不会小于1。
操作系统使用调度器(http://en.wikipedia.org/wiki/Scheduling_(computing)) 来决定什么时候一个等待线程得以执行。下面的列表展示了三种不同的调度器:
多级反馈队列调度器和很多其它线程调度器都会考虑优先级(线程的相对重要性),通常会结合抢占式调度(高优先级线程抢占、中断并取代低优先级线程运行)和轮转时间片调度(同等优先级的线程享有同等的时间片段,也被称为时间片,然后依次执行)。
注意 当探究线程究竟是并行还是并发的时候,通常会遇到两个概念。根据Oracle的《多线程指南》(http://docs.oracle.com/cd/E19455-01/806-5257/6je9h032b/index.html),并行是“一种发生在至少有两个线程同时执行的场景”,相反地,并发是”一种存在于至少有两个线程前进的场景,它是一种更泛化的并行,包括基于时间片的虚拟并行模式”。
Thread通过int getPriority()方法返回当前的优先级以及void setPriority(int priority)方法设置优先级支持优先级操作。传递给优先级的值介于Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间,而Thread.NORMAL_PRIORITY则确定了默认的优先级。请看下面的代码片段:
Thread t = new Thread(r);System.out.println(t.getPriority());t.setPriority(Thread.MIN_PRIORITY);
留心 使用setPriority()会影响应用程序跨操作系统的可移植性,因为不同的调度器会采取不同的方式处理优先级。举个例子,一个操作系统的调度器可能会推迟低优先级线程执行直到高优先级线程完成执行。这一延迟会导致无限延迟,而当无限期地等待执行时,低优先级线程会“饿死”,这会严重损害应用程序的性能。而其它操作系统的调度器可能不会无限期地延迟低优先级的线程,从而改善应用程序的性能。
Java将线程分为守护和非守护线程。一条守护线程扮演非守护线程辅助者的角色并且会在应用程序最后一条非守护线程消失之后自动死亡,因此应用程序才能终止。
你可以通过调用Thread的boolean isDaemon()方法来判断线程是守护还是非守护线程,这个方法会针对一个守护线程返回true。
Thread t = new Thread(r);System.out.println(t.isDaemon()); // Output: false
默认情况下,和Thread对象关联的线程都是非守护线程。想要创建一个守护线程,你必须调用Thread的void setDaemon(boolean isDaemon)方法并传入true作为参数。示例如下:
Thread t = new Thread(r);t.setDaemon(true);
注意 当非守护默认主线程终止后,应用程序还会等到所有后台的非守护线程终止之后才会终止。如果后台的线程本来就是守护线程,那么当默认的主线程终止时,应用程序会立马终止。
在创建一个Thread对象或者其子类的对象之后,你可以通过调用Thread的void start()方法启动与该对象关联的线程。如果线程之前已经启动且处于运行状态,又或者线程已经死亡,这个方法就会抛出java.lang.IllegalThreadStateException:
Thread t = new Thread(r);t.start();
调用start()方法会在运行时创建底层线程,同时调度run()方法中的指令。(start()方法并不会等到所有这些任务都完成才返回。)当run()方法执行完毕,线程就会被销毁,调用start()方法的Thread对像不再可用,这也是start()方法会导致IllegalThreadStateException的原因。
我已经创建了一个应用,它包含了从线程创建到启动的多种基本示例。查看清单1-1.
清单1-1. 线程基础示例
public class ThreadDemo{public static void main(String[] args){boolean isDaemon = args.length != 0;Runnable r = new Runnable(){@Overridepublic void run(){Thread thd = Thread.currentThread();while (true)System.out.printf("%s is %salive and in %s " +"state%n",thd.getName(),thd.isAlive() ? "" : "not ",thd.getState());}};Thread t1 = new Thread(r, "thd1");if (isDaemon)t1.setDaemon(true);System.out.printf("%s is %salive and in %s state%n",t1.getName(),t1.isAlive() ? "" : "not ",t1.getState());Thread t2 = new Thread(r);t2.setName("thd2");if (isDaemon)t2.setDaemon(true);System.out.printf("%s is %salive and in %s state%n",t2.getName(),t2.isAlive() ? "" : "not ",t2.getState());t1.start();t2.start();}}
首先,默认的主线程基于命令行参数是否存在来初始化isDaemon变量。只要有一个参数传递过来,isDaemon就会被设置成true。否则,设置为false。
接下来,一个runnable对象被创建了。这个对象首先调用Thread的静态方法Thread currentThread()获得当前执行线程关联Thread对象的引用。该引用继而会被用于获取该线程的相关信息,这些信息最终会被打印出来。
到这里,初始化成上面的runnable且名为thd1的Thread对象就被创建好了。如果isDaemon是true,那么这个线程对象就会被标记成守护线程,它的名字、存活状态以及执行状态随后会被打印。
第二个初始化成runnable且名为thd2的线程对象也被创建好了。与之前相同,如果isDaemon是true,该线程对象会被标记成守护线程,它的名字、存活状态以及执行状态也会被打印出来。
最终,两条线程都被启动起来。
javac ThreadDemo.java
运行结果程序如下
java ThreadDemo
我在64位的Windows7操作系统上观测到如下不断打印的开始几条结果:
thd1 is not alive and in NEW statethd2 is not alive and in NEW statethd1 is alive and in RUNNABLE statethd2 is alive and in RUNNABLE state
你很有可能在你的操作系统看到不同的输出结果。
小贴士 为了终止无限运行的应用程序,可以在Windows或者非Windows系统上同时按住Ctrl和C键。
现在,像下面这样运行结果程序:
java ThreadDemo x
和前面所有非守护线程执行的结果不同,命令行参数的存在导致所有的线程都会作为守护线程执行。因此,这些线程执行到默认主线程终止。你应该能观测到更简略的打印结果。
之前的线程任务都和如何配置一个线程对象以及启动关联的线程相关。不过,Thread类也能支持更多高级的任务,包括中断其它线程,将线程join到另一条线程中以及致使线程睡眠。
Thread类提供了一种线程可以中断其它线程的机制。当一个线程被中断时,它会抛出java.lang.InterruptedException。这一机制由下面的三种方法构成。
我创建了一个应用程序来演示线程中断。请见清单1-2。 清单1-2. 线程中断示例
public class ThreadDemo{public static void main(String[] args){Runnable r = new Runnable(){@Overridepublic 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: 67Thread-1: 68Thread-0: 768Thread-1: 69Thread-0: 769Thread-0: 770Thread-1: 70Thread-0: 771Thread-0: 772Thread-1: 71Thread-0: 773Thread-1: 72Thread-0: 774Thread-1: 73Thread-0: 775Thread-0: 776Thread-0: 777Thread-0: 778Thread-1: 74Thread-0: 779Thread-1: 75
线程(如默认的主线程)会偶尔启动另一个线程去操作单调的计算、下载大文件或者操作一些其它的耗时任务。在结束它自己的任务之后,这个启动工作线程的线程就准备着处理工作线程的结果,同时等待该工作线程寿终正寝。
Thread类提供了三种join()方法允许调用线程等待执行此方法的线程对象所关联的线程执行完毕:
为了演示不含参数的join()方法,我创建了一个应用程序来计算数学中的常量pi到小数点后50000位。它是借由17世纪早期的一位英国数学家John Machin(https://en.wikipedia.org/ wiki/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 computationprivate static final BigDecimal FOUR = BigDecimal.valueOf(4);// rounding mode to use during pi computationprivate static final int roundingMode = BigDecimal.ROUND_HALF_EVEN;private static BigDecimal result;{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);elseresult = 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.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117067982148086513282306647093844609550582231725359408128481117450284102701938521105559644622948954930381964428810975665933446128475648233786783165271201909145648566923460348610454326648213393607260249141273724587006606315588174881520920962829254091715364367892590360011330530548820466521384146951941511609433057270365759591953092186117381932611793105118548074462379962749567351885752724891227938183011949129833673362440656643086021394946395224737190702179860943702770539217176293176752384674818467669405132000568127
Thread类声明了一对静态方法致使线程睡眠(暂时性地停止执行):
sleep()方法相较于忙循环更优,因为它们不会浪费处理器周期。
我已经重构了清单1-2的应用程序来展示线程睡眠。请看清单1-4。
public class ThreadDemo{public static void main(String[] args){Runnable r = new Runnable(){@Overridepublic 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万行。
接下来的练习旨在测试你对第一章内容的掌握程度:
1. 定义线程。
2. 定义runnable。
3. Thread类和Runnable接口完成了什么?
4. 明确创建一个Runnable对象的两种方式。
5. 明确关联一个runnable到一个Thread对象的两种方式。
6. 明确五种Thread的状态。
7. 判断对错:默认线程的名字会以Thd-作为前缀。
8. 如何给线程赋予非默认的名称?
9. 如何确定线程是死是活?
10. 确认Thread.State枚举的所有常量。
11. 如何获取当前线程的执行状态?
12. 定义优先级。
13. 如何通过setPriority()来影响应用程序跨操作系统的可移植性?
14. 确定Thread的void setPriority(int priority)方法参数的取值范围。
15. 判断对错:当应用程序的最后一个非守护线程死亡之后,守护线程也会自动死亡使得应用程序退出。
16. 在一个正在运行或者已经死亡的Thread 对象上调用Thread的void start()方法会发生什么?
17. 如何在Windows上停止一个无法终止的程序?
18. 确定组成Thread中断机制的所有方法。
19. 判断对错:boolean isInterrupted()方法清除了线程的中断状态。
20. 当线程中断了,该线程会如何反映?
21. 定义一个忙循环。
22. 确定让一条线程等待另一个线程直至死亡的Thread方法。
23. 确定让一条线程睡眠的Thread方法。
24. 编写一个名为IntSleep的应用程序,其创建一条后台线程,不断地打印出Hello,之后睡眠100毫秒。在睡眠了2秒之后,默认主线程应该中断后台线程,此线程在打印出interrupted之后跳出循环。
Java应用程序通过线程执行,线程在程序代码中具有独立的执行路径。每个Java应用程序都有一个执行main()函数的默认主线程。应用程序也可以创建线程在后台操作时间密集型任务以确保对用户的响应。这些封装了代码执行序列的线程对象就被称为runnables。
Thread类为底层操作系统的线程体系架构提供一套统一接口。(操作系统通常负责创建和管理线程。)单个的操作系统线程对应关联一个Thread对象。
Runnable接口为关联Thread对象的线程提供执行代码。这些代码被放在Runnable的void run()方法中,尽管它不接收任何参数且没有返回值,但有可能抛出异常。
除了默认的主线程,线程都是通过创建合适的Thread和Runnable对象介入应用程序的。Thread类声明了几个构造方法来初始化Thread对象。其中有几个需要接收Runnable对象作为参数。
一个Thread对象关联着一条线程的状态。这个状态是由线程名称、线程存活的标识、线程的执行状态(是否正在执行?)、线程的优先级以及线程是否为守护线程等标识构成。
在创建一个Thread对象或者其子类的对象之后,你可以通过调用Thread的void start()方法启动与该对象关联的线程。如果线程之前已经启动且处于运行状态,又或者线程已经死亡,这个方法就会抛出java.lang.IllegalThreadStateException。
之前的线程任务都和如何配置一个线程对象以及启动关联的线程相关。不过,Thread类也能支持更多高级的任务,包括中断其它线程,将线程join到另一条线程中以及致使线程睡眠。
第2章会涉及同步相关的话题。