javaSE 基础笔记之线程
第十三章 多线程
学习目标:
² 掌握线程的基本概念
² 掌握多线程的编写
² 掌握多线程的控制
² 掌握多线程的同步
一:线程的基本概念
1:什么是线程
一个关于计算机的简化的视图是:它有一个执行计算的处理机、包含处理机所执行的程序的ROM(只读存储器)、包含程序所要操作的数据的 RAM(只读存储器)。在这个简化视图中,只能执行一个作业。 一个关于现代计算机比较完整的视图是允许计算机在同一个时间执行一个以上的作业。
你不需关心这一点是如何实现的,只需从编程的角度考虑就可以了。如果你要执行一个以上的作业,这类似有一台以上的计算机。在这个模型中,线程,被认为是带有自己的程序代码和数据的虚拟处理机的封装。 java.lang.Thread类允许用户创建并控制他们的线程。
2:线程的三个部分
进程是正在执行的程序。一个或更多的线程构成了一个进程。一个线程或执行上下文由
三个主要部分组成
- l- 一个虚拟处理机
l- CPU执行的代码 l- 代码操作的数据代码可以或不可以由多个线程共享,这和数据是独立的。两个线程如果执行同一个类的实例代码,则它们可以共享相同的代码。
类似地,数据可以或不可以由多个线程共享,这和代码是独立的。两个线程如果共享对一个公共对象的存取,则它们可以共享相同的数据。
在Java编程中,虚拟处理机封装在 Thread类的一个实例里。构造线程时,定义其上下文的代码和数据是由传递给它的构造函数的对象指定的。
3:Java中的线程的概念
几乎每种操作系统都支持进程的概念——进程就是在某种程度上相互隔离的、 独立运行的程序。
线程的三个部分
l- 处理机 l- 代码 l- 数据
线程化是允许多个活动共存于一个进程中的工具。大多数现代的操作系统都支持线程,而且线程的概念以各种形式已存在了好多年。Java 是第一个在语言本身中显式地包含线程的主流编程语言,它没有把线程化看作是底层操作系统的工具。
有时候,线程也称作轻量级进程。就象进程一样,线程在程序中是独立的、并发的执行路径,每个线程有它自己的堆栈、自己的程序计数器和自己的局部变量。但是,与分隔的进程相比,进程中的线程之间的隔离程度要小。它们共享内存、文件句柄和其它每个进程应有的状态。 进程可以支持多个线程,它们看似同时执行,但互相之间并不同步。一个进程中的多个线程共享相同的内存地址空间,这就意味着它们可以访问相同的变量和对象,而且它们从同一堆中分配对象。尽管这让线程之间共享信息变得更容易,但您必须小心,确保它们不会妨碍同一进程里的其它线程。
Java 线程工具和 API 看似简单。但是,编写有效使用线程的复杂程序并不十分容易。因为有多个线程共存在相同的内存空间中并共享相同的变量,所以您必须小心,确保您的线程不会互相干扰。 每个Java程序都至少有一个线程 — 主线程。当一个Java 程序启动时,JVM 会创建主线程,并在该线程中调用程序的 main() 方法。
JVM 还创建了其它线程,您通常都看不到它们 — 例如,与垃圾收集、对象终止和其它 JVM 内务处理任务相关的线程。其它工具也创建线程,如 AWT(抽象窗口工具箱(Abstract Windowing Toolkit))或 Swing UI 工具箱、servlet 容器、应用程序服务器和 RMI(远程方法调用(Remote Method Invocation) )。
在 Java 程序中使用线程有许多原因。如果您使用 Swing、 servlet、 RMI 或 Enterprise JavaBeans(EJB)技术,您也许没有意识到您已经在使用线程了。
使用线程的一些原因是它们可以帮助:
使 UI 响应更快
事件驱动的 UI 工具箱(如 AWT 和 Swing)有一个事件线程,它处理 UI 事件,如击键或鼠标点击。
利用多处理器系统
多处理器(MP)系统比过去更普及了。以前只能在大型数据中心和科学计算设施中才能找到它们。 现在许多低端服务器系统 — 甚至是一些台式机系统 — 都有多个处理器。
调度的基本单位通常是线程;如果某个程序只有一个活动的线程,它一次只能在一个处理器上运行。如果某个程序有多个活动线程,那么可以同时调度多个线程。在精心设计的程序中,使用多个线程可以提高程序吞吐量和性能。
简化建模
在某些情况下,使用线程可以使程序编写和维护起来更简单。考虑一个仿真应用程序,您要在其中模拟多个实体之间的交互作用。给每个实体一个自己的线程可以使许多仿真和对应用程序的建模大大简化。
服务器应用程序从远程来源(如套接字)获取输入。当读取套接字时,如果当前没有可用数据,那么对 SocketInputStream.read() 的调用将会阻塞,直到有可用数据为止。
如果单线程程序要读取套接字,而套接字另一端的实体并未发送任何数据,那么该程序只会永远等待,而不执行其它处理。相反,程序可以轮询套接字,查看是否有可用数据,但通常不会使用这种做法,因为会影响性能。
线程和进程的区别是:
每个进程都有独立的代码和数据空间(进程上下文),进程切换的开销大。 线程作为轻量的进程, 同一类线程可以共享代码和数据空间,但每个线程有独立的运行栈和程序计数器,因此线程切换的开销较小。
多进程——在操作系统中能同时运行多个任务(程序) ,也称多任务。
多线程——在同一应用程序中有多个顺序流同时执行。
4:Java编程中的线程
4.1第一个线程
下面来学习如何创建第一个线程, 以及如何使用构造函数参数来为一个线程提供运行时的数据和代码。
一个Thread类构造函数带有一个参数,它是 Runnable的一个实例。一个Runnable是由一个实现了Runnable接口(即,提供了一个public void run()方法)的类产生的。
例如:
1 public class ThreadTest { 2 3 public static void main(String args[]) { 4 5 Xyz r = new Xyz(); 6 7 Thread t = new Thread(r); 8 9 } 10 11 } 12 13 class Xyz implements Runnable { 14 15 int i; 16 17 public void run() { 18 19 while (true) { 20 21 System.out.println("Hello " + i++); 22 23 if (i == 50) break; 24 25 } 26 27 } 28 29 }
首先,main()方法构造了 Xyz 类的一个实例 r。实例 r 有它自己的数据,在这里就是整数 i。因为实例 r 是传给 Thread的类构造函数的,所以 r 的整数 i 就是线程运行时刻所操作的数据。线程总是从它所装载的Runnable实例(在本例中,这个实例就是r。)的 run()方法开始运行。
一个多线程编程环境允许创建基于同一个 Runnable 实例的多个线程。这可以通过以下方法来做到:
Thread t1= new Thread(r);
Thread t2= new Thread(r);
此时,这两个线程共享数据和代码。
总之,线程通过Thread对象的一个实例引用。线程从装入的 Runnble实例的 run()方法开始执行。线程操作的数据从传递给 Thread构造函数的 Runnable的特定实例处获得。
4.2 启动线程
一个新创建的线程并不自动开始运行。你必须调用它的 start()方法。例如,你可以发现上例中第4行代码中的命令:
t.start();
调用 start()方法使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM调度并执行。这并不意味着线程就会立即运行。
4.3 线程状态
一个Thread对象在它的生命周期中会处于各种不同的状态。下图形象地说明了这点:
线程状态及状态转换规则具体说明如下:
(1)新建状态——就绪状态:
一个新创建的线程(使用 new+Thread 构造方法创建的对象)不会自动运行,此时处于新建(New)状态。当程序员显式调用线程的 start()方法时,该线程进入就绪(Runnable)状态,也称可运行状态。进入就绪状态的线程不一定立即开始运行,因为此时计算机CPU 可能正在运行其它的线程。可能有多个线程同时进入就绪状态,在就绪队列中排队等候。
(2)就绪状态——运行状态:
Java 运行时系统提供的线程调度器按照一定的规则进行调度,一但某个线程获得执行机会,则立即进入运行(Running)状态、开始执行线程体代码。
(3)运行状态——阻塞状态:
处于运行状态的线程可能因某种事件的发生而进入阻塞(Blocked)状态、暂时停止执行。例如,线程进行I/O操作,等待用户输入数据。当一个运行状态的线程发生阻塞时,调度器立即调度就绪队列中的另一个线程开始运行。
(4)阻塞状态——就绪状态:
当处于阻塞状态的线程所等待的条件已经具备,例如用户输入操作已经完成时,该线程将解除阻塞, 进入就绪状态。注意,不是恢复执行,而是重新到就绪队列中去排队。
(5)运行状态——终止状态:
线程的run()方法正常执行完毕后,其运行也就自然结束,线程进入终止(Dead)状态。也可以在运行过程中,非正常地终止一个线程的执行,例如调用其 stop()方法。处于终止状态的线程不能在重新运行,因此不允许在一个 Thread 对象上两次调用start()方法。
二:多线程的编写和控制
1:多线程的创建
1.1:实现Runnable 接口
Java 中引入线程机制的目的在于实现多线程,以提高程序的效能。这主要是通过多线程之间共享代码和数据来实现的,例如可以使用同一个 Runnable 接口(的实现类)类型的实例构造多个线程。
示例13-1 使用多线程
程序:TestThread2java
1 public class TestThread2 { 2 3 public static void main(String args[]) { 4 5 Runner2 r = new Runner2(); 6 7 Thread t1 = new Thread(r); 8 9 Thread t2 = new Thread(r); 10 11 t1.start(); 12 13 t2.start(); 14 15 } 16 17 18 19 }1 2 3 class Runner2 implements Runnable { 4 5 public void run() { 6 7 for(int i=0; i<20; i++) { 8 9 String s = Thread.currentThread().getName(); 10 11 System.out.println(s + ": " + i); 12 13 } 14 15 } 16 17 }程序运行结果为:
程序 TestThread2.java 中创建了两个新的线程 t1 和 t2,他们共享代码——Runner2中的run()方法,同时也共享数据——Runnable 类型的对象 r,两个线程在运行过程中分别操纵对象r调用其run()方法。 从输出结果可以看出, 线程 t1 和 t2作为独立的顺序控制流,并发地交替执行。可以想象,如果先启动的线程因某种原因处于阻塞状态,例如等待用户键盘输入数据,CPU会立即转而执行其他的线程,而不必空置。
需要注意的是,这和在 main()方法中直接调用两个方法有本质的不同,那样不会出现交替的情况,必须要前面的方法执行完才会执行后面的方法。
1.2:继承 Thread
在示例13-1中,直接定义了Runnable 接口的实现类来提供线程体,这是创建线程的基本方式。除此之外,还可以采用直接继承 Thread 类、重写其中的 run()方法并以之作为线程体的方式创建线程,见示例13-2:
程序:TestThread3.java
1 public class TestThread3 { 2 3 public static void main(String args[]){ 4 5 Thread t = new Runner3(); 6 7 t.start(); 8 9 } 10 11 } 12 13 14 15 class Runner3 extends Thread { 16 17 public void run() { 18 19 for(int i=0; i<30; i++) { 20 21 System.out.println("No. " + i); 22 23 } 24 25 } 26 27 }程序TestThread3.java 与示例13-1 中的 TestThread1.java 运行结果相同。从中可以看出,
第二种创建线程方式的一般步骤为:
1. 定义一个类继承Thread 类,重写其中的run()方法;
2. 创建该Thread子类的对象;
3. 调用该对象的start()方法,启动线程。
这种情况下,线程 t 所执行的线程体就是 t 本身的成员方法 run()。实际上,Thread
类也已经实现了Runnable 接口。
1.3:两种方法的比较
给定各种方法的选择,你如何决定使用哪个?每种方法都有若干优点。
实现Runnable的优点
从面向对象的角度来看,Thread 类是一个虚拟处理机严格的封装,因此只有当处理机模型修改或扩展时,才应该继承类。正因为这个原因和区别一个正在运行的线程的处理机、代码和数据部分的意义,本教程采用了这种方法。
由于Java技术只允许单一继承,所以如果你已经继承了 Thread,你就不能再继承其它任何类,例如 Applet。在某些情况下,这会使你只能采用实现 Runnable的方法。
因为有时你必须实现 Runnable,所以你可能喜欢保持一致,并总是使用这种方法。
继承Thread的优点
当一个run()方法体现在继承 Thread类的类中,用this指向实际控制运行的Thread 实例。因此,代码不再需要使用如下控制:
Thread.currentThread().join();
而可以简单地用:
join();
因为代码简单了一些,许多 Java编程语言的程序员使用扩展 Thread 的机制。
注意:如果你采用这种方法,在你的代码生命周期的后期,单继承模型可能会给你带来困难。
2:线程调度
尽管线程变为可运行的,但它并不立即开始运行。在一个只带有一个处理机的机器上,在一个时刻只能进行一个动作。下节描述了如果有一个以上可运行线程时,如何分配处理机。
在Java中,线程是抢占式的,但并不一定是分时的 (一个常见的错误是认为“抢占式”只不过是“分时”的一种新奇的称呼而已) 。
抢占式调度模型是指可能有多个线程是可运行的,但只有一个线程在实际运行。这个线程会一直运行,直至它不再是可运行的,或者另一个具有更高优先级的线程成为可运行的。
对于后面一种情形,低优先级线程被高优先级线程抢占了运行的机会。
下面几种情况下,当前线程会放弃 CPU,进入阻塞状态:
l- 线程调用了yield(),suspend()或 sleep()方法主动放弃; l- 由于当前线程进行I/O 访问,外存读写,等待用户输入等操作,导致线程阻塞; l- 为等候一个条件变量,线程调用 wait()方法; l- 抢先式系统下,有高优先级的线程参与调度;时间片方式下,当前时间片用完,有同优先级的线程参与调度。
2.1:线程优先级
每个线程都有自己的优先级,通常优先级高的线程将先于优先级低的线程执行。线程的优先级用数字来表示,范围从1到10,主线程的缺省优先级是 5。其他线程的优先级默认与父线程相同,也可以使用Thread 类的下述线方法获得或设置线程对象的优先级:
public final int getPriority();
public final void setPriority(int newPriority)
为使用方便,Thread类还提供了几个public static int 常量:
Thread.MIN_PRIORITY = 1
Thread.MAX_PRIORITY = 10
Thread.NORM_PRIORITY = 5
示例13-3 使用线程优先级程序:TestPriority.java
1 public class TestPriority { 2 3 public static void main(String args[]){ 4 5 System.out.println("线程名\t优先级"); 6 7 Thread current = Thread.currentThread(); 8 9 System.out.print(current.getName() + "\t"); 10 11 System.out.println(current.getPriority()); 12 13 Thread t1 = new Runner(); 14 15 Thread t2 = new Runner(); 16 17 Thread t3 = new Runner(); 18 19 t1.setName("First"); 20 21 t2.setName("Second"); 22 23 t3.setName("Third"); 24 25 t2.setPriority(Thread.MAX_PRIORITY); 26 27 t1.start(); 28 29 t2.start(); 30 31 t3.start(); 32 33 34 35 36 37 } 38 39 } 40 41 42 43 class Runner extends Thread { 44 45 public void run() { 46 47 System.out.print(this.getName() + "\t"); 48 49 System.out.println(this.getPriority()); 50 51 } 52 53 }程序运行结果为:
线程名 优先级
main 5
Second 10
First 5
Third 5
一个线程可能因为各种原因而不再是可运行的。线程的代码可能执行了一个Thread.sleep()调用, 要求这个线程暂停一段固定的时间。这个线程可能在等待访问某个资源,而且在这个资源可访问之前,这个线程无法继续运行。
所有可运行线程根据优先级保存在池中。当一个被阻塞的线程变成可运行时,它会被放回相应的可运行池。优先级最高的非空池中的线程会得到处理机时间(被运行)。
因为 Java 线程不一定是分时的,所以你必须确保你的代码中的线程会不时地给另外一个线程运行的机会。这可以通过在各种时间间隔中发出 sleep()调用来做到。
1 public class Xyz implements Runnable { 2 3 public void run() { 4 5 while (true) { 6 7 // do lots of interesting stuff 8 9 // Give other threads a chance 10 11 try { 12 13 Thread.sleep(10); 14 15 } catch (InterruptedException e) { 16 17 // This thread's sleep was interrupted 18 19 // by another thread 20 21 } 22 23 } 24 25 } 26 27 } 28 29注意try和catch块的使用。 Thread.sleep()和其它使线程暂停一段时间的方法是可中断的。线程可以调用另外一个线程的 interrupt()方法,这将向暂停的线程发出一个InterruptedException。
注意 Thread类的sleep()方法对当前线程操作,因此被称作 Thread.sleep(x),它是一个静态方法。sleep()的参数指定以毫秒为单位的线程最小休眠时间。除非线程因为中断而提早恢复执行,否则它不会在这段时间之前恢复执行。
Thread类的另一个方法yield(),可以用来使具有相同优先级的线程获得执行的机会。
如果具有相同优先级的其它线程是可运行的,yield()将把调用线程放到可运行池中并使另一个线程运行。如果没有相同优先级的可运行进程,yield()什么都不做。
注意 sleep()调用会给较低优先级线程一个运行的机会。yield()方法只会给相同优先级线程一个执行的机会。
3:线程控制
为有效地进行线程管理和状态控制,Object类和 Thread 类中提供多个有用的方法,如
表13-1所示:
表13-1 线程控制基本方法。

示例13-4 使用sleep()方法
程序:TestSleep.java
1 import java.awt.*; 2 3 import java.util.Calendar; 4 5 public class TestSleep{ 6 7 public static void main(String args[]) { 8 9 Frame f = new Frame("My Watch"); 10 11 12 13 Label l = new Label(); 14 15 f.add(l); 16 17 f.setSize(100,50); 18 19 f.setVisible(true); 20 21 while(true){ 22 23 Calendar c = Calendar.getInstance(); 24 25 l.setText(c.get(Calendar.HOUR_OF_DAY) + ":" 26 27 + c.get(Calendar.MINUTE) + ":" 28 29 + c.get(Calendar.SECOND)); 30 31 try{ 32 33 Thread.sleep(1000); 34 35 }catch(InterruptedException e){} 36 37 } 38 39 } 40 41 }本程序实现了电子时钟的功能,其图形界面如图13-4所示。
图13-4 电子时钟
线程阻塞,休眠 1000 毫秒后再恢复运行时刷新显示时间。在其休眠期间,系统可以执行其他程序或线程,以提高运行效率。
示例 使用join()方法程序:TestJoin.java
1 public class TestJoin { 2 3 public static void main(String args[]){ 4 5 MyRunner r = new MyRunner(); 6 7 Thread t = new Thread(r); 8 9 t.start(); 10 11 try{ 12 13 t.join(); 14 15 }catch(InterruptedException e){ 16 17 } 18 19 for(int i=0;i<50;i++){ 20 21 System.out.println("主线程:" + i); 22 23 } 24 25 } 26 27 } 28 29 30 31 class MyRunner implements Runnable { 32 33 public void run() { 34 35 for(int i=0;i<50;i++) { 36 37 System.out.println("SubThread: " + i); 38 39 } 40 41 } 42 43 }程序运行输出结果为:
SubThread: 0
SubThread: 1
SubThread: 2
……
SubThread: 49
主线程:0
主线程:1
主线程:2
……
主线程:49
从中可以看出,在主线程执行过程中,调用 t.join()方法导致当前线程(主线程)阻塞,直到线程t运行终止后,主线程才获得执行的机会。如果多线程程序中,一个线程要用道另一个线程执行后提供的条件,则可考虑使用join() 方法。
3.1:创建线程和启动线程并不相同
在一个线程对新线程的 Thread 对象调用 start() 方法之前,这个新线程并没有真正开始执行。Thread 对象在其线程真正启动之前就已经存在了,而且其线程退出之后仍然存在。这可以让您控制或获取关于已创建的线程的信息,即使线程还没有启动或已经完成了。
通常在构造器中通过 start() 启动线程并不是好主意。这样做,会把部分构造的对象暴露给新的线程。如果对象拥有一个线程,那么它应该提供一个启动该线程的 start() 或 init() 方法,而不是从构造器中启动它。
3.2:结束线程
线程会以以下三种方式之一结束:
· 线程到达其 run() 方法的末尾。
· 线程抛出一个未捕获到的 Exception 或 Error。
· 另一个线程调用一个弃用的 stop() 方法。弃用是指这些方法仍然存在,但是您不应该在新代码中使用它们,并且应该尽量从现有代码中除去它们。
当 Java 程序中的所有线程都完成时,程序就退出了。
3.3:加入线程
Thread API 包含了等待另一个线程完成的方法:join() 方法。当调用 Thread.join() 时,调用线程将阻塞,直到目标线程完成为止。
Thread.join() 通常由使用线程的程序使用,以将大问题划分成许多小问题,每个小问题分配一个线程。本章结尾处的示例创建了十个线程,启动它们,然后使用 Thread.join() 等待它们全部完成。
3.4:调度
除了何时使用 Thread.join() 和 Object.wait() 外, 线程调度和执行的计时是不确定的。如果两个线程同时运行,而且都不等待,您必须假设在任何两个指令之间,其它线程都可以运行并修改程序变量。如果线程要访问其它线程可以看见的变量,如从静态字段(全局变量)直接或间接引用的数据,则必须使用同步以确保数据一致性。
在以下的简单示例中,我们将创建并启动两个线程,每个线程都打印两行到
System.out:
1 public class TwoThreads { 2 3 4 5 public static class Thread1 extends Thread { 6 7 public void run() { 8 9 System.out.println("A"); 10 11 System.out.println("B"); 12 13 } 14 15 } 16 17 18 19 public static class Thread2 extends Thread { 20 21 public void run() { 22 23 System.out.println("1"); 24 25 System.out.println("2"); 26 27 } 28 29 } 30 31 32 33 public static void main(String[] args) { 34 35 new Thread1().start(); 36 37 new Thread2().start(); 38 39 } 40 41 }我们并不知道这些行按什么顺序执行,只知道“1”在“2”之前打印,以及“A”在“B”之前打印。输出可能是以下结果中的任何一种:
· 1 2 A B
· 1 A 2 B
· 1 A B 2
· A 1 2 B
· A 1 B 2
· A B 1 2
不仅不同机器之间的结果可能不同,而且在同一机器上多次运行同一程序也可能生成不同结果。永远不要假设一个线程会在另一个线程之前执行某些操作,除非您已经使用了同步以强制一个特定的执行顺序。
3.5:休眠
Thread API 包含了一个 sleep() 方法,它将使当前线程进入等待状态,直到过了一段指定时间,或者直到另一个线程对当前线程的 Thread 对象调用了 Thread.interrupt(),从而中断了线程。当过了指定时间后,线程又将变成可运行的,并且回到调度程序的可运行线程队列中。
如果线程是由对 Thread.interrupt() 的调用而中断的,那么休眠的线程会抛出 InterruptedException,这样线程就知道它是由中断唤醒的,就不必查看计时器是否过期。
Thread.yield() 方法就象 Thread.sleep() 一样,但它并不引起休眠,而只是暂停当前线程片刻,这样其它线程就可以运行了。在大多数实现中,当较高优先级的线程调用
Thread.yield() 时,较低优先级的线程就不会运行。
CalculatePrimes 示例使用了一个后台线程计算素数,然后休眠十秒钟。当计时器过期后,它就会设置一个标志,表示已经过了十秒。
3.6:守护程序线程
我们提到过当 Java 程序的所有线程都完成时,该程序就退出,但这并不完全正确。隐藏的系统线程,如垃圾收集线程和由 JVM 创建的其它线程会怎么样?我们没有办法停止这些线程。如果那些线程正在运行,那么 Java 程序怎么退出呢?
这些系统线程称作守护程序线程。Java 程序实际上是在它的所有非守护程序线程完成后退出的。
任何线程都可以变成守护程序线程。
三:多线程的同步
1: 临界资源问题
多个线程间共享的数据称为共享资源或临界资源,由于是线程调度器负责线程的调度,程序员无法精确控制多线程的交替顺序。这种情况下,多线程对临界资源的访问有时会导致数据的不一致性。
这个问题的产生可以看下面的例子:
想象一个表示栈的类。这个类最初可能象下面那样:
1 public class MyStack { 2 3 int idx = 0; 4 5 char [] data = new char[6]; 6 7 public void push(char c) { 8 9 data[idx] = c; 10 11 idx++; 12 13 } 14 15 public char pop() { 16 17 idx--; 18 19 return data[idx]; 20 21 } 22 23 }
注意这个类没有处理栈的上溢和下溢,所以栈的容量是相当有限的。这些方面和本讨论无关。
这个模型的行为要求索引值包含栈中下一个空单元的数组下标。“先进后出”方法用来产生这个信息。
现在想象两个线程都有对这个类里的一个单一实例的引用。一个线程将数据推入栈,而另一个线程,或多或少独立地,将数据弹出栈。通常看来,数据将会正确地被加入或移走。
然而,这存在着潜在的问题。
假设线程a正在添加字符,而线程 b 正在移走字符。线程 a 已经放入了一个字符,但还没有使下标加1。因为某个原因,这个线程被剥夺(运行的机会)。这时,对象所表示的数据模型是不一致的。
buffer |p|q|r| | | |
idx = 2 ^
特别地,一致性会要求idx=3,或者还没有添加字符。
如果线程a恢复运行,那就可能不造成破坏,但假设线程 b 正等待移走一个字符。在线程a等待另一个运行的机会时,线程 b正在等待移走一个字符的机会。
pop()方法所指向的条目存在不一致的数据,然而pop方法要将下标值减 1。
buffer |p|q|r| | | |
idx = 1 ^
这实际上将忽略了字符“r”。此后,它将返回字符“q”。至此,从其行为来看,就好象没有推入字母“r”,所以很难说是否存在问题。现在看一看如果线程 a 继续运行,会发生什么。
线程 a从上次中断的地方开始运行,即在 push()方法中,它将使下标值加 1。现在你可以看到:
buffer |p|q|r| | | |
idx = 2 ^
注意这个配置隐含了:“q”是有效的,而含有“r”的单元是下一个空单元。也就是说,读取“q”时,它就象被两次推入了栈,而字母“r”则永远不会出现。
这是一个当多线程共享数据时会经常发生的问题的一个简单范例。 需要有机制来保证共享数据在任何线程使用它完成某一特定任务之前是一致的。
2 互斥锁
在 Java 语言中,为保证共享数据操作的完整性,引入了对象互斥锁的概念。每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。这好似学校里专用教室的使用规则:为保证高年级学生顺利进行课程设计,将个别教室规定为“专教”并在门上标明。任意时刻,只能有一个班级使用该教室,直至他们课程设计活动结束。即使中午或周末休息,也不必担心教室被其他的班级占用,或黑板上的文字被他人擦除。显然,这种做法会降低教室的利用率,因此除特别标明以外,教室应该是公用的,哪个班级都可以使用,由教务处负责安排调度。
Java 对象默认也是可以被多线程共用的,在需要时才启动“互斥”机制,成为专用对象。关键字 synchronized 来与对象的互斥锁联系。当某个对象用 synchronized 修饰时,表明该对象已启动“互斥”机制,在任一时刻只能由一个线程访问。即使该线程出现阻塞,该对象的被锁定状态也不会解除,其他线程仍不能访问该对象。
synchronized 关键字的使用方式有两种:
l- 用在对象前面限制一段代码的执行 l- 用在方法声明中,表示整个方法为同步方法。看一看下面修改过的代码片断:
public void push(char c) {
synchronized(this) {
data[idx] = c;
idx++;
}
}
当线程运行到 synchronized 语句,它检查作为参数传递的对象,并在继续执行之前试
图从对象获得锁标志。
2.1:对象锁标志
意识到它自身并没有保护数据是很重要的。因为如果同一个对象的 pop()方法没有受到synchronized 的影响,且 pop()是由另一个线程调用的,那么仍然存在破坏 data 的一致性的危险。如果要使锁有效,所有存取共享数据的方法必须在同一把锁上同步。
下图显示了如果 pop()受到 synchronized 的影响,且另一个线程在原线程持有那个对象的锁时试图执行pop()方法时所发生的事情:
当线程试图执行synchronized(this)语句时,它试图从 this对象获取锁标志。由于得不到标志,所以线程不能继续运行。然后,线程加入到与那个对象锁相关联的等待线程池中。当标志返回给对象时,某个等待这个标志的线程将得到这把锁并继续运行。
2.2 释放锁标志
由于等待一个对象的锁标志的线程在得到标志之前不能恢复运行, 所以让持有锁标志的线程在不再需要的时候返回标志是很重要的。
锁标志将自动返回给它的对象。持有锁标志的线程执行到 synchronized()代码块末尾时将释放锁。Java 技术特别注意了保证即使出现中断或异常而使得执行流跳出synchronized()代码块,锁也会自动返回。此外,如果一个线程对同一个对象两次发出synchronized 调用,则在跳出最外层的块时,标志会正确地释放,而最内层的将被忽略。这些规则使得与其它系统中的等价功能相比,管理同步块的使用简单了很多。
2.3 synchronized――放在一起
正如所暗示的那样,只有当所有对易碎数据的存取位于同步块内,synchronized()才会发生作用。
所有由 synchronized 块保护的易碎数据应当标记为 private。考虑来自对象的易碎部分的数据的可存取性。如果它们不被标记为 private,则它们可以由位于类定义之外的代码存取。这样,你必须确信其他程序员不会省略必需的保护。
一个方法,如果它全部属于与这个实例同步的块,它可以把 synchronized 关键字放到它的头部。下面两段代码是等价的:
public void push(char c) {
synchronized(this) {
}
}
public synchronized void push(char c) {
:
:
}
为什么使用另外一种技术?
如果你把synchronized作为一种修饰符,那么整个块就成为一个同步块。这可能会导致不必要地持有锁标志很长时间,因而是低效的。
然而,以这种方式来标记方法可以使方法的用户由 javadoc产生的文档了解到:正在同步。这对于设计时避免死锁(将在下一节讨论)是很重要的。注意 javadoc 文档生成器将synchronized 关键字传播到文档文件中,但它不能为在方法块内的 synchronized(this)做到这点。
synchronized-放在一起
l- 所有对易碎数据的存取应当同步。 l- 由synchronized保护的易碎数据应当是private的。3 线程的死锁
如果程序中有多个线程竞争多个资源,就可能会产生死锁。当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。在这种情况下,除非另一个已经执行到synchronized 块的末尾,否则没有一个线程能继续执行。由于没有一个线程能继续执行,所以没有一个线程能执行到块的末尾。
Java 技术不监测也不试图避免这种情况。因而保证不发生死锁就成了程序员的责任。避免死锁的一个通用的经验法则是:决定获取锁的次序并始终遵照这个次序。按照与获取相反的次序释放锁。
讨论下面的情况,两个线程A、B用到同一个对象 s(s 为共享资源),且线程 A 在执行中要用到B运行后所创造的条件(例如,使用 B.join()语句与 B建立同步) 。在这种前提下A先开始运行,进入同步语句块后,对象s 被锁定,接着线程 A因等待 B运行结束这一条件而进入阻塞状态。于是线程 B 开始运行,但因无法访问对象 s(已在 A 中被锁定),线程 B也进入阻塞状态,等待s被线程A解除锁定。最终的结果是,两个线程互相等待,都无法运行,这种状态称为线程的死锁。
要避免死锁,应该确保在获取多个锁时,在所有的线程中都以相同的顺序获取锁。
4 wait()和notify()方法
使用synchronized关键字锁定Java对象,就好似规定某个教室为专用教室一样,不可避免的会降低程序效率。
为了实现线程阻塞时释放其锁定的共享资源,以给其他的线程提供运行的机会。
java.lang 包中定义了几个有用的方法:wait(), notify(), notifyAll()。
在同步方法(语句块)中,被锁定的对象可以调用 wait()方法,这将导致当前线程被阻塞并放弃该对象的互斥锁,即解除了 wait()方法的当前对象的锁定状态,其他的线程就有机会访问该对象。因调用了 wait()方法而阻塞的线程,将被加入一个特殊的对象等待队列中,直到调用该wait()方法的对象在其他的线程中调用了 notify()或 notifyAll()方法,这种等待才可能解除。
如果有多个线程因调用了 wait()方法而在等待同一个对象,则应使用该对象在其他线程中调用notifyAll()方法,以使等待队列上的所有线程离开阻塞状态。而notify()方法每次运行只能唤醒等待队列中一个线程,至于是哪一个被唤醒则由线程调度器决定,程序员是无法控制的。引入了wait()/notify()方法后线程的状态转换情况如图所示:
经常创建不同的线程来执行不相关的任务。然而,有时它们所执行的任务是有某种联系的,为此必须编写使它们交互的程序。
4.1 场景
把你自己和出租车司机当作两个线程。你需要出租车司机带你到终点,而出租车司机需要为乘客服务来获得车费。所以,你们两者都有一个任务。
4.2 问题
你希望坐到出租车里,舒服地休息,直到出租车司机告诉你已经到达终点。如果每 2秒就问一下“我们到了哪里?”,这对出租车司机和你都会是很烦的。出租车司机想睡在出租车里,直到一个乘客想到另外一个地方去。出租车司机不想为了查看是否有乘客的到来而每5分钟就醒来一次。所以,两个线程都想用一种尽量轻松的方式来达到它们的目的。
4.3 解决方案
出租车司机和你都想用某种方式进行通信。当你正忙着走向出租车站时,司机正在车中安睡。当你告诉司机你想坐他的车时,司机醒来并开始驾驶,然后你开始等待并休息。到达终点时,司机会通知你,所以你必须继续你的任务,即走出出租车,然后去工作。出租车司机又开始等待和休息,直到下一个乘客的到来。
4.4 wait()和notify()
java.lang.Object 类中提供了两个用于线程通信的方法:wait()和 notify()。如果线程对一个同步对象 x 发出一个 wait()调用,该线程会暂停执行,直到另一个线程对同一个同步对象x也发出一个 wait()调用。
在上个场景中,在车中等待的出租车司机被翻译成执行 cab.wait()调用的“出租车司机”线程,而你使用出租车的需求被翻译成执行cab.notify()调用的“你”线程。
为了让线程对一个对象调用 wait()或 notify(),线程必须锁定那个特定的对象。也就是说,只能在它们被调用的实例的同步块内使用 wait()和 notify()。对于这个实例来说,
需要一个以synchronized(cab)开始的块来允许执行cab.wait()和 cab.notify()调用。
关于池
当线程执行包含对一个特定对象执行 wait()调用的同步代码时,那个线程被放到与那个对象相关的等待池中。此外,调用 wait()的线程自动释放对象的锁标志。可以调用不同的wait():
wait() 或 wait(long timeout);
对一个特定对象执行 notify()调用时,将从对象的等待池中移走一个任意的线程,并放到锁池中,那里的对象一直在等待,直到可以获得对象的锁标记。 notifyAll()方法将从等待池中移走所有等待那个对象的线程并放到锁池中。 只有锁池中的线程能获取对象的锁标记,锁标记允许线程从上次因调用wait()而中断的地方开始继续运行。
在许多实现了 wait()/notify()机制的系统中,醒来的线程必定是那个等待时间最长的线程。然而,在Java技术中,并不保证这点。
注意,不管是否有线程在等待,都可以调用 notify()。如果对一个对象调用 notify()方法,而在这个对象的锁标记等待池中并没有阻塞的线程,那么 notify()调用将不起任何作用。对 notify()的调用不会被存储。
4.5 同步的监视模型
同步的监视模型
l- 使共享数据处于一致的状态 l- 保证程序不死锁 l- 不要将期待不同通知的线程放到同一个等待协调两个需要存取公共数据的线程可能会变得非常复杂。你必须非常小心,以保证可能有另一个线程存取数据时,共享数据的状态是一致的。因为线程不能在其他线程在等待这把锁的时候释放合适的锁,所以你必须保证你的程序不发生死锁,在出租车范例中,代码依赖于一个同步对象――出租车,在其上执行 wait()和 notify()。
如果有任何人在等待一辆公共汽车,你就需要一个独立的公共汽车对象,在它上面施用notify()。记住,在同一个等待池中的所有线程都因来自等待池的控制对象的通知而满足。
永远不要设计这样的程序:把线程放在同一个等待池中,但它们却在等待不同条件的通知。
4.6 放在一起
下面将给出一个线程交互的实例,它说明了如何使用 wait()和 notify()方法来解决一个经典的生产者-消费者问题。
我们先看一下栈对象的大致情况和要存取栈的线程的细节。然后再看一下栈的详情,以及基于栈的状态来保护栈数据和实现线程通信的机制。
实例中的栈类称为SyncStack,用来与核心java.util.Stack相区别,它提供了如下公共的API:
public synchronized void push(char c);
public synchronized char pop();
生产者线程运行如下方法:
1 public void run() { 2 3 char c; 4 5 for (int i = 0; i < 200; i++) { 6 7 c = (char)(Math.random() * 26 + 'A'); 8 9 theStack.push(c); 10 11 System.out.println("Producer" + num + ": " + c); 12 13 try { 14 15 Thread.sleep((int)(Math.random() * 300)); 16 17 } catch (InterruptedException e) { 18 19 // ignore it 20 21 } 22 23 } 24 25 }这将产生200 个随机的大写字母并将其推入栈中, 每个推入操作之间有 0到 300 毫秒的随机延迟。 每个被推入的字符将显示到控制台上, 同时还显示正在执行的生产者线程的标识。
消费者
消费者线程运行如下方法:
1 public void run() { 2 3 char c; 4 5 for (int i = 0; i < 200; i++) { 6 7 c = theStack.pop(); 8 9 System.out.println(" Consumer" + num + ": " + c); 10 11 try { 12 13 Thread.sleep((int)(Math.random() * 300)); 14 15 } catch (InterruptedException e) { 16 17 // ignore it 18 19 } 20 21 } 22 23 }上面这个程序从栈中取出200个字符, 每两个取出操作的尝试之间有 0 到300 毫秒的随机延迟。每个被弹出的字符将显示在控制台上,同时还显示正在执行的消费者线程的标识。
现在考虑栈类的构造。你将使用 Vector 类创建一个栈,它看上去有无限大的空间。按照这种设计,你的线程只要在栈是否为空的基础上进行通信即可。
SyncStack类
一个新构造的SyncStack对象的缓冲应当为空。下面这段代码用来构造你的类:
1 public class SyncStack { 2 3 private Vector buffer = new Vector(400,200); 4 5 public synchronized char pop() { 6 7 } 8 9 public synchronized void push(char c) { 10 11 } 12 13 }请注意,其中没有任何构造函数。包含有一个构造函数是一种相当好的风格,但为了保持简洁,这里省略了构造函数。
现在考虑 push()和 pop()方法。为了保护共享缓冲,它们必须均为 synchronized。此外,如果要执行pop()方法时栈为空,则正在执行的线程必须等待。若执行 push()方法后栈不再为空,正在等待的线程将会得到通知。
pop()方法如下:
1 public synchronized char pop() { 2 3 char c; 4 5 while (buffer.size() == 0) { 6 7 try { 8 9 this.wait(); 10 11 } catch (InterruptedException e) { 12 13 // ignore it 14 15 } 16 17 } 18 19 c = ((Character)buffer.remove(buffer.size()-1)).charValue(); 20 21 return c; 22 23 }注意这里显式地调用了栈对象的 wait(),这说明了如何对一个特定对象进行同步。如果栈为空,则不会弹出任何数据,所以一个线程必须等到栈不再为空时才能弹出数据。
由于一个 interrupt()的调用可能结束线程的等待阶段,所以 wait()调用被放在一个try/catch 块中。对于本例,wait()还必须放在一个循环中。如果 wait()被中断,而栈仍为空,则线程必须继续等待。
栈的 pop()方法为 synchronized 是出于两个原因。首先,将字符从栈中弹出影响了共享数据buffer。其次,this.wait()的调用必须位于关于栈对象的一个同步块中,这个块由this表示。
你将看到 push()方法如何使用 this.notify()方法将一个线程从栈对象的等待池中释放出来。一旦线程被释放并可随后再次获得栈的锁,该线程就可以继续执行 pop()完成从栈缓冲区中移走字符任务的代码。
注 - 在 pop()中, wait()方法在对栈的共享数据作修改之前被调用。 这是非常关键的一点,因为在对象锁被释放和线程继续执行改变栈数据的代码之前,数据必须保持一致的状态。你必须使你所设计的代码满足这样的假设:在进入影响数据的代码时,共享数据是处于一致的状态。
需要考虑的另一点是错误检查。你可能已经注意到没有显式的代码来保证栈不发生下溢。这不是必需的,因为从栈中移走字符的唯一方法是通过 pop()方法,而这个方法导致正
在执行的线程在没有字符的时候会进入 wait()状态。因此,错误检查不是必要的。push()在影响共享缓冲方面与此类似,因此也必须被同步。此外,由于 push()将一个字符加入缓冲区,所以由它负责通知正在等待非空栈的线程。这个通知的完成与栈对象有关。
push()方法如下:
1 public synchronized void push(char c) { 2 3 this.notify(); 4 5 Character charObj = new Character(c); 6 7 buffer.addElement(charObj); 8 9 }对this.notify()的调用将释放一个因栈空而调用 wait()的单个线程。在共享数据发生真正的改变之前调用notify()不会产生任何结果。只有退出该 synchronized块后,才会释放对象的锁,所以当栈数据在被改变时,正在等待锁的线程不会获得这个锁。
4.7 SyncStack范例
完整的代码
现在,生产者、消费者和栈代码必须组装成一个完整的类。还需要一个测试工具将这些代码集成为一体。特别要注意,SyncTest是如何只创建一个由所有线程共享的栈对象的。
SyncTest.java
1.package mod14; 2.public class SyncTest { 3.public static void main(String args[]) { 4. 5.SyncStack stack = new SyncStack(); 6. 7.Producer p1 = new Producer(stack); 8.Thread prodT1= new Thread(p1); 9.prodT1.start(); 11.Producer p2 = new Producer(stack); 12.Thread prodT2= new Thread(p2); 13.prodT2.start(); 15.Consumer c1 = new Consumer(stack); 16.Thread consT1 = new Thread(c1); 17.consT1.start(); 19.Consumer c2 = new Consumer(stack); 20.Thread consT2 = new Thread(c2); 21.constT2.start(); 22.} 23.}Producer.java
1.package mod14; 2.public class Producer implements Runnable { 3.private SyncStack theStack; 4.private int num; 5.private static int counter = 1; 6.public Producer (SyncStack s) { 7.theStack = s; 8.num = counter++; 9.} 10. 11.public void run() { 12.char c; 13. 14.for (int i = 0; i < 200; i++) { 15.c = (char)(Math.random() * 26 + `A'); 16.theStack.push(c); 17.System.out.println("Producer" + num + ": " + c); 18.try { 19.Thread.sleep((int)(Math.random() * 300)); 20.} catch (InterruptedException e) { 21.// ignore it 22.} 23.} 24.} 25.}Consumer.java
1.package mod14; 2.public class Consumer implements Runnable { 3.private SyncStack theStack; 4.private int num; 5.private static int counter = 1; 6. 7.public Consumer (SyncStack s) { 8.theStack = s; 9.num = counter++; 10.} 11. 12.public void run() { 13.char c; 14.for (int i=0; i < 200; i++) { 15.c = theStack.pop(); 16.System.out.println("Consumer" + num + ": " + c); 17.try { 18.Thread.sleep((int)(Math.random() * 300)); 19.} catch (InterruptedException e) { 20.// ignore it 21.} 22.} 23.} 24.}SyncStack.java
1.package mod14; 2. 3.import java.util.Vector; 4. 5.public class SyncStack { 6.private Vector buffer = new Vector(400,200); 7. 8.public synchronized char pop() { 9.char c; 10. 11.while (buffer.size() == 0) { 12.try { 13.this.wait(); 14.} catch (InterruptedException e) { 15.// ignore it 16.} 17.} 18. 19. c = ((Character)buffer.remove(buffer.size()- 1).charValue(); 20.return c; 21.} 22.public synchronized void push(char c) { 23.this.notify(); 24.Character charObj = new Character(c); 25.buffer.addelement(charObj); 26.} 27.}运行javamodB.SyncTest的输出如下。 请注意每次运行线程代码时, 结果都会有所不同。
Producer2: F
Consumer1: F
Producer2: K
Consumer2: K
Producer2: T
Producer1: N
Producer1: V
Consumer2: V
Consumer1: N
Producer2: V
Producer2: U
Consumer2: U
Consumer2: V
Producer1: F
Consumer1: F
Producer2: M
Consumer2: M
Consumer2: T
Java 语言包含了内置在语言中的功能强大的线程工具。您可以将线程工具用于:
· 增加 GUI 应用程序的响应速度
· 利用多处理器系统
· 当程序有多个独立实体时,简化程序逻辑
· 在不阻塞整个程序的情况下,执行阻塞 I/O
当使用多个线程时,必须谨慎,遵循在线程之间共享数据的规则,我们将在共享对数据的访问中讨论这些规则。所有这些规则归结为一条基本原则:不要忘了同步。
5:同步的应用
5.1可见性同步
跨线程维护正确的可见性,只要在几个线程之间共享非 final 变量,就必须使用synchronized 以确保一个线程可以看见另一个线程做的更改。
可见性同步的基本规则是在以下情况中必须同步:
· 读取上一次可能是由另一个线程写入的变量
· 写入下一次可能由另一个线程读取的变量
5.2用于一致性的同步
除了用于可见性的同步, 从应用程序角度看, 您还必须用同步来确保一致性得到了维护。当修改多个相关值时, 您想要其它线程原子地看到这组更改 — 要么看到全部更改,要么什么也看不到。这适用于相关数据项(如粒子的位置和速率)和元数据项(如链表中包含的数据值和列表自身中的数据项的链)。
考虑以下示例,它实现了一个简单(但不是线程安全的)的整数堆栈:
1 public class UnsafeStack { 2 3 public int top = 0; 4 5 public int[] values = new int[1000]; 6 7 public void push(int n) { 8 9 values[top++] = n; 10 11 } 12 13 public int pop() { 14 15 return values[--top]; 16 17 } 18 19 }如果多个线程试图同时使用这个类,会发生什么?这可能是个灾难。因为没有同步,多个线程可以同时执行 push() 和 pop()。如果一个线程调用 push(),而另一个线程正好在递增了 top 并要把它用作 values 的下标之间调用 push(),会发生什么?结果,这两个线程会把它们的新值存储到相同的位置!当多个线程依赖于数据值之间的已知关系,但没有确保只有一个线程可以在给定时间操作那些值时,可能会发生许多形式的数据损坏,而这只是其中之一。
对于这种情况,补救办法很简单:同步 push() 和 pop() 这两者,您将防止线程执行相互干扰。 请注意, 需要使用 synchronized 来确保 top 和 values 之间的关系保持一致。
5.3不变性和 final 字段
许多 Java 类,包括 String、Integer 和 BigDecimal,都是不可改变的:一旦构造之后,它们的状态就永远不会更改。如果某个类的所有字段都被声明成 final,那么这个类就是不可改变的。 (实际上,许多不可改变的类都有非 final 字段,用于高速缓存以前计算的方法结果,如 String.hashCode(),但调用者看不到这些字段。) 不可改变的类使并发编程变得非常简单。因为不能更改它们的字段,所以就不需要担心把状态的更改从一个线程传递到另一个线程。 在正确构造了对象之后, 可以把它看作是常量。
同样,final 字段对于线程也更友好。因为 final 字段在初始化之后,它们的值就不能更改,所以当在线程之间共享 final 字段时,不需要担心同步访问
5.4什么时候不需要同步
在某些情况中,您不必用同步来将数据从一个线程传递到另一个,因为 JVM 已经隐含
地为您执行同步。这些情况包括:
· 由静态初始化器(在静态字段上或 static{} 块中的初始化器)初始化数据时
· 访问 final 字段时
· 在创建线程之前创建对象时
· 线程可以看见它将要处理的对象时
5.5性能考虑事项
关于同步的性能代价有许多说法 — 其中有许多是错的。同步,尤其是争用的同步,确实有性能问题,但这些问题并没有象人们普遍怀疑的那么大。 许多人都使用别出心裁但不起作用的技巧以试图避免必须使用同步,但最终都陷入了麻烦。一个典型的示例是双重检查锁定模式。这种看似无害的结构据说可以避免公共代码路径上的同步,但却令人费解地失败了,而且所有试图修正它的尝试也失败了。 在编写并发代码时,除非看到性能问题的确凿证据,否则不要过多考虑性能。瓶颈往往出现在我们最不会怀疑的地方。 投机性地优化一个也许最终根本不会成为性能问题的代码路径 — 以程序正确性为代价 — 是一桩赔本的生意。
5.6同步准则
当编写 synchronized 块时,有几个简单的准则可以遵循,这些准则在避免死锁和性能危险的风险方面大有帮助:
使代码块保持简短。Synchronized 块应该简短 — 在保证相关数据操作的完整性的同时,尽量简短。把不随线程变化的预处理和后处理移出 synchronized 块。
不要阻塞。不要在 synchronized 块或方法中调用可能引起阻塞的方法,如InputStream.read()
在持有锁的时候,不要对其它对象调用方法。这听起来可能有些极端,但它消除了最
常见的死锁源头.
6:线程有关的API
除了使用轮询(它可能消耗大量 CPU 资源,而且具有计时不精确的特征),Object 类
还包括一些方法, 可以让线程相互通知事件的发生。 Object 类定义了 wait()、 notify() 和notifyAll() 方法。 要执行这些方法, 必须拥有相关对象的锁。 Wait() 会让调用线程休眠,直到用 Thread.interrupt() 中断它、过了指定的时间、或者另一个线程用 notify() 或notifyAll() 唤醒它。 当对某个对象调用 notify() 时,如果有任何线程正在通过 wait()等待该对象,那么就会唤醒其中一个线程。当对某个对象调用 notifyAll() 时,会唤醒所有正在等待该对象的线程。
这些方法是更复杂的锁定、 排队和并发性代码的构件。 但是, notify() 和 notifyAll()的使用很复杂。尤其是,使用 notify() 来代替 notifyAll() 是有风险的。除非您确实知道正在做什么,否则就使用 notifyAll()。
与其使用 wait() 和 notify() 来编写您自己的调度程序、线程池、队列和锁,倒不如使用 util.concurrent 包,这是一个被广泛使用的开放源码工具箱,里面都是有用的并发性实用程序。JDK 1.5 将包括 java.util.concurrent 包;它的许多类都派生自util.concurrent。
6.1:线程优先级
Thread API 让您可以将执行优先级与每个线程关联起来。但是,这些优先级如何映射到底层操作系统调度程序取决于实现。在某些实现中,多个 — 甚至全部 — 优先级可能被映射成相同的底层操作系统优先级。
在遇到诸如死锁、资源匮乏或其它意外的调度特征问题时,许多人都想要调整线程优先级。但是,通常这样只会把问题移到别的地方。大多数程序应该完全避免更改线程优先级。
6.2:线程组
ThreadGroup 类原本旨在用于把线程集合构造成组。但是,结果证明 ThreadGroup 并没有那样有用。您最好只使用 Thread 中的等价方法。
ThreadGroup 确实提供了一个有用的功能部件(Thread 中目前还没有):
uncaughtException() 方法。线程组中的某个线程由于抛出了未捕获的异常而退出时,会调用 ThreadGroup.uncaughtException() 方法。这就让您有机会关闭系统、将一条消息写到日志文件或者重新启动失败的服务
6.3:SwingUtilities
虽然 SwingUtilities 类不是 Thread API 的一部分,但还是值得简单提一下。
正如前面提到的, Swing 应用程序有一个 UI 线程 (有时叫称为事件线程), 所有 UI 活动都必须在这个线程中发生。有时,另一个线程也许想要更新屏幕上某样东西的外观,或者触发 Swing 对象上的一个事件。
SwingUtilities.invokeLater() 方法可以让您将 Runnable 对象传送给它,并且在事件线程中执行指定的 Runnable。它的同类 invokeAndWait() 会在事件线程中调用
Runnable,但 invokeAndWait() 会阻塞,直到 Runnable 完成执行之后。
1 void showHelloThereDialog() throws Exception { 2 3 Runnable showModalDialog = new Runnable() { 4 5 public void run() { 6 7 JOptionPane.showMessageDialog(myMainFrame, "Hello There"); 8 9 } 10 11 }; 12 13 SwingUtilities.invokeLater(showModalDialog); 14 15 }对 于 AWT 应 用 程序 , java.awt.EventQueue 还 提 供 了 invokeLater() 和invokeAndWait()。
6.4:suspend()和resume()方法
JDK1.2 中不赞成使用 suspend()和 resume()方法。resume()方法的唯一作用就是恢复被挂起的线程。所以,如果没有suspend(),resume()也就没有存在的必要。从设计的角度来看,有两个原因使suspend()非常危险:它容易产生死锁;它允许一个线程控制另一个线程代码的执行。下面将分别介绍这两种危险。
假设有两个线程:threadA 和 threadB。当正在执行它的代码时,threadB 获得一个对象的锁,然后继续它的任务。现在 threadA 的执行代码调用 threadB.suspend(),这将使threadB 停止执行它的代码。
如果threadB.suspend()没有使threadB释放它所持有的锁,就会发生死锁。如果调用threadB.resume()的线程需要threadB仍持有的锁,这两个线程就会陷入死锁。
假设threadA调用threadB.suspend()。如果 threadB被挂起时 threadA获得控制,那么 threadB 就永远得不到机会来进行清除工作,例如使它正在操作的共享数据处于稳定状态。为了安全起见,只有threadB 才可以决定何时停止它自己的代码。
你应该使用对同步对象调用wait()和 notify()的机制来代替 suspend()和 resume()进行线程控制。这种方法是通过执行 wait()调用来强制线程决定何时“挂起”自己。这使得同步对象的锁被自动释放,并给予线程一个在调用 wait()之前稳定任何数据的机会。
6.5: stop()方法
suspend()和resume()方法
l- JDK1.2不赞成使用它们 l- 应当用wait()和notify()来代替它们stop()方法
l- 在终止前释放锁。 l- 可能使共享数据处于不一致的状态。 l- 应当用wait()和notify()来代替它们stop()方法的情形是类似的,但结果有所不同。如果一个线程在持有一个对象锁的时候被停止,它将在终止之前释放它持有的锁。这避免了前面所讨论的死锁问题,但它又引入了其他问题。
在前面的范例中,如果线程在已将字符加入栈但还没有使下标值加 1 之后被停止,你在释放锁的时候会得到一个不一致的栈结构。
总会有一些关键操作需要不可分割地执行, 而且在线程执行这些操作时被停止就会破坏操作的不可分割性。
一个关于停止线程的独立而又重要的问题涉及线程的总体设计策略。 创建线程来执行某个特定作业,并存活于整个程序的生命周期。换言之,你不会这样来设计程序:随意地创建和处理线程,或创建无数个对话框或 socket 端点。每个线程都会消耗系统资源,而系统资源并不是无限的。这并不是暗示一个线程必须连续执行;它只是简单地意味着应当使用合适而安全的wait()和notify()机制来控制线程。
练习实践
本章的内容为多线程,实践重点:
l 多线程的含义与应用
程序 1
多线程基础
需求:建立两个线程,各显示0~500的数。
目标:
1、 线程的概念;
2、 线程的创建;
3、 理解线程与进程的区别;
4、 线程的使用。
程序:
1 package com.useful.java.part5; 2 3 4 5 public class TestThread extends Thread { 6 7 8 9 int intNumberThread = 0; 10 11 int intNumberThreadMain = 0; 12 13 int intTotal = 500; 14 15 16 17 public TestThread() { 18 19 } 20 21 22 23 public void run(){ 24 25 while(true){ 26 27 System.out.println("Thread : " + intNumberThread); 28 29 intNumberThread++; 30 31 if(intNumberThread > intTotal){ 32 33 return; 34 35 } 36 37 } 38 39 } 40 41 42 43 public static void main(String[] args) { 44 45 TestThread testThread1 = new TestThread(); 46 47 testThread1.start(); 48 49 while(testThread1.intNumberThreadMain < testThread1.intTotal){ 50 51 System.out.println("Main() -- : " + 52 53 testThread1.intNumberThreadMain); 54 55 testThread1.intNumberThreadMain++; 56 57 58 59 } 60 61 } 62 63 } 64 65
说明:
1、 创建的常用方法之一就是继承 Thread,然后覆盖 run();
2、 线程的执行无先后之分;
3、 运行情况如下图:
程序 2
线程的睡眠
需求:在线程应用sleep,让线程执行时进行一段时间的睡眠。
目标:
1、 sleep的使用;
2、 多线程应用。
程序:
//: TestMultiThreadSleep.java
1 package com.useful.java.part5; 2 3 4 5 public class TestMultiThreadSleep extends Thread{ 6 7 8 9 int intNumberThread = 0; 10 11 int intNumberThreadMain = 0; 12 13 int intTatol = 500; 14 15 16 17 public TestMultiThreadSleep() { 18 19 } 20 21 22 23 public void run(){ 24 25 26 27 while(true){ 28 29 System.out.println("Thread : " + intNumberThread); 30 31 intNumberThread++; 32 33 //Sleep 0.01 秒钟 34 35 try { 36 37 sleep(10); 38 39 } catch (InterruptedException e){ 40 41 e.printStackTrace(); 42 43 } 44 45 if(intNumberThread > intTatol){ 46 47 return; 48 49 } 50 51 } 52 53 } 54 55 56 57 public static void main(String[] args) { 58 59 TestMultiThreadSleep testThread1 = new TestMultiThreadSleep(); 60 61 testThread1.start(); 62 63 //再加一个线程 testThread2 64 65 TestMultiThreadSleep testThread2 = new TestMultiThreadSleep(); 66 67 testThread2.start(); 68 69 while(testThread1.intNumberThreadMain < testThread1.intTatol){ 70 71 System.out.println("Main() -- : " + 72 73 testThread1.intNumberThreadMain); 74 75 testThread1.intNumberThreadMain++; 76 77 } 78 79 } 80 81 } 82 83
说明:
1、 sleep只能在线程中使用;
2、 sleep(1000)表示睡眠 1秒,这里的单位默认为微秒;
3、 当使用sleep时,需要捕获异常 InterruptedException。
作业
1、 从Thread继承一个类,并覆盖 run()方法。在run()内,打印出一条消息,然后调用
sleep()。重复三遍这些操作,然后从run()返回。在构建器中放置一条启动消息,并
覆盖 finalize(),打印一条关闭消息。创建一个独立的线程类,使它在 run()内调用
System.gc()和System.runFinalization(),并打印一条消息,表明调用成功。创建这两
种类型的几个线程,然后运行它们,看看会发生什么。














浙公网安备 33010602011771号