代码改变世界

Java多线程

2012-12-16 21:25  lefan  阅读(436)  评论(0编辑  收藏  举报

正在跟着视频学J2SE,写完聊天程序,并运行成功,很是高兴。对代码也少了些恐惧。把刚学完的多线程总结下,材料取自网上,当学习记录用。

一、Java 多线程机制

     线程是程序中完成任务的从头到尾的执行, 任务就是一个独立于程序其他部分执行单元。多线程是指同时存在几个执行单元,也就是运行多个任务的能力,主要是为了提高运算速度。例如文件处理系统允许在输入文字的同时,打印或者保存文件。Java 是通过多线程运行机制来支持多任务和并行处理的,也提供了锁定资源来避免冲突。Java 语言的多线程机制是怎么样的呢?

(一)Java 中线程的实现

在Java 程序中可以通过两种方法实现:1.对1Thread 类的继承派生一个子类并重写run方法,然后生成该子类的对象。来实现多线程。2.直接定义一个接口Runnable,Runnable中只有一个方法,用以定义线程运行体,然后再由这个类生成对象来实现,Runable可以为多个接口提供共享的数据。一般推荐使用第一种方法实现。

(二)线程的优先级

Java提供了一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应该调度哪个线程来执行。在多线程系统中,每个线程都被赋予一个执行优先级。优先级决定了线程被CPU 执行的优先秩序。优先级高的线程可以在一段的时间内获得比优先级低的线程更多的执行时间。这样好像制造了不平等,但是却带来了高效率。如果线程的优先级完全相等,就按照“先来先用”的原则进行调度。每个JAVA 线程都有一个优先级,范围在

Thread.MIN_PRIORITY=1,Thread.MAX_PRIORITY=10。

默认情况下,每个线程的优先级都为Thread.N0RM_PR10RITY= 5。每个新线程都继承其父线程的优先级。使用setPriority 和getPriority方法来设置和读取线程的优先级。

(三)线程的调度机制

     多个线程的并发执行,实际上是通过一个调度程序来进行调度的。调度就是指在各个线程之间分配CPU 资源。线程调度有两种模型:抢占式模型和分时模型。在分时模型中,CPU 资源是按照时间片来分配的。获得CPU 资源的线程只能在指定的时间片内执行,一旦时间片使用完毕,就必须把CPU 让给另一个处于就绪状态的线程。但是线程本身不会让出CPU 的。在抢占式模型中,如果在一个低优先级的线程的执行过程中,又有一个高优先级的线程准备就绪,那么低优先级的线程就把CPU 资源让给高优先级的线程。Java 支持的就是抢占式调度模型。因此,为了使低优先级的线程有机会运行,高优先级的线程应该不时主动进入“sleep”状态。

(四)线程间的同步

一个Java 程序的多线程之间可以共享数据,这就产生了同步的问题。假如两个线程A 和B 同时访问同一个数据对象,线程A 读这个数据对象,而线程B 写这个数据对象,或者两个线程同时改写了这个数据对象,就会导致诸如一致性、数据丢失等问题。这些问题在一些实际应用中如银行系统、电脑订票系统中尤其致命。

Java 提供了一套同步化的机制,其基本思想就是避免多个线程访问同一个资源。在java语言中,引入了对象互斥锁的概念,保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”的标记,这个标记保证在任一时刻,只能有一个线程访问该对象。JAVA 使用关键字synchronized 来与对象的互斥锁联系,当某个对象被synchronized修饰时,表明该对象在任一时刻只能由一个线程访问。

(1)同步整个方法

可以在方法的声明中使用synchronized 关键字来对该方法中的所有代码进行同步,如:

public synchronized void method(){⋯}

(2)同步一段代码块

如果只对方法中访问共享资源的代码块进行同步,则需要将这段代码放入一个synchronized 块中,如:synchronized(someobject){⋯}。当第一个线程占有了以某个对象someobject 为标记的锁,其它需要进入这段代码块的线程将被放入以这个对象为标记的锁池中,等待获得锁的机会。

(3)释放锁

由于等待一个锁的线程在得到锁之前不能恢复运行。所以让持有锁的线程在不再需要的时候及时释放锁是很重要的。持有锁的线程执行到synchronized代码块末尾时将释放锁。如果线程执行到同步代码块时出现中断或异常而跳出synchronized代码块,锁也会自动释放。此外,还可以使用wait()方法显示释放锁。

(五)线程的阻塞

    Java 引入了同步机制,当多个线程对共享资源的访问,同步机制已经不够了,因为在任意时刻所要求的资源不一定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个。为了解决这种情况下的访问控制问题,Java 引入了对阻塞机制的支持。

    阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。Java 提供了大量方法来支持阻塞,下面让对它们逐一分析。

    1. sleep方法:sleep允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。

    典型地,sleep被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。

    2. suspend和resume方法:两个方法配套使用,suspend使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume 被调用,才能使得线程重新进入可执行状态。典型地,suspend和 resume被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用resume使其恢复。

    3. yield 方法:yield 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。

    4. wait和 notify方法:两个方法配套使用,wai使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify被调用。

    2和4区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。上述的核心区别导致了一系列的细节上的区别。

    首先,前面叙述的所有方法都隶属于Thread 类,但是这一对却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait方法导致线程阻塞,并且该对象上的锁被释放。而调用任意对象的notify方法则导致因调用该对象的 wait方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。

    其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现异常。

    wait和 notify方法的上述特性决定了它们经常和synchronized 方法或块一起使用,将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchronized方法或块提供了类似于操作系统原语的功能,它们的结合用于解决各种复杂的线程间通信问题。

    关于 wait 和 notify方法最后再说明两点:

    第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。

    第二:除了 notify(),还有一个方法 notifyAll也可起到类似作用,唯一的区别在于,调用 notifyAll方法将把因调用该对象的wait方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。

    谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend方法和不指定超时期限的 wait 方法的调用都可能产生死锁。遗憾的是,Java 并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。

   (六)线程的生命周期

Java 的线程从产生到灭亡,有以下几个状态。

(1)新建状态(New):创建了线程类子类的实例并初始化后,该对象就处于新建状态,此时有了相应的存储空间和相应进程的资源。

(2)就绪状态或可运行状态(Runnable):处于新建状态的线程被启动后,就处于就绪状态,即进入就绪队列等待CPU 时间片的到来。此时已具备了运行的条件。至于该线程何时才被真正执行,则取决于线程的优先级和就绪队列的当前状态。

(3)挂起状态(Blocked):正在运行或处于就绪状态的线程由于某种原因,让出CPU 并暂时终止自己的执行,即进入挂起状态,只有当被挂起的原因被解除时方可转入就绪或运行状态。

(4)终止状态(Dead):一个线程完成了其所有操作或被提前强行结束时即终止,此时线程不能再被恢复和执行。

图1 线程的状态 

 二、多线程应用实例

下面的实例利用Java 的多线程功能实现简单的动画程序,程序功能是:用按钮Start 控制小球的出现,当画面上出现新的小球,即按照一定的路线移动,而且小球移动的速度是相同的,看起来就好像是一些气泡在画面上飘动。窗口的布局设置采用Java 绘制图形的相关类,

这里重点介绍线程类Bubble。代码如下:

class Bubble extends Thread{
    MainPanel mainpanel;
    Private static final int XSIZE=10;
    Private static final int YSIZE=10;
    private int x=0, y=0, dx=2, dy=2; //x,y 表示小球中心点的坐标
    public Bubble(MainPanel b) { mainpanel=b; }
    public void draw(){
    Graphics g=mainpanel.getGraphics();
    g.fillOval(x,y,XSIZE,YSIZE);
    g.dispose();
}

public void move(){
    if(!mainpanel.isVisable()) return;   
    x+=dx; y+=dy;
    Dimension d=mainpanel.getSize();
    if(x<0) {x=0; dx=-dx;} if(y<0) {y=0; dy=-dy;}
    if (x+XSIZE>=d.width) {x=d.width-XSIZE;
    dx=-dx;}
    if (y+YSIZE>=d.height) {y=d.height-YSIZE;
    dy=-dy;}
    mainpanel.repaint();
}

public void run(){
    try{
        draw();
        for(int i=0; i<=1000;i++){
            move(); sleep(50);
            }
            }catch(Exception e) {
        }
    }
}

在这个类的定义中,通过基础Thread 类实现线程类。继承Thread 类时实现了run()方法,它是一个线程的入口点,线程运行时执行的动作都是由这个方法决定的。在run()方法中首先调用了draw()方法,它完成了小球图形的绘制。其中执行了一个1000

次的循环,再每次循环过程中,通过move()方法改变小球中心点的坐标值来实现了小球的移动。之后调用了一个sleep(50)方法,使当前线程休眠50 毫秒,即将线程从运行状态转变到挂起状态,CPU 被空出来供其它的线程完成自己的动作。Java 提供线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应调度哪些线程来执行。在上面的程序中调用sleep()方法就是主动放弃CPU,除了这种方法,还可以通过使用suspend()来实现放弃CPU。但二者有些不同。调用sleep()方法时指定了线程的休眠时间,当时间结束线程自动从挂起状态回到运行状态。而对于suspend()方法,可以由线程自身调用来暂停自己,也可以由其他线程调用暂停其执行。但是要恢复由suspend()方法挂起的线程,就只能由其它线程来调用resume()方法。

另外,除了主动放弃CPU 外,还有以下四种情

况可以使线程进入挂起状态[3]:

(1)由于当前线程进行I/O 访问,外存读写,等

待用户输入等操作,导致线程阻塞。

(2)为等候一个条件变量,线程调用wait()方法。

(3)抢占式系统下,有高优先级的线程参与调度。

(4)时间片方式下,当前时间片用完,有同优先

级的线程参与调度。

在上面的Bubble 类中用到了MainPanel 类来

作为其构造函数的参数,设计代码如下:

class MainPanel extends Panel{
  public Vector Bubbles;
  synchronized public void paint(Graphics g){
  paintBubbles();
}
public void paintBubbles(){
  Enumeration bubbleCollection = bubbles.elements();
  while(bubbleCollection.hasMoreElements()){
  Bubble bubble= (Bubble)bubbleCollection;
  nextElement();   bubble.draw();   }
} 

   由于在Bubble 类的move () 方法中调用了MainPanel 类的repaint()方法,因此小球的位置一旦改变,MainPanel 类进行的repaint 操作就会调用paint 方法,进而调用paintBubbles()方法,将所有的小球重新绘制一遍,进而实现了小球的移动。在paint () 方法的定义中使用了一个关键字synchronized,将paint()方法声明为同步化,主要是为了解决多线程中的资源冲突问题。由于每个线程对象都要调用repaint()方法来完成自己的移动,因此必须对这个共享资源加锁。

三、线程实现方法的比较

线程的建立还可以通过实现Ruannable 接口来完成。具体方法是:提供一个实现接口Ruannable 的类作为线程的目标对象,在初始化一个Thread 类或Thread 子类的线程对象时,把目标对象传递给这个线程实例,由该目标对象提供线程体run()。对于这种方法和直接继承Thread 类的方法进行简单比较可以得出:

(1)使用Runnable 接口可以将CPU、代码和数据分开,形成清晰的模型;还可以从其它类继承;有利于保持程序风格的一致性。

(2)直接继承Thread 类不能再从其它类继承,但这种方法编写简单,可以直接操纵线程,无需使用Thread.currentThread()。

 

附:进程与线程

 

 

[1] 章英 Java 多线程机制探讨及实践

[2] 图1:http://dingchaoqun12.blog.163.com/blog/static/116062504201042011715171/