540 likes | 648 Views
第 9 章 多线程. 教学目的要求. 1. 理解进程和线程的概念,学习 java 中线程的使用 ; 2. 掌握线程的状态和生命周期、线程的调度和控制方法 ; 3. 理解多线程的互斥和同步的实现原理,以及多线程的应用,能够熟练编写关于线程、线程间的同步与通信的小程序. 9.1 多线程的概念. 进程和线程
E N D
第9章 多线程 教学目的要求 1.理解进程和线程的概念,学习java中线程的使用 ; 2.掌握线程的状态和生命周期、线程的调度和控制方法; 3.理解多线程的互斥和同步的实现原理,以及多线程的应用,能够熟练编写关于线程、线程间的同步与通信的小程序
9.1 多线程的概念 进程和线程 • 进程就是在计算机中正在执行的程序(即处于活动状态的程序),每一个进程都有自己独立的一块内存空间和一组系统资源,比如在Windows、Linux等操作系统中可以同时执行多个程序,这里的每一个运行的程序都是一个进程,这些程序使用的内存空间和系统资源都是独立的,并且每个程序也是为了各自的任务而运行,互不干扰。进程概念的引入是操作系统发展史上的一个里程碑,正是进程技术的引入才使得计算机操作系统同时处理多个任务成为可能,这也促使了像Windows一样的多任务操作系统的出现,使计算机的运行效率在很大程度上得到了提升。在进程的基础上,线程概念后来又被提出,它使得在一个进程中同时处理多个任务成为可能。
多线程 • 多线程允许在程序中“并行”执行多个指令流,每个指令流被称作一个线程,彼此间的执行互相独立。多线程需要操作系统的支持,WIN32平台支持多线程程序,允许程序中存在多个线程。在单CPU计算机系统中,系统把CPU的时间片按照调度算法分配给各个线程,因此各线程实际上是分时执行的,而在多CPU的计算机系统中,同一个程序的不同线程可以分配到不同的CPU上去执行。多个线程的执行是并发的,也就是在逻辑上“同时”,而不是物理上的“同时”。如果系统只有一个CPU,那么真正的“同时”是不可能的,但是由于CPU的速度非常快,用户感觉不到其中的区别。
(a)单线程程序 (b)多线程程序 图9.1 单线程与多线程的对比
Java中的多线程机制 • 线程需要计算机系统的支持,并不是所有类型的计算机都支持多线程应用程序。但是由于Java引入了虚拟处理器技术,所以Java语言将线程支持与语言运行环境结合在一起,不管在任何系统下,Java语言都提供了多任务并发执行的能力,如图9.2所示。这就好比一个人在处理家务的过程中,将米放在电饭锅里后再把衣服放到洗衣机中自动洗涤,然后开始做菜,等菜做好了,饭也熟了,同时衣服也洗好了。只要合理安排各个线程的运行,就可以极大地提高程序的运行效率。
虚拟CPU Code Data • 对多线程的综合支持是Java语言的一个重要特色,在Java中,内置了Thread类来实现多线程,当程序引用了java.lang.Thread类,也就引入了一个Java执行环境。由图9.2可知,一个线程是由三部分组成的: • 虚拟处理机(CPU),封装在java.lang.Thread类中,它控制着整个线程的运行,提供对多线程的支持; • 执行的程序代码,传递给Thread类,由Thread类控制顺序执行; • 程序所处理的数据,传递 给Thread类,是在代码执 行过程中所要处理的数据。 图9.2 线程的组成
在Java编程中,虚拟处理机(CPU)被封装在Thread线程类的实例之中。这样一来,有多少个需要完成的子任务(线程)就有多少个虚拟CPU这样的“虚拟计算机”在同时运行,把一个较大的任务分割成许多较小的子任务分别地、“同时”地去完成,这就是Java多线程机制。在Java编程中,虚拟处理机(CPU)被封装在Thread线程类的实例之中。这样一来,有多少个需要完成的子任务(线程)就有多少个虚拟CPU这样的“虚拟计算机”在同时运行,把一个较大的任务分割成许多较小的子任务分别地、“同时”地去完成,这就是Java多线程机制。 • Java的线程是通过java.lang包中定义的类Thread来实现的。当生成一个Thread类的对象之后,就产生了一个线程,通过该对象实例,可以启动线程、终止线程、或者暂时挂起线程等。由于Java在语言级提供了对线程的支持,所以在Java语言中使用多线程要远比在其它语言中使用线程简单得多。
9.2 线程类及其线程创建 线程类 • 通常在Java程序设计中,任何机制都是基于类的,当然线程也不例外,所以要学会Java中的多线程编程,就必需了解实现线程的Thread类。 • 线程对象实例表示Java程序中的真正的线程,通过它可以启动线程、终止线程、挂起线程等操作,Thread类是负责向其它类提供线程支持的最主要的类,Thread类在包java.lang中定义,它的构造方法为: public Thread(ThreadGroup group,Runnable target,String name); • 其中,group指明该线程所属的线程组(关于线程组将在后面详细讲解);target是指实际执行线程体的目标对象,它必须实现接口Runnable;name为线程名,Java中的每个线程都有自己的名称,Java提供了不同Thread类构造器,允许给线程指定名称,如果name为null时,则Java将自动为其分配一个唯一的名称。
在线程构造方法中,每个参数都可以为空,当上述构造方法的某个参数为null时,就可以得到下面的几个构造方法:在线程构造方法中,每个参数都可以为空,当上述构造方法的某个参数为null时,就可以得到下面的几个构造方法: • public Thread(); • public Thread(String name); • public Thread(Runnable target); • public Thread(Runnable target,String name); • public Thread(ThreadGroup group,String name); • public Thread(ThreadGroup group,Runnable target);
Thread类中也有许多有用的方法,在这里只介绍部分常用的方法:Thread类中也有许多有用的方法,在这里只介绍部分常用的方法: 1. public static void yield();引起当前执行线程暂停,允许其他线程执行。 2. public static void sleep(long millis);使当前执行的线程睡眠指定的时间。参数millis是线程睡眠的毫秒数。如果这个线程已经被别的线程中断,就会产生InterruptedException异常。 3. public void start();使线程由新建状态变成可运行状态。 4. public void run();如果线程实例是使用实现了Runnable接口的类的实例创建的,就调用这个类的实例的run( )方法,否则什么都不做并返回。 5. public final void stop();停止(杀死)当前线程。 6. public final void wait();返回当前线程所在的线程组中的线程的数目 7. public final boolean isAlive();测试线程是否处于活动状态,即已启动,但还没有终止。 8. public final void setPriority(int new);改变线程的优先级。 9. public static Thread currenthread();返回当前执行线程对象的引用。 10. public final void notify();唤醒一个等待中的线程。 11. public final void notifyAll();唤醒所有处于等待中的线程。
线程的创建 • 在Java中,每个程序至少自动拥有一个线程,称为主线程,当程序加载到内存时,启动主线程,如果需要使用其它线程,则可以采用以下两种方式创建新的线程:一种是扩展java.lang.Thread类,用它覆盖Thread类的run( )方法;另一种是编写一个类,使之实现java.lang.Runnable接口,然后在Thread构造函数中使用它。 • 第一种方式只能在类没有扩展其它任何类的情况下才能使用,因为Java不允许多重继承。因此,如果一个类要继承其它的类,最好选用第二种方法,这样会有更大的灵活性。下面分别介绍两种创建线程的方式。
1.扩展Thread类 • 类Thread位于java.lang包中,由于java.lang包自动被导入每个Java程序中,所以可以直接使用类Thread而无需在Java程序开始处编写import语句,也这说明了Java对线程支持的彻底性。通过这个类中的方法,可以启动、终止、中断线程以及查询、设置线程的当前状态。 • 使用扩展Thread类的方式创建并执行一个线程,需要执行下面4个步骤: • 扩展java.lang.Thread的类; • 用希望的执行代码来实现run()方法; • 通过new关键字实例化该类的一个新对象(即一个线程); • 通过调用start()方法启动线程。
例如: public class yourThread extends Thread { public run(){ //需要以线程方式运行的代码,也就是所希望的执行代码 } } • 上面的代码完成了扩展Thread类和重写了run( )方法,为了在程序中使用线程,还需要创建一个对象并调用run( )方法,如: yourThread tt=new yourThread(); tt.start(); • 在这里,start()方法将自动调用在线程体内重写的run()方法来执行线程的具体处理,当run()方法执行完毕时,线程将自动结束。 • 下面我们将通过例9.1学习如何使用扩展Thread类的方法来实现线程。
System.out.println("线程 " + number + ":计数 " + count); if (++count == 6) return; } } //run() public static void main(String args[]) { for(int i = 0; i < 10; i++){ Li9_01 tt = new Li9_01(i+1);//构造属Li9_01类的线程tt,并向线程传递参数 tt.start( );//通过调用start( )方法 } } //main( ) } public class Li9_01 extends Thread { int count = 1, number; public Li9_01(int num) { //通过形参num接收参数传递 number = num; System.out.println("创建线程 " + number); } public void run( ) { //重新构造run( )方法 while(true) { 〖例9.1〗 扩展Thread类创建线程。本例通过扩展Thread类来创建线程的方法,类Li9_01声明为Thread的子类,具体创建方法可以用下面的代码来描述。
在例9.1中构造了一个称为tt的Li9_01类的线程,并在构造后通过循环调用了start()方法多次启动这个线程,这可以在运行后的结果中看出。需要注意的是,在Java中,Thread对象通过start()方法启动线程而不能直接调用run()方法,在调用start()方法后首先进行一些初始化,然后再调用run()方法启动线程。在例9.1中构造了一个称为tt的Li9_01类的线程,并在构造后通过循环调用了start()方法多次启动这个线程,这可以在运行后的结果中看出。需要注意的是,在Java中,Thread对象通过start()方法启动线程而不能直接调用run()方法,在调用start()方法后首先进行一些初始化,然后再调用run()方法启动线程。 • 由于main()线程(其实main()本身就可以看作是一个线程)和tt线程的调度情况是由操作系统动态决定的,因此编译并运行这个程序,每次运行的结果不一定相同。 • 这种方法简单明了,符合大家的习惯。但是它的缺点是:如果类已经从一个类继承,则无法再继承Thread 类,这时如果又不想建立一个新的类,应该怎么办呢?
在这里不妨来探索一种新的方法:不创建 Thread 类的子类,而是直接使用它,那么只能将方法作为参数传递给Thread类的实例,有点类似回调函数。但是 Java 没有指针,只能传递一个包含这个方法的类的实例。那么如何限制这个类必须包含这一方法呢?当然是使用接口!Java 提供了接口 java.lang. Runnable 来支持这种方法。
2.实现Runnable接口 • 利用Runnable接口创建和运行线程的编程步骤为: 第1步:定义一个Runnable接口的实现类,如MyThread,其内必须实现Runnable接口所声明的run( )方法。 定义Runnable接口的方法如下: public class yourThread implements Runnable { public void run(){ ... //需要以线程方式运行的代码 } }
第2步:创建一个Thread类的对象,即创建一个新线程,并用Runnable接口或者Thread类的引用变量指向它,调用Thread类带Runnable引用作为形参的构造方法,把Runnable接口实现类对象传递给Thread类的对象即传递给新线程,为新线程提供程序代码和数据。如:第2步:创建一个Thread类的对象,即创建一个新线程,并用Runnable接口或者Thread类的引用变量指向它,调用Thread类带Runnable引用作为形参的构造方法,把Runnable接口实现类对象传递给Thread类的对象即传递给新线程,为新线程提供程序代码和数据。如: yourThread yourt = new yourThread(); Thread tt = new Thread(yourt) ; 第3步:用线程对象调用start()方法启动线程。如: tt.start(); 定义一个实现了Runnable接口的类,将该类的对象作为Thread类的构造方法的参数,生成的Thread对象即为想要创建的线程,这样的线程同样通过start( )方法启动。
System.out.println("线程 " + number + ":计数 " + count); if(++count== 6) return; }} //run() public static void main(String args[]) { for(int i = 0; i < 10; i++) { Li9_02 yourt = new Li9_02(i+1); Thread tt = new Thread(yourt); tt.start(); //启动线程} } //main() } 〖例9.2〗将例9.1改为通过接口Runnable创建线程的实例。具体创建方法可以用下面的代码来描述。 //通过Runnable接口创建线程。Li9_02.java public class Li9_02 implements Runnable {//定义Runnable接口 int count= 1, number ; public Li9_02(int num) { number = num ; System.out.println("创建线程 " + number); } public void run() { //重新构造run方法 while(true) {
从以上创建线程的实例可以看出,构造线程体的两种方法各自的优缺点分析如下:从以上创建线程的实例可以看出,构造线程体的两种方法各自的优缺点分析如下: 1.使用Runnable接口 • 可以将CPU、代码和数据分开,形成清晰的模型; • 可以从其他类继承,当一个线程已继承了另一个类时,就只能用实现Runnable接口的方法来创建线程; • 便于保持程序风格的一致性。 2.扩展Thread类 • 不能再从其他类继承,适用于单继承线程情况; • 编写简单,可以直接操作线程; 由以上分析可知,两种方法各有利弊,读者应该根据实际情况灵活运用。
线程的状态与控制 • 在这里需要明确的是:无论采用扩展Thread类还是实现Runnable接口的方法来实现应用程序的多线程能力,都需要在该类中定义用于完成实际功能的run()方法,这个run()方法称为线程体(Thread body)。按照线程体在计算机系统内存中的状态,可以将线程从产生到灭亡分为新建、就绪、运行、挂起、死亡等5种状态。图9.3描述了线程的生命周期的几种状态和状态之间的转换。
新建状态:线程在已经利用new关键字创建但是还未执行的这段时间里,处于一种特殊的新建状态中,此时,线程对象已经被分配了内存空间,私有数据已经被初始化,但是该线程尚未被调度。此时的线程可以被调度,变成可运行状态,也可以被杀死,变成死亡状态。新建状态:线程在已经利用new关键字创建但是还未执行的这段时间里,处于一种特殊的新建状态中,此时,线程对象已经被分配了内存空间,私有数据已经被初始化,但是该线程尚未被调度。此时的线程可以被调度,变成可运行状态,也可以被杀死,变成死亡状态。 • 就绪状态:在处于创建状态的线程中调用start()方法将线程的状态转换为就绪状态。这时,线程已经得到除CPU时间之外的其它系统资源,只等JVM(Java虚拟机)的线程调度器按照线程的优先级对该线程进行调度,从而使该线程拥有能够获得CPU时间片的机会。
运行状态:运行状态表明线程正在运行,该线程已经拥有了对CPU的控制权。这个线程一直运行到运行完毕,除非该线程主动放弃CPU的控制权或者CPU的控制权被优先级更高的线程抢占。处在运行状态的线程在下列情况下将让出CPU的控制权:运行状态:运行状态表明线程正在运行,该线程已经拥有了对CPU的控制权。这个线程一直运行到运行完毕,除非该线程主动放弃CPU的控制权或者CPU的控制权被优先级更高的线程抢占。处在运行状态的线程在下列情况下将让出CPU的控制权: • 线程运行完毕; • 有比当前进程优先级更高的线程进入可运行状态; • 线程主动睡眠一段时间; • 线程在等待某一资源。 • 挂起状态:如果一个线程处于挂起状态,那么暂时这个线程无法进入就绪队列。处于挂起状态的线程通常需要由某些事件才能唤醒,至于由什么事件唤醒该线程,则取决于其挂起的原因。处于睡眠状态的线程必须被挂起一段固定的时间,当睡眠时间结束时就变成可运行状态;因等待资源或消息而被挂起的线程则需要由一个外来事件唤醒。 • 死亡状态:正常情况下run()返回使得线程死亡。调用stop()或destroy()亦有同样效果,但是不被推荐,因为前者会产生异常,后者是强制终止,不会释放内存。
到目前为止,我们仅运行了自动终止的线程,它们执行了一项任务,然后终止。但是如何控制Java线程的终止、如何停止一个线程?这就需要使用一些方法来控制线程。表9-1列出了控制线程执行的方法,可以使用表9-1中的方法创建线程并控制线程的执行。到目前为止,我们仅运行了自动终止的线程,它们执行了一项任务,然后终止。但是如何控制Java线程的终止、如何停止一个线程?这就需要使用一些方法来控制线程。表9-1列出了控制线程执行的方法,可以使用表9-1中的方法创建线程并控制线程的执行。 • 线程调用的意义在于JVM应对运行的多个线程进行系统级的协调,以避免多个线程争用有限资源而导致应用系统死机或者崩溃。 • Java定义了线程的优先级策略。Java将线程的优先级分为10个等级,分别用1~10之间的数字表示,数字越大表明线程的级别越高。相应地,在Thread类中定义了表示线程最低、最高和普通优先级的成员变量MIN_PRIORITY、MAX_PRIORITY和NORMAL_PRIORITY,代表的优先级等级分别为1、10和5。当一个线程对象被创建时,其默认的线程优先级是5。
为了控制线程的运行策略,Java定义了线程调度器来监控系统中处于就绪状态的所有线程。线程调度器按照线程的优先级决定哪个线程投入处理器运行。在多个线程处于就绪状态的条件下,具有高优先级的线程会在低优先级线程之前得到执行。线程调度器同样采用“抢占式”策略来调度线程执行,即当前线程执行过程中有较高优先级的线程进入就绪状态,则高优先级的线程立即被调度执行。具有相同优先级的所有线程采用轮转的方式来共同分配CPU时间片。为了控制线程的运行策略,Java定义了线程调度器来监控系统中处于就绪状态的所有线程。线程调度器按照线程的优先级决定哪个线程投入处理器运行。在多个线程处于就绪状态的条件下,具有高优先级的线程会在低优先级线程之前得到执行。线程调度器同样采用“抢占式”策略来调度线程执行,即当前线程执行过程中有较高优先级的线程进入就绪状态,则高优先级的线程立即被调度执行。具有相同优先级的所有线程采用轮转的方式来共同分配CPU时间片。 • 在应用程序中可以调用线程对象的setPriority()方法改变该线程的运行优先级,同样可以调用getPriority()方法获取当前线程的优先级。Java 支持10个优先级,基层操作系统支持的优先级可能要少得多,这样会造成一些混乱。因此,只能将优先级作为一种很粗略的工具使用,最后的控制可以通过使用yield()方法来完成。通常情况下,不要依靠线程优先级来控制线程的状态。
public void run ( ) { while (true) { System.out.println ("线程 " + number + ":计数 " + count); if(++count = = 6) return; } } //run() public static void main(String args[]) { for(int i = 0; i < 10; i++) { Li9_03 my = new Li9_03(i+1); Thread tt = new Thread(my); tt.start(); Thread.yield(); //该命令使线程自动释放处理器 } } } • yield()方法提供了一个处理时间分片问题的简单方法,如果使用了yield()方法,线程会自动释放处理器,这使得其它线程有机会获得处理器。 〖例9.3〗使用yield()方法重新改写前面例9.2的程序。 //Li9_03.java public class Li9_03 implements Runnable { int count = 1, number; public Li9_03 (int num) { number = num; System.out.println ("创建线程 " + number); }
9.3 线程的同步 线程同步的概念 • 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问,这套机制就是线程同步。 • 线程同步是指Java避免多个线程同时访问一个数据而造成数据混乱的方法。它可以避免多个线程同时访问相同的数据产生线程之间的争抢,避免一个线程刚生成的数据又会被其他线程生成的数据所覆盖。 • Java用监视器手段来完成线程的同步。就好像监视器把受保护的资源外面添加了一把锁,而这把锁只有一把钥匙。每一个线程只有在得到这把钥匙之后才可以对被保护的资源执行操作线程,而其它的线程只能等待,直到能拿到这把钥匙。 • Java使用关键字synchronized来实现多线程的同步,线程同步有两种实现方法,一种是方法同步,另一种是对象同步。
9.3 线程的同步 方法同步 • 一个类中任何方法都可以设计成为synchronized方法,以防止多线程数据崩溃。当一个线程进入synchronized方法后,能保证在其他任何线程访问这个方法之前完成自己的一次执行。如果一个线程试图访问一个已经启动的synchronized方法,则这个线程必须等待,直到已启动线程执行完毕,释放这个synchronized方法后才能访问。 • 通过在方法声明中加入 synchronized关键字来声明 synchronized 方法: public synchronized void methodName([parameterList]) { …… } • 下面将通过模拟一个银行帐号存取款的操作来看一下线程同步的重要性。
〖例9.4〗 模拟银行中的多个线程同时对同一个储蓄账户进行存款、取款操作。在主程序中我们首先生成了10个线程,然后启动它们,每一个线程都对同一账户进行存20元,然后马上又取出10元。这样,对于该账户来说,最终账户的余额应该是对原帐户存款增加1000元才对。 //多线程的同步举例。Li9_04.java public class Li9_04 implements Runnable { Account acc; public Li9_04(Account acc) { this.acc = acc; } public void run() { acc.deposit(20.0f); acc.withdraw(10.0f); } //run()
private static int NUM_OF_THREAD = 100; static Thread[] threads = new Thread[NUM_OF_THREAD]; //创建线程数组 public static void main(String[] args){ final Account acc = new Account("王红", 1000.0f); for (int i = 0; i< NUM_OF_THREAD; i++) { Li9_04 my = new Li9_04(acc); threads[i] = new Thread(my); //创建新线程 threads[i].start(); //运行线程 } for (int i=0; i<NUM_OF_THREAD; i++){ try { threads[i].join(); //等待所有线程运行结束 } catch (InterruptedException e) { } } System.out.println("完成,王红的帐户余额为:" + acc.getBalance()); } //main() }
class Account { String name; float amount; public Account(String name, float amount) { this.name = name; this.amount = amount; } public void deposit(float amt) { float tmp = amount; tmp += amt; try { Thread.sleep(1);//模拟其它处理所需要的时间,比如刷新数据库等 } catch (InterruptedException e) { } amount = tmp; } //deposit()
public void withdraw(float amt) { float tmp = amount; tmp -= amt; try { Thread.sleep(1);//模拟其它处理所需要的时间,比如刷新数据库等 } catch (InterruptedException e) { } amount = tmp; } //withdraw() public float getBalance() { return amount; } //getBalance() }
上面在类Account的deposit()和withdraw()方法中之所以要把对amount的运算使用一个临时变量首先存储,sleep(睡眠)一段时间,然后再赋值给amount,是为了模拟真实运行时的情况。因为在真实系统中,账户信息肯定是存储在持久媒介中,此处的睡眠时间相当于比较耗时的数据库操作,最后把临时变量tmp的值赋值给amount相当于把amount的改动写入数据库中。编译成功后,运行该程序某5次,结果如下:上面在类Account的deposit()和withdraw()方法中之所以要把对amount的运算使用一个临时变量首先存储,sleep(睡眠)一段时间,然后再赋值给amount,是为了模拟真实运行时的情况。因为在真实系统中,账户信息肯定是存储在持久媒介中,此处的睡眠时间相当于比较耗时的数据库操作,最后把临时变量tmp的值赋值给amount相当于把amount的改动写入数据库中。编译成功后,运行该程序某5次,结果如下: 完成,王红的帐户余额为:1160.0 完成,王红的帐户余额为:1040.0 完成,王红的帐户余额为:1100.0 完成,王红的帐户余额为:1100.0 完成,王红的帐户余额为:1120.0
读者在每次运行该程序时结果可能与上述结果也不相同,为什么会是这样呢?这是因为多线程中的不同步问题。在例9.4中,Account类中的amount变量会同时被多个线程所访问,它是一个竞争资源,通常称作竞态条件。对于这样的多个线程共享的资源在编写程序时必须进行同步,以避免一个线程的改动被另一个线程所覆盖。amount是一个竞态条件,所有对amount的修改访问的方法都要进行同步,所以应该将deposit()和withdraw()两个方法进行同步,这两个方法分别修改为:读者在每次运行该程序时结果可能与上述结果也不相同,为什么会是这样呢?这是因为多线程中的不同步问题。在例9.4中,Account类中的amount变量会同时被多个线程所访问,它是一个竞争资源,通常称作竞态条件。对于这样的多个线程共享的资源在编写程序时必须进行同步,以避免一个线程的改动被另一个线程所覆盖。amount是一个竞态条件,所有对amount的修改访问的方法都要进行同步,所以应该将deposit()和withdraw()两个方法进行同步,这两个方法分别修改为: public synchronized void deposit(float amt) public synchronized void withdraw(float amt)
修改后重新编译并运行该程序,每次运行结果都将得到王红银行最后存款余额为2000元,这样最终结果才是正确的。修改后重新编译并运行该程序,每次运行结果都将得到王红银行最后存款余额为2000元,这样最终结果才是正确的。 • 从本例可以看到,由于没使用synchronized关键字,线程A在对帐户操作的同时其它线程也在对帐户进行操作,而一个线程取得的帐户数据是其它线程没有提交之前的数据,所以最后线程在提交自己的数据时将会覆盖原来的数据,也就相当于只执行了其中部分线程。 • 在使用synchronized关键字后,将会使程序中的一个线程在调用方法同步的方法时,对该方法加锁,此时其它线程将等待,直到这个线程提交数据后另外一个线程才能得到方法该方法的钥匙,即该线程能够调用该方法,所以该线程此时取得的帐户余额是上一个线程提交后的帐户余额。这样程序才能得到一个正确结果,但这些都是对同步机制有所理解的前提下进行的。
对象同步 • synchronized关键字除了可以放在方法声明中表示整个方法为同步方法外,还可以放在对象前面限制一段代码,当某个对象用synchronized修饰时,表明该对象在任何一个时刻只能由一个线程访问。例如: synchronized(object) { //允许访问控制的代码 } 方法同步和对象同步的代码是可以相互等价转换的,例如: public synchronized void yourMethod() { //修饰方法 // ....... }
与下面对象同步的代码效果是一样的: public void yourMethod () { synchronized(this){ //修饰对象的引用 // ..... } } • 有时,一个方法执行时间很长,而其中只有很短的一段时间访问关键数据,在这种情况下,将整个方法声明为synchronized,将导致其他线程因无法调用该线程的synchronized方法进行操作而长时间无法继续执行,这在整体效率上是不划算的。此时,就可以使用对象同步,只把访问关键数据的代码段用花括号括起来,在其前加上synchronized(this)即可。 • 关于对象同步在这里就不再多讲,如有兴趣的读者,可以自己把上面的银行存取款的例子使用对象同步的方法进行修改。
同步方法的缺点 • 同步机制虽然很方便,但可能导致死锁。死锁是指发生在线程之间相互阻塞的现象,这种现象导致同步线程相互等待,以致每个线程都不能往下执行。在这种情况下,多个线程都在等待对方完成某个操作,从而产生死锁现象。 • 例如,一个线程持有对象X,另一个线程持有对象Y。第一个线程在拥有对象X,但必须拥有第二个线程所持有的对象Y才能执行;同样,第二个线程在拥有对象Y,但必须拥有第一个线程所持有的对象X才能执行,这样这两个线程就会无限期地阻塞,这时,线程就会出现死锁。在现实程序中,错误的同步往往会出现死锁,而且是较难发现的。这就像两个人只有一双筷子使用时,每个人拿到了一根筷子,而两个人却都想得到对方的筷子,这时就产生了死锁,两人都无法得到所需的资源。
为了防止死锁问题,在进行多线程程序设计时需要遵循如下原则:为了防止死锁问题,在进行多线程程序设计时需要遵循如下原则: • 在指定的任务真正需要并行时才采用多线程来进行程序设计; • 在对象的同步方法中需要调用其他同步方法时必须小心; • 在synchronized封装的块中的时间尽可能的短,需要长时间运行的任务尽量不要放在synchronized封装的同步块中。 • 另外,若将一个大的方法声明为synchronized 将会大大影响效率。典型地,如果一个方法执行时间很长,而其中只有很短的一段时间访问关键数据,在这种情况下,将整个方法声明为synchronized,将导致其他线程因无法调用该线程的其他synchronized方法进行操作而长时间无法继续执行,这将在很大程度上降低程序的运行效率。
9.4 线程组 线程组 • 线程组(Thread group)是包括了许多线程的对象集,线程组拥有一个名字以及与它相关的一些属性,可以用于作为一个组来管理其中的线程。线程组能够有效组织JVM的线程,并且可以提供一些组间的安全性。 • 在Java中,所有线程和线程组都隶属于一个线程组,可以是一个默认线程组,亦可是一个创建线程时明确指定的组。在创建之初,线程被限制到一个组里,而且不能改变到另外的组。若创建多个线程而不指定一个组,它们就会自动归属于系统线程组。这样,所有线程组和线程组成了一棵以系统线程组为根的树。如图9.4所示。
Thread类中提供了构造方法使创建线程时同时决定其线程组。本章第2节介绍了Thread类提供六种构造方法,前四种缺省了线程组,表示所创建的线程属于main线程组,后两种则指定了所创建线程的线程组。线程可以访问自己所在的线程组,但不能访问本线程组的父类。对线程组进行操作就是对线程组中的各个线程同时进行操作。Thread类中提供了构造方法使创建线程时同时决定其线程组。本章第2节介绍了Thread类提供六种构造方法,前四种缺省了线程组,表示所创建的线程属于main线程组,后两种则指定了所创建线程的线程组。线程可以访问自己所在的线程组,但不能访问本线程组的父类。对线程组进行操作就是对线程组中的各个线程同时进行操作。 • Java的ThreadGroup类提供了一些方法来方便我们对线程组树中的线程组和线程进行操作,比如可以通过调用线程组的相应方法来设置其中所有线程的优先级,也可以启动或阻塞其中的所有线程。
ThreadGroup类 • Java的线程组由包java.lang中的类ThreadGroup实现。在生成线程时,可以指定将线程放在某个线程组中,也可以由系统将它放在某个缺省的线程组中。通常,缺省的线程组就是生成该线程所在的线程组。但是,一旦某个线程加入某个线程组,它将一直是这个线程组的成员,而不能被移出这个线程组。 • 在创建线程之前,可以创建一个ThreadGroup对象,下面代码创建线程组并在其中加入两个线程: ThreadGroup yourThreadGroup = new ThreadGroup(”x”); Thread yourThread1 = new Thread(yourThreadGroup,”worker1”); Thread yourThread2 = new Thread(yourThreadGroup,”worker2”); yourThread1.start(); yourThread2.start();
如上例所示,首先创建一个线程组,然后创建两个线程,并传递给ThreadGroup对象,称为线程组中的成员。每一个线程可以各自调用start( )方法启动。ThreadGroup类并不提供一次启动所有线程的start()方法,但是可以通过调整线程组的优先级来挂起或者运行某个线程组中的所有线程。 • 类ThreadGroup提供了一些方法对线程组中的线程和子线程组进行操作,下面将对这些方法进行介绍。 • getName():返回线程组的名字。 • getParent():返回该线程的父线程组的名称。 • activeCount():返回线程组中当前激活的线程的数目,包括子线程组中的活动线程。
enumerate(Thread[ ]):将所有该线程组中激活的线程复制到一个特殊的数组中。 • setMaxPriority(int pri):设置线程组的最高优先级,pri是该线程组的新优先级,优先级从1~10依次增高。 • getMaxPriority(): 返回线程在线程组中拥有的最高优先级。 • getTheradGroup(): 返回线程组。 • interrupt(): 向线程组及其子组中的线程发送一个中断信息。 • setDaemon(booleam daemon): 将该线程设置为Daemon( 守护,即常驻内存)状态。 • isDaemon():判断是否是Daemon线程组。 • isDestroyed():判断线程组是否已经被销毁。
parentOf(ThreadGroup g):判断线程组是否是线程组g或g的子线程组。 • list():对每个Thread调用toString。 • toString():返回一个表示本线程组的字符串,包括它的名字和最高线程优先级。 • 另外,ThreadGroup类还提供了几个方法用来改变线程组中的所有线程的当前状态,如resume()、stop()、suspend(),但这些方法已不鼓励使用,对线程组的挂起或运行建议通过优先级的调整和让线程睡眠(sleep)来实现。 • 〖例9.5〗 利用线程组的各种方法演示线程的基本用法。
//演示线程的基本用法。Li9_05.java public class Li9_05{ public void test( ){ ThreadGroup tg = new ThreadGroup("test");//创建名称为test的tg线程组 Thread A = new Thread(tg,"A"); //创建线程,并设置该线程属于tg线程组 Thread B = new Thread(tg,"B"); Thread C = new Thread(tg,"C"); A.setPriority(6); //设置线程A的优先级为6 C.setPriority(4); //设置线程C的优先级为4,线程B默认为5 System.out.println("tg线程组正在活动的线程个数:"+tg.activeCount()); System.out.println("线程A的优先级是:"+A.getPriority());
tg.setMaxPriority(8); //设置线程组tg的优先级 System.out.println("tg线程组的优先级是:"+tg.getMaxPriority()); System.out.println("tg线程组上一级线程组信息:"+tg.getParent()); System.out.println("线程组名称:"+tg.getName()); System.out.print("tg线程组的信息:"); tg.list( ); //调用toString()方法,返回线程组的名称和优先级 } //test( ) public static void main(String argv[]){ Li9_05 ttg = new Li9_05( ); //构造函数ttg ttg.test( ); //运行函数ttg中的test方法 }