~ Java Mu1ti-threaded Mechanism 刘廷苍t董华彪。 Liu Tingcang Dong Huabiao (1.江西理工大学南昌校区信息工程系,江西南昌330013;2.沈阳理工大学应用技术学院,辽宁沈阳110168) (1.Faculty of Information Science and Engineering,Jiangxi University of Science and Technology。Jiangxi Nanchang 330013;2.Shenyang Ligong University,Liaoning Shenyang 110168) 摘要:由于在语言级提供了线程支持,Java语言中使用多线程要远比在C或C++中来得简单。本文主要从线程的基 本概念出发,对线程的生命周期、管理调度以及线程同步等方面的问题进行了阐述。 关键词:Java;多线程;同步 中图分类号:TP311 文献标识码:A 文章编号:1671-4792-(2008)12-0222-03 Abstract: Because provided the thread support in the language level,useing the mu1tithreading in the Java language is more simple than in C or C什.The thread basic conceptions.the thread life cycle,manage- ment aspects and thread synchronization are elaborated in this paper. Keywords:Java; Mul ti-threaded; Synchr0nization O程序、进程、线程的概念 程序是一组指令的有序集合,它本身没有任何运行的含 境结合在一起,提供了多任务并发执行的能力。这就好比一 个人在处理家务的过程中,将衣服放到洗衣机中自动洗涤, 将大米放在电饭锅里,然后开始做菜。等菜做好了,饭熟了同 时衣服也洗好了。 1线程的状态与生命周期 一义,它只是一个静态的实体。而进程则不同,它是程序在某个 数据集上的执行。进程是一个动态的实体,它有自己的生命 周期。它因创建而产生,因调度而运行,因等待资源或事件而 被处于等待状态,因完成任务而被撤消,反映了一个程序在 一个线程从创建、启动、运行到完毕称为一个线程的生 定的数据集上运行的全部动态过程。线程是进程的一个实 命周期。新建的线程在它的一个完整的生命周期中通常要经 历如下5种状态。 (1)新建状态 体,是CPU调度和分派的基本单位,它是比进程更小的能独 立运行的基本单位。线程本身基本上不拥有系统资源,但是 它可与同属一个进程的其他的线程共享进程所拥有的全部 资源。所以线程是比进程更小的执行单位,一个进程在其执 行过程中,可以产生多个线程,形成多条执行线索,每条线 当一个Thread类或其子类的对象被声明并创建时,新 生的线程对象处于新建状态,此时它已经有了相应的内存空 间和其他资源。 (2)就绪状态 索,即每个线程也有它自身的产生、存在和消亡的过程,也是 一个动态的概念。 线程创建之后仅仅是占有了内存资源,在JVM管理的线 程中还没有这个线程,此线程必须调用start 0方法(从父 Java的多个线程的执行是并发的,也就是在逻辑上“同 时”,而不管是否是物理上的“同时”。在操作系统每次分时给 Java程序一个时间片的CPU时间内,在若干个的可控 制的线程之间切换。如果系统只有一个CPU,那么真正的“同 时”是不可能的,但是由于CPU的速度非常快,用户感觉不到 其中的区别,因此,只需要设想各个线程是同时执行即可。线 程需要操作系统的支持,不是所有类型的计算机都支持多线 程应用程序。Java程序设计语言将线程支持与语言运行环 类继承的方法)通知JVM,这样JVM就会知道又有一个新线 程排队等侯切换了。 (3)运行状态 线程创建之后就具备了运行的条件,一旦轮到它来享用 CPU资源时,即JVM将CPU使用权切换给该线程时,此线程 就可以脱离创建它的主线程开始自己的生命周期了(即 run 0方法执行的过程,该方法规定了线程的具体使命)。 (4)阻塞状态 一‘ 间。JVM中的线程调度器负责管理线程,调度器把Java中线 程的优先级从低到高以整数1 1o表示,共分为1O级,设置 优先级是通过调用线程对象的setPriority 0方法实现的。 3实现多线程的方法 L 个正在运行的线程因某种原因不能继续运行时,进 人阻塞状态。有4种原因的阻塞:CPU资源从当前线程切换 给其他线程、执行了sleep(int millsecond)方法、执行了 wait 0方法、执行某个操作进入阻塞状态(如执行读/写操 作引起阻塞)。 (5)终止状态 ∞ < ∞ Java是面向对象的程序语言,用Java进行程序设计就 是设计和使用类,Java提供了线程类Thread来创建线程, 创建线程与创建普通类的对象操作是一样的,而线程就是 Thread类或其子类的实例对象。 多 线 程 机 制 处于死亡状态的线程不具有继续运行的能力。线程死亡 的原因有二:一个是正常运行的线程完成了它的全部工作, 即执行完了run0方法的最后一个语句并退出;另一个是线 程被提前强制性的终止,即强制run()方法结束。 图一线程基本状态转换 2线程调度与优先级 对于多线程程序,每个线程的重要程度不尽相同,如多 个线程在等待获得CPU时间时,往往需要优先级高的线程优 先抢占到CPU时间得以执行;又如多个线程交替执行时,优 先级决定了级别高的线程得到CPU的次数多一些且时间长 一些。这样,高优先级的线程处理任务的效率就高一些。 在Java技术中,线程通常是抢占式的而不需要时间片 分配进程(分配给每个线程相等的CPU时间的进程)。一个经 常犯的错误是认为“抢占”就是“分配时间片”。在Solaris平 台的运行环境中,相同优先级的线程不能相互抢占对方的 CPU时间。但是,在使用时间片的Windows平台运行环境中, 可以抢占相同甚至更高优先级线程的CPU时间。抢占并不是 绝对的,可是大多数JVM的实现结果在行为上表现出了严格 的抢占。纵观JVM的实现,并没有绝对的抢占或是时间片,而 是依赖于编码者对wait 0和sleep 0这两个方法的使用。 抢占式调度模型就是许多线程属于可以运行状态,但实 际上只有一个线程在运行。该线程一直运行到它终止进入可 运行状态或是另一个具有更高优先级的线程变成可运行状 态。在后一种情况下,低优先级的线程被高优先级的线程抢 占,高优先级的线程获得运行的机会。 线程可以因为各种各样的原因进人阻塞状态。例如,线 程的代码可以在适当时候执行Thread.sleep0方法,故意 让线程中止;线程可能为了访问资源而不得不等待直到该资 源可用为止。所有可运行的线程根据优先级保持在不同的池 中,一旦被阻塞的线程返回可运行状态,它将会被放回适当 的可运行池中。非空最高优先级池中的线程将获得CPU时 Thread thread l=new Thread(); //声明一个对象实例,即创建一个线程 thread1.run0; //用Thread类中的run0方法启动线程 从第一句看到,可以通过Thread 0构造方法创建一个 线程,并启动该线程。事实上,启动线程,也就是启动线程的 run0方法,而Thread类中的run0方法没有任何操作语 句,所以这个线程没有任何操作。要使线程实现预定功能,必 须定义自己的run 0方法。Java中通常有两种方式定义run ()方法: (1)继承Thread类,覆盖方法run0 在创建的Thread类的子类中重写run(),加入线程所 要执行的代码即可。这种方法简单明了,符合大家的习惯,但 是,它也有一个很大的缺点,那就是如果类已经从一个类继 承(如小程序必须继承自Applet类),则无法再继承 Thread类,这时如果又不想建立一个新的类,应该如何处 理。 不妨来探索一种新的方法:如果不创建Thread类的子 类,而是直接使用它,那么只能将方法作为参数传递给 Thread类的实例,有点类似回调函数。但是Java没有指 针,只能传递一个包含这个方法的类的实例。如何这个 类必须包含这一方法,当然是使用接口(虽然抽象类也可满 足,但是需要继承,而之所以要采用这种新方法,就是为了避 免继承带来的)。Java提供了接口java.1ang. Runnable来支持这种方法。 (2)实现Runnable接口 Runnable接口只有一个方法run(),声明自己的类实 现Runnable接口并提供这一方法,将线程代码写入其中, 就完成了这一部分的任务。但是Runnable接口并没有任何 对线程的支持,还必须创建Thread类的实例,这一点通过 Thread类的构造函数来实现。 如果是创建Thread类的实例,必须注意的是,该子类必 须没有覆盖Threa类的run 0方法,否则该线程执行的将是 子类的run0方法,而不是用以实现Runnable接口的类的 run0方法。 使用Runnable接口来实现多线程使得我们能够在一个 类中包容所有的代码,有利于封装。它的缺点在于,只能使用 一套代码,若想创建多个线程并使各个线程执行不同的代 ~ 码,则仍必须额外创建类。如果这样的话,在大多数情况下也 许还不如直接用多个类分别继承Thread来得紧凑。 4线程的同步 由于同一进程的多个线程共享同一片存储空间,在带来 方便的同时,也带来了访问冲突这个严重的问题。Java语言 提供了专门机制以解决这种冲突,有效避免了同一个数据对 象被多个线程同时访问。 由于可以通过private关键字来保证数据对象只能被 方法访问,只需针对方法提出一套机制,这套机制就是syn- chronized关键字,它包括两种用法:synchronized方法和 synchronized块 (1)synchronized方法 通过在方法声明中加入synchronized关键字来声明 synchronized方法。i ̄rl: public synchronized void accessYal(int newVa1); synchronized方法控制对类成员变量的访问:每个类 实例对应一把锁,每个synchronized方法都必须获得调用 该方法的类实例的锁方能执行,否则所属线程阻塞。方法一 旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后 被阻塞的线程方能获得该锁,重新进入可执行状态。这种机 制确保了同一时刻对于每一个类实例,其所有声明为syn- chronized的成员函数中至多只有一个处于可执行状态(因 为至多只有一个能够获得该类实例对应的锁),从而有效避 免了类成员变量的访问冲突(只要所有可能访问类成员变量 的方法均被声明为synchronized)。 在Java中,不光是类实例,每一个类也对应一把锁,这 样也可将类的静态成员函数声明为synchronized,以控制 其对类的静态成员变量的访问。synchronized方法的缺陷: 若将一个大的方法声明为synchronized,将会大大影响效 率。若将线程类的方法run 0声明为synchronized,由于在 线程的整个生命期内它一直在运行,因此将导致它对本类任 何synchronized方法的调用都永远不会成功。当然可以通 过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized,并在主方法中调用来解决这一问题。但是, Java提供了更好的解决办法,那就是synchronized块。 (2)synchronized块 通过synchronized关键字来声明synchronized块。 语法如下: synchronized(syncObdect){ //允许访问控制的代码 ) synchronized块是这样一个代码块,其中的代码必须 获得对象syncObdect(如前所述,可以是类实例或类)的锁 方能执行,具体机制同前所述。由于可以针对任意代码块,且 可任意指定上锁的对象,故灵活性较高。 5结束语 由于Java的多线程功能齐全,各种情况面面具到,它带 来的好处也是显然易见的。多线程带来的更大的好处是更好 的交互性能和实时控制性能。当然实时控制性能还取决于系 统本身(UNIX、Windows、Macintosh等),在开发难易程度和 性能上都比单线程要好。当然一个好的程序设计语言肯定也 难免有不足之处。由于多线程还没有充分利用基本Os的这 一功能,对于不同的系统,上面的程序可能会出现截然不同 的结果,这使编程者偶会感到迷惑不解。希望在不久的将来 Java的多线程能充分利用操作系统,减少对编程者的困惑。 参考文献 【1】耿祥义,张跃平.Java2实用教程(第三版)[M].北 京:清华大学出版社,2006。8. [2]洪维恩,何嘉.Java2面向对象程序设计[M].北京: 中国铁道出版社,2005,5. 作者简介 刘廷苍(197 ),江西南昌人,硕士,主要研究方向:数 据仓库和数据挖掘。