多线程编程

1.1 进程与线程

   Java对多线程编程(multithreaded programming)提供了内置支持。多线程程序包含同时运行的两个或更多个部分。这种程序的每一部分被称为一个线程,并且每个线程定义了单独的执行路径。因此,多线程是特殊形式的多任务处理。
   所有现代操作系统都支持多任务处理。但是,多任务处理有两种不同的类型:基于进程的多任务处理和基于线程的多任务处理。进程本质上是正在执行的程序。因此,基于进程的多任务处理就是允许计算机同时运行两个或更多个程序的特性。例如,基于进程的多任务处理可以运行Java编译器的同时使用文本编辑器或浏览网站。在基于进程的多任务处理中,程序是调度程序能够调度的最小代码单元。
   在基于线程的多任务环境中,最小的可调度代码单元是线程,这意味着单个程序可以同时执行两个或更多个任务。例如,文本编辑器可以在打印的同时格式化文本,只要这两个动作是通过两个独立的线程执行即可。因此,基于进程的多任务处理“大局”,而基于线程的多任务处理“细节”。
   多任务线程需要的开销比多任务进程小。进程是重量级的任务,它们需要自己的地址空间。进程间通信开销很大并且有许多限制。从一个进程上下文切换到另一个进程上下文的开销也很大。另一方面,线程是轻量级的任务。它们共享相同的地址空间,并且合作共享同一重量级的进程。线程间通信的开销不大,并且从一个线程上下文切换到另一个线程上下文开销更小。虽然Java程序使用基于多进程的多任务环境,但是基于多进程的多任务处理不是由Java控制的。不过,基于多线程的多任务处理是由Java控制的。
   使用多线程可以编写出更加高效的程序,以最大限度地利用系统提供的处理功能。多线程实现最大限度利用系统功能的一种重要方式是使空闲空间保持最少。对于交互网络环境中的Java操作这很重要,因为对于这种情况空闲时间很普遍。例如,网络上数据的传输速率比计算机能够处理的速率低很多。即使是读写本地系统资源,速度也比CPU的处理速率慢很多。并且,用户输入速度当然也比计算机的处理速率低很多。在单线程环境中,程序在处理这些任务中的下一个任务之前必须等待当前任务完成——尽管在等待输入时,程序在大部分时间是空闲的。多线程有助于减少空闲时间,因为当等待输入时可以运行另一个线程。

1.2 Java 线程模型

   Java运行时系统在许多方面依赖于线程,并且所有类库在设计时都考虑了多线程。事实上,Java通过利用线程使得整个环境能够异步执行。这有助于通过防止浪费CPU时钟周期来提高效率。
   通过与单线程环境进行比较,可以更好地理解多线程环境的价值。单线程系统使用一种称为轮询事件循环(event loop with polling)的方法。在这种模型中,单个线程在一个无限循环中控制运行,轮询一个事件队列以决定下一步做什么。一旦轮询返回一个信号,比如准备读取网络文件的信号,事件循环就将控制调度至适当的事件处理程序。在这个事件处理程序返回之前,程序不能执行任何其他工作。这浪费了CPU时间,并且会导致程序的一部分支配着系统而阻止对所有其他部分进行处理。通常,在单线程环境中,当线程因为等待某些资源而阻塞(即挂起执行)时,整个程序会停止运行。
   Java多线程的优点消除了主循环/轮询机制。可以暂停一个线程而不会停止程序的其他部分。例如,由于线程从网络读取数据或等待用户输入而造成的空闲时间,可以在其他地方得以利用。多线程允许当前激活的循环在两帧之间休眠,而不会造成整个系统暂停。当Java程序中的线程阻塞时,只有被阻塞的线程会暂停,所有其他线程仍将继续运行。
   Java的多线程系统在单核多核这两种类型的系统中都可以工作。在单核系统中并发执行的线程共享CPU,每个线程得到一片CPU时钟周期。所以,在单核系统中,两个或更多个线程不是真正同时运行的,但是空闲时间被利用了。然而,在多核系统中,两个或更多个线程可能是真正同步执行的。在许多情况下,这会进一步提高程序的效率并提高特定操作的速度。
   线程有多种状态,下面是一般描述。线程可以处于运行(running)状态,只要获得CPU时间就准备运行。运行的线程可以被挂起(suspended),这会临时停止线程的活动。挂起的线程可以被恢复(resumed),从而允许线程从停止处恢复执行。当等待资源时,线程会被阻塞(blocked)。在任何时候,都可以终止线程,这会立即停止线程的执行。线程一旦终止,就不能再恢复。

1.2.1 线程优先级

   Java为每个线程都指定了优先级,优先级决定了相对于其他线程应当如何处理某个线程。线程优先级是一些整数,它们指定了一个线程相对于另一个线程的优先程度。优先级的绝对值没有意义;如果只有一个线程在运行,优先级高的线程不会比优先级低的线程运行快。反而,线程的优先级用于决定何时从一个运行的线程切换到下一个,这称为上下文切换(context switch)。决定上下文切换发生时机的规则比较简单:

  • 线程自愿地放弃控制。线程显示地放弃控制权、休眠或在I/O之前阻塞,都会出现这种情况。在这种情况下,检查所有其他线程,并且准备运行的线程中优先级最高的那个线程会获得CPU资源。
  • 线程被优先级更高的线程取代。对于这种情况,没有放弃控制权的低优先级线程不管在做什么,都会被高优先级线程简单地取代。基本上,只要高优先级线程希望运行,它就会取代低优先级线程,这称为抢占式多任务处理(preemptive multitasking)。
       如果具有相同优先级的两个线程竞争CPU资源,这种情况有些复杂。对于Windows这类操作系统,优先级相等的线程以循环方式自动获取CPU资源。对于其他类型的操作系统,优先级相等的线程必须自愿地向其他线程放弃控制权,否则其他线程就不能运行。
    警告:操作系统以不同的方式对具有相等优先级的线程进行上下文切换,可能会引起可移植型问题。

1.2.1 同步

   因为多线程为程序引入了异步行为,所以必须提供一种在需要时强制同步的方法。例如,如果希望两个线程进行通信并共享某个复杂的数据结构,如链表,就需要以某种方式确保它们相互之间不会发生冲突。也就是说,当一个线程正在读取该数据结构时,必须阻止另外一个线程向该数据结构写入数据。为此,Java以监视器这一年代久远的进程间同步模型为基础,实现了一种巧妙的方案。可以将监视器看做非常小的只能包含一个线程的盒子。一旦某个线程进入监视器,其他所有线程就必须等待,直到该线程退出监视器。通过这种方式,可以将监视器用于保护共享的资源,以防止多个线程同时对资源进行操作。
   Java没有提供"Monitor"类;相反,每个对象都有自己的隐式监视器。如果调用对象的同步方法,就会自动进入对象的隐式监视器。一旦某个线程位于一个同步方法中,其他线程就不能调用同一对象的任何其他方法。因为语言本身内置了同步支持,所以可以编写出非常清晰并且简明的多线程代码。

1.2.3 消息传递

   将程序分隔到独立的线程之后,需要定义它们之间相互通信的方式。当使用某些其他语言编写程序时,必须依赖操作系统建立线程之间的通信。当然,这会增加系统开销。相反,通过调用所有对象都具有的预先定义的方法,Java为两个或更多个线程之间的相互通信提供了一种简洁的低成本方式。Java的消息传递系统允许某个线程进入对象的同步方法,然后进行等待,直到其他线程显示地通知这个线程退出为止。

1.2.3 Thread 类和 Runnable接口

   Java的多线程系统是基于Thread类、Thread类的方法及其伴随接口Runnable而构建的。Thread类封装了线程的执行。因为不能直接引用正在运行的线程的细微状态,所以需要通过代理进行处理,Thread实例就是线程的代理。为了创建新线程,程序可以扩展Thread类或实现Runnable接口。
   Thread类定义了一些用于帮助管理线程的方法,表1-2中显示了本篇所用到的几个方法。

表 1-2 Thread 类定义的一些方法
方法含义
getName()获取线程的名称
getPriority()获取线程的优先级
isAlive()确定线程是否仍然在运行
join等待线程终止
run()线程的入口点
sleep()挂起线程一段时间
start()通过调用线程run()方法启动线程

1.3 主线程

   当Java程序启动时,会立即开始运行一个线程,因为它是程序开始时执行的线程,所以这个线程通常称为程序的主线程。主线程很重要,有以下两个原因:

  • 其他子线程都是从主线程产生的
  • 通常,主线程必须是最后才结束执行的线程,因为它要执行各种关闭动作。
       尽管主线程是在程序启动时自动创建的,但是可以通过Thread对象对其进行控制。为此,必须调用currentThread()方法获取对主线程的一个引用。该方法是Thread类的公有静态成员,它的一般形式如下所示:
static Thread currentThread()

   这个方法返回对调用它的线程的引用。一旦得到对主线程的引用,就可以像控制其他线程那样控制主线程了。
   首先分析下面这个例子:

//Controlling the main Thread.
class CurrentThreadDomo {
    public static void main(String args[]) {
        Thread t = Thread.currentThread();
        System.out.println("Current thread: " + t);
        //change the name of the thread
        t.setName("My Thread");
        System.out.println("After name change: " + t);
        try {
            for (int n = 5; n > 0; n--) {
                System.out.println(n);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted");
        }
        /*
        运行结果:
        Current thread: Thread[main,5,main]
        After name change: Thread[My Thread,5,main]
        5
        4
        3
        2
        1
         */
    }
}

   在这个程序中,通过调用currentThread()方法来获取对当前线程(在本例中是主线程)的引用,并将这个引用存储在局部变量t中。接下来,程序显示有关线程的信息。然后程序调用setName()方法更改线程的内部名称。然后再次显示有关线程的信息。在从5递减的循环中通过sleep()方法实现两次循环之间暂停1秒。传递给sleep()方法的参数以毫秒为单位指定延迟的间隔时间。请注意封装循环的try/catch代码块。Thread类的sleep()方法可能会抛出InterruptedException异常。如果其他线程试图中断这个正在睡眠的线程,就会发生这种情况。
   注意将t用作println()方法的参数时产生的输出,这将依次显示线程的名称、优先级以及线程所属线程组的名称。默认情况下,主线程的名称是main,优先级是5,这是默认值,并且main也是主线程所属线程组的名称。线程组(thread group)是将一类线程作为整体类线程作为整体来控制状态的数据结构。在更改了线程的名称后,再次输出t,这一次将显示线程新的名称。
   下面进一步分析在程序中使用的Thread类定义的方法。sleep()方法使线程从调用时挂起,暂缓执行指定的时间间隔(毫秒数),它的一般形式如下所示:

static void sleep(long milliseconds) throws InterruptedException

   挂起的毫秒数由milliseconds指定,这个方法可能会抛出InterruptedException异常。
   sleep()方法还有第二种形式,如下所示,这种形式允许按照毫秒加纳秒的形式指定挂起的时间间隔:

static void sleep(long milliseconds,int nanoseconds) throws InterruptedException

   只有在计时周期精确到纳秒级的环境中,sleep()方法的第二种形式才有用。
   正如前面的程序所示,使用setName()方法可以设置线程的名字,它和getName(获取线程的名字),都是Thread类的成员,它们的声明如下所示:

final void setName(String threadName)
final void getName()

1.4 创建线程

   在最通常的情况下,通过实例化Thread类型的对象创建线程。Java定义了创建线程的两种方法:

  • 实现Runnable接口
  • 扩展Thread类本身

1.4.1 实现 Runnable 接口

   创建线程的最简单方式是创建实现了Runnable接口的类。Runnable接口抽象了一个可执行代码单元。可以依托任何实现了Runnable接口的对象来创建线程。为了实现Runnable接口,类只需要实现run()方法,该方法的声明如下所示:

public void run()

   在run方法内部,定义组成新线程的代码。run()方法可以调用其他方法,使用其他类,也可以声明变量,就像main线程那样。唯一的区别是:run()方法为程序中另外一个并发线程的执行建立了入口。当run()方法返回时,这个线程将结束。
   在创建实现了Runnable接口的类之后,可以在类中实例化Thread类型的对象。Thread类定义了几个构造函数。我们将使用的那个构造函数如下所示:

Thread(Runnable threadOb,String threadName)

   在这个构造函数中,threadOb是实现了Runnable接口的类的实例,这定义了从何处开始执行线程。新线程的名称由threadName指定。
   在创建了新线程之后,只有调用线程的strat()方法,线程才会运行,该方法是在Thread类中声明的。本质上,start()方法执行对run()方法的调用。start()方法的声明如下所示:

void start();

   下面的例子创建了一个新的线程并开始运行:

//Create a second thread.
class NewThread implements Runnable {
    Thread t;

    NewThread() {
        //Create a new,second thread
        t = new Thread(this, "Demo Thread");
        System.out.println("Child thread: " + t);
        t.start();//Start the thread
    }

    //This is the entry point for the second thread
    public void run() {
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println("Child Thread: " + i);
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            System.out.println("Child interrupted.");
        }
        System.out.println("Exiting child thread.");
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        new NewThread();//create a new thread
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println("Main thread: " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("Main thread exiting.");
    }
}

   在NewThread类的构造函数中,通过下面这条语句创建了一个新的Thread对象:

t = new Thread(this,"Demo Thread");

   传递this作为第一个参数,以表明希望新线程调用this对象的run()方法。接下来调用start()方法,从run()方法开始启动线程的执行。这会导致开始执行子线程的for循环。调用完start()方法之后,NewThread类的构造函数返回到main()方法。当恢复主线程时,会进入主线程的for循环。两个线程继续运行,在单核系统中它们会共享CPU,直到它们的循环结束。这个程序生成的输出如下所示(基于特定的执行环境,输出可能有所变化):

Child thread: Thread[Demo Thread,5,main]
Main thread: 5
Child Thread: 5
Child Thread: 4
Main thread: 4
Child Thread: 3
Child Thread: 2
Main thread: 3
Child Thread: 1
Exiting child thread.
Main thread: 2
Main thread: 1
Main thread exiting.

   如前所述,在多线程程序中,主线程通常必须在最后结束运行。事实上,对于某些旧的JVM,如果主线程在子线程完成之前结束,Java运行时系统可能会“挂起”。上面的程序确保主线程在最后结束,因为主线程在每次迭代之间休眠1000毫秒,而子线程只休眠500毫秒。这使得子线程比主线程终止得更早。下面会介绍更好的等待线程结束的方法。

1.4.2 扩展 Thread 类

   创建线程的第二种方式是创建一个扩展了Thread的新类,然后创建该类的实例。扩展类必须重写run()方法,run()方法是新线程的入口点。扩展类还必须调用start()方法以开始新线程的执行。下面的程序对前面的程序进行了改写以扩展Thread类。

//Create a second thread by extending Thread
class NewThread extends Thread {
    NewThread() {
        //Create a new,second thread
        super("Demo Thread");
        System.out.println("Child thread: " + this);
        start();//Start the thread
    }

    //This is the entry point for the second thread
    public void run() {
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println("Child Thread: " + i);
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            System.out.println("Child interrupted.");
        }
        System.out.println("Exiting child thread.");
    }
}

class ExtendThread {
    public static void main(String[] args) {
        new NewThread();//create a new thread
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println("Main thread: " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("Main thread exiting.");
    }
}

   这个程序产生的输出和前面版本的相同,可以看出,子线程是通过实例化NewThread类创建的,NewThread类派生自Thread,其中对super()方法的调用,会调用以下形式的Thread构造函数:public Thread(String threadName)。

1.4.3 两种创建方式的对比

   Java为什么提供两种创建子线程的方式,哪种方式更好一些呢?
   Thread类定义了派生类可以重写的几个方法。在这些方法中只有一个方法必须重写,即run()方法。当然,这也是实现Runnable接口时需要实现的方法。只有当类正在以某种方式增强或修改时,才应当对类进行扩展。因此,如果不重写Thread类的其他方法,创建子线程的最好方式可能是简单地实现Runnable接口。此外,通过实现Runnbale接口,您的线程类不需要继承Thread类,从而可以自由的继承其他类。

1.5 创建多个线程

   到目前为止,只使用了两个线程:主线程和一个子线程。但是,程序可以产生所需要的任意多个线程。例如,下面的程序创建了三个子线程:

//Create multiple threads
class NewThread implements Runnable {
    String name;// name of thread
    Thread t;

    NewThread(String threadname) {
        name = threadname;
        t = new Thread(this, name);
        System.out.println("New thread: " + t);
        t.start();//Start the thread
    }

    //This is the entry point for the second thread
    public void run() {
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println(name + ": " + i);
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            System.out.println(name + "Interrupted.");
        }
        System.out.println(name + " exiting.");
    }
}

class MultiThreadDemo {
    public static void main(String[] args) {
        new NewThread("One");//start threads
        new NewThread("Two");
        new NewThread("Three");
        try {
          //wait for other threads to end
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("Main thread exiting.");
    }
}

   这个程序的一次样本输出如下所示(根据特定的执行环境,输出可能会有所变化):

New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
New thread: Thread[Three,5,main]
One: 5
Two: 5
Three: 5
One: 4
Two: 4
Three: 4
One: 3
Three: 3
Two: 3
One: 2
Two: 2
Three: 2
One: 1
Three: 1
Two: 1
One exiting.
Three exiting.
Two exiting.
Main thread exiting.

   可以看出,启动之后,所有三个子线程共享CPU。注意在main()方法中对sleep(10000)的调用,这会导致主线程休眠10秒钟,从而确保主线程在最后结束。

1.6 使用 isAlive()和 join()方法

   前面提到过,通常希望主线程在最后结束。在前面的demo中,通过在main()方法中调用sleep()方法,并指定足够长的延迟时间来确保所有子线程在主线程之前终止。但是,这完全不是一个令人满意的方案,并且还会造成一个更大的问题:一个线程如何知道另外一个线程何时结束?幸运的是,Thread类提供了一种能够解决这个问题的方法。
   有两种方法可以确定线程是否已经结束。首次,可以为线程调用isAlive()方法。这个方法是由Thread类定义的,它的一般形式如下所示:

final boolean isAlive()

   如果线程仍然在运行,isAlive()方法就返回true,否则返回false。
   虽然isAlive()方法有时很有用,但是通常使用join()方法来等待线程结束,如下所示:

final void join() throws InterruptedException

   该方法会一直等待,直到调用线程终止。如此命名该方法的原因是:调用线程一直等待,直到指定的线程加入(join)其中为止。join()方法的另外一种形式,允许指定希望等待指定线程终止的最长时间。
   下面是前面例子的改进版本,该版本使用join()方法确保主线程在最后结束,另外还演示了isAlive()方法的使用:

//Using join() to wait for threads to finish.
class NewThread implements Runnable {
    String name;// name of thread
    Thread t;

    NewThread(String threadname) {
        name = threadname;
        t = new Thread(this, name);
        System.out.println("New thread: " + t);
        t.start();//Start the thread
    }

    //This is the entry point for thread.
    public void run() {
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println(name + ": " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println(name + "Interrupted.");
        }
        System.out.println(name + " exiting.");
    }
}

class DemoJoin {
    public static void main(String[] args) {
        NewThread ob1 = new NewThread("One");//start threads
        NewThread ob2 = new NewThread("Two");
        NewThread ob3 = new NewThread("Three");
        System.out.println("Thread One is alive: " + ob1.t.isAlive());
        System.out.println("Thread Two is alive: " + ob2.t.isAlive());
        System.out.println("Thread Three is alive: " + ob3.t.isAlive());
        //wait for threads to finish
        try {
            System.out.println("Wait for threads to finish.");
            ob1.t.join();
            ob2.t.join();
            ob3.t.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("Thread One is alive: " + ob1.t.isAlive());
        System.out.println("Thread Two is alive: " + ob2.t.isAlive());
        System.out.println("Thread Three is alive: " + ob3.t.isAlive());
        System.out.println("Main thread exiting.");
    }
}

   下面是该程序的一次样本输出(基于特定的执行环境,输出可能会有不同):

New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
New thread: Thread[Three,5,main]
Thread One is alive: true
Thread Two is alive: true
Thread Three is alive: true
Wait for threads to finish.
Two: 5
One: 5
Three: 5
Three: 4
One: 4
Two: 4
Two: 3
One: 3
Three: 3
One: 2
Three: 2
Two: 2
One: 1
Three: 1
Two: 1
Two exiting.
Three exiting.
One exiting.
Thread One is alive: false
Thread Two is alive: false
Thread Three is alive: false
Main thread exiting.

   可以看出,在join()方法调用返回之后,线程停止执行。

1.7 线程优先级

   线程调度程序根据线程优先级决定每个线程应当何时运行。理论上,优先级更高的线程比优先级更低的线程会获得更多的CPU时间。实际上,线程得到的CPU时间除了依赖于优先级外,通常还依赖于其他几个因素(例如,操作系统实现多任务的方式会影响CPU时间的相对可用性)。具有更高优先级的线程还可能会取代更低优先级的线程。例如,当一个低优先级的线程正在运行时,需要恢复一个更高优先级的线程(例如,从休眠或等待 I/O 中恢复)时,高优先级的线程将取代低优先级的线程。
   理论上,具有相同优先级的线程应当得到相等的CPU时间。但是,这需要谨慎对待。请记住,Java被设计为在范围广泛的环境中运行。有些环境实现多任务的方式与其他环境不同。为了安全起见,具有相同优先级的线程应当时不时释放控制权。这样可以确保所有线程在非抢占式操作系统中有机会运行。实际上,即使是在非抢占式环境中,大部分线程仍然有机会运行,因为大部分线程不可避免地会遇到一些阻塞情况,例如I/O等待。当发生这种情况时,阻塞的线程被挂起,其他线程就可以运行。但是,如果希望平滑多个线程的执行,最好不要依赖这种情况。此外,某些类型的任务是CPU密集型的。这种线程会支配CPU。对于这类线程,我们会希望经常地释放控制权,以使其他线程能够运行。
   为了设置线程的优先级,需要使用setPriority()方法,它是Thread类的成员。下面是该方法的一般形式:

final void setPriority(int level)

   其中,level指定了为调用线程设置的新优先级。level的值必须在MIN_PRIORITY和MAX_PRIORITY之间选择。目前,这些值分别是1和0。如果希望将线程设置为默认优先级,可以使用NORM_PRIORITY,目前的值是5。这些优先级是在Thread类中作为static final变量定义的。
   可以通过调用Thread类的getPriority()方法获取当前设置的优先级,该方法如下所示:

final int getPriority()

   不同的Java实现对于任务调度可能有很大的区别。如果线程依赖于抢占式行为,而不是协作性地放弃CPU,那么经常会引起不一致性。使用Java实现可预测、跨平台行为的最安全方法是使用自愿放弃CPU控制权的线程。

1.8 同步

   当两个或多个线程需要访问共享的资源时,它们需要以某种方式确保每次只有一个线程使用资源。实现这一目的的过程称为同步。正如即将看到的,Java为同步提供了独特的、语言级的支持。
   同步的关键是监视器的概念,监视器是用作互斥锁的对象。在给定时刻,只有一个线程可以拥有监视器。当线程取得琐时,也就进入了监视器。其他所有企图进入加锁监视器的线程都会被挂起,直到第一个线程退出监视器。也就是说,这些等待的其他线程在等待监视器。如果需要的话,拥有监视器的线程可以再次进入监视器。
   可以使用两种方法同步代码。这两种方法都要用到synchronized关键字,下面分别介绍这两种方法。

1.8.1 使用同步方法

   在Java中进行同步很容易,因为所有对象都有与它们自身关联的隐式监视器。为了进入对象的监视器,只需要调用使用synchronized关键字修饰过的方法。当某个线程进入同步方法中时,调用同一实例的该同步方法(或任何其他同步方法)的所有其他线程都必须等待。为了退出监视器并将对象的控制权交给下一个等待线程,监视器的拥有者只需要简单地从同步方法返回。
   为了理解对同步的需求,下面介绍一个应当使用但是还没使用同步的例子。下面的程序有3个简单地类。第一个类是Callme,其中只有一个方法call()。call()方法带有一个String类型的参数msg,这个方法尝试在方括号中输出msg字符串。需要注意的是:call()方法在输出开括号和msg字符串之后调用Thread.sleep(1000),这会导致当前线程暂停1秒。
   下一个类是Caller,其构造函数带有两个参数:对Callme实例的引用和String类型的字符串。这两个参数分别存储在成员变量target和msg中。构造函数还创建了一个新的调用对象run()方法的线程。线程会立即启动。Caller类的run()方法调用Callme类实例target的call()方法,并传入msg字符串。最后,Synch类通过创建一个Callme类实例和3个Caller类实例来启动程序,每个Caller类实例带有唯一的消息字符串,但是为每个Caller类传递同一个Callme实例。

//This program is not synchronized.
public class Callme {
    void call(String msg) {
        System.out.println("[" + msg);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("Interrupted");
        }
        System.out.println("]");
    }
}

public class Caller implements Runnable {
    String msg;
    Callme target;
    Thread t;

    public Caller(Callme targ, String s) {
        target = targ;
        msg = s;
        t = new Thread(this);
        t.start();
    }

    @Override
    public void run() {
        target.call(msg);
    }
}

public class Synch {
    public static void main(String[] args) {
        Callme target = new Callme();
        Caller ob1=new Caller(target,"Hello");
        Caller ob2=new Caller(target,"Synchronized");
        Caller ob3=new Caller(target,"World");

        // wait for threads to end
        try{
            ob1.t.join();
            ob2.t.join();
            ob3.t.join();
        }catch (InterruptedException e){
            System.out.println("Interrupted");
        }
        /**
         * 输出结果
         [Hello[Synchronized[World]
         ]
         ]
         */
    }
}

   可以看出,通过调用sleep()方法,call()方法允许执行切换到另一个线程,这会导致混合输出3个消息字符串。在这个程序中,没有采取什么方法以阻止3个线程在相同的时间调用同一对象的同一方法,这就是所谓的竞态条件(race condition),因为3个线程相互竞争以完成方法。这个例子使用了sleep()方法,使得效果可以重复并且十分明显。在大多数情况下,竞态条件会更加微妙并且更不可预测,因为不能确定何时会发生线程上下文切换。这会造成程序在某一次运行正确,而在下一次可能运行错误。
   为了修复前面的程序,必须按顺序调用call()方法。也就是说,必须限制每次只能由一个线程调用call()方法。为此,只需要简单地在call()方法定义的前面添加关键字synchronized,如下所示:

class Callme{
 synchronized void call(String msg){
 ...

   当一个线程使用call()方法时,这会阻止其他线程进入该方法,将synchronized关键字添加到call()方法之后,程序的输出如下所示:

[Hello]
[World]
[Synchronized]

   在多线程情况下,如果有一个或一组方法用来操作对象的内部状态,那么每次都应当使用synchronized关键字,以保证状态不会进入竞态条件。请记住,一旦线程进入一个实例的同步方法,所有其他线程就都不能再进入相同实例的任何同步方法。但是仍然可以继续调用同一实例的非同步部分。

1.8.2 synchronized 语句

   虽然在类中创建同步方法是一种比较容易并且行之有效的实现同步的方式,但并不是在所有情况下都可以使用这种方式。为了理解其中的原因,我们分析下面的内容。假设某个类没有针对多线程访问进行设计,即类没有使用同步方法,而又希望同步对类的访问。进一步讲,类不是由您创建的,而是由第三方创建的,并且您不能访问类的源代码。因此,不能为类中的合适方法添加synchronized 修饰符。如何同步访问这种类的对象呢?可以简单地将对这种类定义的方法的调用放到synchronized代码块中。
   下面是synchronized语句的一般形式:

synchronized(objRef){
 //statements to be synchronized
}

   其中,objRef是对被同步对象的引用。synchronized代码块确保对objRef对象的成员方法的调用,只会在当前线程成功进入objRef的监视器之后发生。
   下面是前面例子的另外一个版本,该版本在run()方法中使用synchronized代码块:

//This program uses a synchronized block.
public class Callme {
    void call(String msg) {
        System.out.print("[" + msg);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("Interrupted");
        }
        System.out.println("]");
    }
}
public class Caller implements Runnable {
    String msg;
    Callme target;
    Thread t;

    public Caller(Callme targ, String s) {
        target = targ;
        msg = s;
        t = new Thread(this);
        t.start();
    }

    //synchronized calls to call()
    @Override
    public void run() {
        synchronized (target) {//synchronized block
            target.call(msg);
        }
    }
}
public class Synch1 {
    public static void main(String[] args) {
        Callme target = new Callme();
        Caller ob1=new Caller(target,"Hello");
        Caller ob2=new Caller(target,"Synchronized");
        Caller ob3=new Caller(target,"World");

        // wait for threads to end
        try{
            ob1.t.join();
            ob2.t.join();
            ob3.t.join();
        }catch (InterruptedException e){
            System.out.println("Interrupted");
        }
    }
}

   在此没有使synchronized修饰call()方法。反而,在Caller类的run()方法中使用了synchronized语句。这会使该版本的输出和前面版本的相同,它们都是正确的,因为每个线程在开始之前都要等待前面的线程先结束。

1.9 线程间通信

   前面的例子无条件地锁住其他线程对特定方法的异步访问。Java对象的隐式监视器的这种用途很强大,但是通过进程间通信可以实现更细微级别的控制。正如即将看到的,在Java中这很容易实现。
   在前面讨论过,多线程通过将任务分隔到独立的逻辑单元来替换事件循环编程。线程还提供了第二个优点:消除了轮询检测。轮询检测通常是通过重复检查某些条件的循环实现的。一旦条件为true,就会发生恰当的动作,这会浪费CPU时间。例如,分析经典的队列问题,对于这种问题,一个线程产生一些数据,另外一个线程使用这些数据。为了使问题更有趣,假定生产者在产生更多数据之前,必须等待消费者结束。在轮询检测系统中,消费者在等待生成者生成时需要消耗许多的CPU时间。一旦生产者结束生产数据,就会开始轮询,在等待消费者结束的过程中,会浪费更多CPU时间。显然,这种情况不是我们所期待的。
   为了避免轮询检测,Java通过wait()、notify()以及notifyAll()方法,提供了一种巧妙的进程间通信机制,这些方法在Object中是作为final方法实现的,因此所有类都具有这些方法。所有这三个方法都只能在同步上下文中调用:

  • wait()方法通知调用线程放弃监视器并进入休眠,直到其他线程进入同一个监视器并调用notify()方法或notifyAll()方法。
  • notify()方法唤醒调用相同对象的wait()方法的线程。
  • notifyAll()方法唤醒调用相同对象的wait()方法的所有线程,其中的一个线程将得到访问授权。
       这些方法都是在Object类中定义的,如下所示:
final void wait() throws InterruptedException
final void notify()
final void notifyAll()

   wait()方法还有另外一种形式,允许指定等待的时间间隔。
   尽管在正常情况下,wait()方法会等待直到调用notify()或notifyAl()方法,但是还有一种几率很小的可能会发生的情况,等待线程由于假唤醒(spurious wakeup)而被唤醒。对于这种情况,等待线程也会被唤醒,然而却没有调用notify()或notifyAll()方法(本质上,线程在没有什么明显的理由下就被恢复了)。因为存在这种极小的可能,Oracle推荐应当在一个检查线程等待条件的循环中调用wait()方法。下面的例子演示了这种技术。
   下面通过使用wait()和notify()方法的例子演示线程间通信。首先分析下面的示例程序,该程序以不正确的方式实现了一个简单形式的生产者/消费者问题。该实例包含4个类:类Q是试图同步的队列;类Producer是产生队列条目的线程对象;类Consumer是使用队列条目的线程对象;类PC是一个小型类,用于创建类Q、Producer和Comsumer的实例。

//An incorrect implementation of a producer and consumer.
public class Q {
    int n;

    synchronized int get() {
        System.out.println("Got:" + n);
        return n;
    }

    synchronized void put(int n) {
        this.n = n;
        System.out.println("Put:" + n);
    }
}
public class Producer implements Runnable {
    Q q;

    Producer(Q q) {
        this.q = q;
        new Thread(this,"Producer").start();
    }

    @Override
    public void run() {
        int i=0;
        while (true){
            q.put(++i);
        }

    }
}
public class Consumer implements Runnable {
    Q q;

    Consumer(Q q) {
        this.q = q;
        new Thread(this, "Consumer").start();
    }

    @Override
    public void run() {
        while (true) {
            q.get();
        }
    }
}
public class PC {
    public static void main(String[] args) {
        Q q = new Q();
        new Producer(q);
        new Consumer(q);
    }
}

   尽管类Q中的put()和get()方法是同步的,但是没有什么措施能够停止生产者过度运行,也没有什么措施能够停止消费者两次消费相同的队列值。因此,得到的输出是错误的,如下所示(根据处理器的速度和加载的任务,实际的输出可能会不同):

Put: 1
Got: 1
Got: 1
Got: 1
Got: 1
Got: 1
Put: 2
Put: 3
Put: 4
Put: 5
Put: 6
Put: 7
Got: 7

   可以看出,生产者在将1放入队列之后,消费者开始运行,并且连续5次获得相同的数值1。然后,生产者恢复执行,并产生数值2到7,而不让消费者有机会使用它们。
   使用Java编写这个程序的正确方式是使用wait()和notify()方法在两个方向上发信号,如下所示:

//A correct implementation of a producer and consumer.
public class Q {
    int n;
    boolean valueSet = false;

    synchronized int get() {
        while (!valueSet) {
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println("InterruptedException caught");
            }
        }
        System.out.println("Got:" + n);
        valueSet = false;
        notify();
        return n;
    }

    synchronized void put(int n) {
        while (valueSet) {
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println("InterruptedException caught");
            }
        }
        this.n = n;
        valueSet = true;
        System.out.println("Put:" + n);
        notify();
    }
}
public class Producer implements Runnable {
    Q q;

    Producer(Q q) {
        this.q = q;
        new Thread(this,"Producer").start();
    }

    @Override
    public void run() {
        int i=0;
        while (true){
            q.put(++i);
        }

    }
}
public class Consumer implements Runnable {
    Q q;

    Consumer(Q q) {
        this.q = q;
        new Thread(this, "Consumer").start();
    }

    @Override
    public void run() {
        while (true) {
            q.get();
        }
    }
}
public class PCFixed {
    public static void main(String[] args) {
        Q q = new Q();
        new Producer(q);
        new Consumer(q);
    }
}

   在get()方法中调用wait()方法,这会导致get()方法的执行被挂起,直到生产者通知已经准备好一些数据。当发出通知时,恢复get()方法中的执行。在获得数据之后,get()方法调用notify()方法。该调用通知生产者可以在队列中放入更多数据。在put()方法中,wait()方法暂时执行直到消费者从队列中删除条目。当执行恢复时,下一个数据条目被放入到队列中,并调用notify()方法。这会通知消费者,现在应当删除该数据条目。
   下面是这个程序的一些输出,这些输出显示了清晰的同步行为:

Put:1
Got:1
Put:2
Got:2
Put:3
Got:3
Put:4
Got:4
Put:5
Got:5
Put:6
Got:6

1.9.1 死锁

   需要避免的与多任务处理明确相关的特殊类型的错误是死锁(deadlock),当两个线程循环依赖一对同步对象时,会发生这种情况。例如,假设一个线程进入对象X的监视器,另一个线程进入对象Y的监视器。如果X中的线程试图调用对象Y的任何同步方法,那么就会被阻塞。但是,如果对象Y中的线程也试图调用对象X的任何同步方法,那么会永远等待下去,因为为了进入X,必须释放对Y加的锁,这样第一个线程才能完成,死锁是一种很难调试的错误,原因有两点:

  • 死锁通常很少发生,只有当两个线程恰好以这种方式获取CPU时钟周期时才会发生死锁。
  • 死锁可能涉及更多的线程以及更多的同步对象(也就是说,死锁可能是通过更复杂的事件序列发生的,而不是通过刚才描述的情况发生的)。
       下面的例子创建了两个类——A和B,这两个类分别具有方法foo()和bar(),在调用对方类中的方法之前会暂停一会儿。主类Deadlock创建A的一个实例和B的一个实例,然后开始第二个线程以设置死锁条件。方法foo()和bar()使用sleep()作为强制死锁条件发生的手段。
public class A {
    synchronized void foo(B b) {
        String name = Thread.currentThread().getName();
        System.out.println(name + " entered A.foo");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("A Interrupted");
        }
        System.out.println(name + " trying to call B.last()");
        b.last();
    }

    synchronized void last() {
        System.out.println("Inside A.last");
    }
}
public class B {
    synchronized void bar(A a) {
        String name = Thread.currentThread().getName();
        System.out.println(name + " entered B.bar");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("B Interrupted");
        }
        System.out.println(name + " trying to call A.last()");
        a.last();
    }

    synchronized void last() {
        System.out.println("Inside A.last");
    }
}
public class Deadlock implements Runnable {
    A a = new A();
    B b = new B();

    Deadlock() {
        Thread.currentThread().setName("MainThread");
        Thread t = new Thread(this, "RacingThread");
        t.start();
        a.foo(b);//get lock on a in this thread
        System.out.println("Back in main thread");
    }

    @Override
    public void run() {
        b.bar(a);// get lock on b in other thread
        System.out.println("Back in other thread");
    }

    public static void main(String[] args) {
        new Deadlock();
    }
}

   当运行这个程序时,会看到如下所示的输出:

MainThread entered A.foo
RacingThread entered B.bar
RacingThread trying to call A.last()
MainThread trying to call B.last()

   可以看出程序被死锁,当等待a的监视器时,RacingThread拥有b的监视器。同时MainThread拥有a,并且在等待获取b。这个程序永远不会结束。正如该程序所演示的,如果多线程程序偶尔被锁住,那么首先应当检查是否是由于死锁造成的。

1.10 挂起、恢复与停止线程

   有时,挂起线程的执行是有用的。例如,可以使用单独的线程显示一天的时间。如果用户不想要时钟,那么可以挂起时钟线程。
   在Java2之前,程序使用Thread类定义的suspend()、resume()和stop()方法暂停、重新启动和停止线程的执行。
   虽然这些方法对于管理线程执行看起来是一种合理并且方便的方式,但是在新的Java程序中不能使用它们。下面是其中的原因。Java2之后不再推荐使用Thread类的suspend()方法,因为suspend()方法有时会导致严重的系统故障。假定线程为关键数据结构加锁,如果这时线程被挂起,那么这些锁将无法释放。其他可能等待这些资源的线程会被死锁。
   方法resume()也不推荐使用。虽然不会造成问题,但是如果不实用suspend()方法,就不能使用resume()方法,它们是配对使用的。
   对于Thread类的stop()方法,Java2也反对使用,因为有时这个方法也会造成严重的系统故障。假定线程正在向关键的重要数据结构中写入数据。并且只完成了部分发生变化的数据,如果这时停止线程,那么数据结构可能会处于损坏状态。问题是:stop()会导致释放调用线程的所有锁。因此,另一个正在等待相同锁的线程可能会使用这些已损坏的数据。
   因此现在不能使用suspend()、resume()以及stop()方法控制线程,所以您可能会认为没有方法来暂停、重启以及终止线程。但并不是,反而,线程必须被设计为run()方法周期性地进行检查,以确定是否应当挂起、恢复或停止线程自身的执行。通常,这是通过建立用来标志线程执行状态的变量来完成的。只要这个标志被设置为"运行",run()方法就必须让线程执行起来。如果标志变量被设置为"挂起",线程就必须暂停。如果标志变量被设置为"停止",线程就必须终止。当然,编写这种代码的方式有很多,但是对于所有程序,中心主题是相同的。
   下面的例子演示了如何使用继承自Object的wait()和notify()方法控制线程的执行。下面分析这个程序中的操作。NewThread类包含布尔型实例变量suspendFlag,该变量用户控制线程的执行,构造函数将该变量初始化为false。方法run()包含检查suspendFlag变量的synchronized代码块。如果该变量为true,就调用wait()方法,挂起线程的执行。mysuspend()方法将suspendFlag变量设置为true。myresume()方法将suspendFlag设置为false,并调用notify()方法以唤醒线程。最后对mian()方法进行修改以调用mysuspend()和myresume()方法。

//Suspending and resuming a thread the modern way
public class NewThread implements Runnable{
    String name;//name of thread
    Thread t;
    boolean suspendFlag;

    NewThread(String threadName) {
        name = threadName;
        t = new Thread(this, name);
        System.out.println("New Thread: " + t);
        suspendFlag=false;
        t.start();
    }
//This is the entry point for thread.
    @Override
    public void run() {
        try {
            for(int i=15;i>0;i--){
                System.out.println(name+": "+i);
                Thread.sleep(200);
                synchronized (this){
                    while (suspendFlag){
                        wait();
                    }
                }
            }
        } catch (InterruptedException e) {
            System.out.println(name+" interrupted.");
        }
        System.out.println(name+" exiting.");
    }
    synchronized void mysuspend(){
        suspendFlag=true;
    }
    synchronized void myresume(){
        suspendFlag=false;
        notifyAll();
    }
}
public class SuspendResume {
    public static void main(String[] args) {
        NewThread ob1=new NewThread("One");
        NewThread ob2=new NewThread("Two");
        try {
            Thread.sleep(1000);
            ob1.mysuspend();
            System.out.println("Suspending thread One");
            Thread.sleep(1000);
            ob1.myresume();;
            System.out.println("Resume thread One");
            ob2.mysuspend();
            System.out.println("Suspending thread Two");
            Thread.sleep(1000);
            ob2.myresume();
            System.out.println("Resumeing thread Two");
        } catch (InterruptedException e) {
            System.out.println("Main Thread Interrupted.");
        }
        //wait for threads to finish
        try {
            System.out.println("Waiting for threads to finish.");
            ob1.t.join();
            ob2.t.join();
        } catch (InterruptedException e) {
            System.out.println("Main Thread Interrupted");
        }
        System.out.println("Main Thread exiting.");
    }
    /**
     * 执行结果
     * New Thread: Thread[One,5,main]
     * New Thread: Thread[Two,5,main]
     * One: 15
     * Two: 15
     * Two: 14
     * One: 14
     * Two: 13
     * One: 13
     * One: 12
     * Two: 12
     * One: 11
     * Two: 11
     * Suspending thread One
     * Two: 10
     * Two: 9
     * Two: 8
     * Two: 7
     * Two: 6
     * Resume thread One
     * One: 10
     * Suspending thread Two
     * One: 9
     * One: 8
     * One: 7
     * One: 6
     * Resumeing thread Two
     * Waiting for threads to finish.
     * Two: 5
     * One: 5
     * Two: 4
     * One: 4
     * Two: 3
     * One: 3
     * Two: 2
     * One: 2
     * Two: 1
     * One: 1
     * Two exiting.
     * One exiting.
     * Main Thread exiting.
     */
}

   运行这个程序时,会看到线程被挂起和恢复。尽管这种机制并没有旧机制那么“清晰”,但是不管怎样,这是确保不会发生运行错误所需要做的。对于所有新代码,必须使用这种方式。

1.11 获取线程的状态

   线程可以处于许多不同的状态。可以调用Thread类定义的getState()方法来获取线程当前的状态,该方法如下所示:

Thread.State getState()

   该方法返回Thread.State类型的值,指示在调用该方法时线程所处的状态。State是由Thread类定义的枚举。下面列出了getState()可以返回的值:

状态
BLOCKED线程因为正在等待需要的锁而挂起执行
NEW线程还没有开始运行
RUNNABLE线程要么当前正在执行,要么在获得CPU的访问权之后执行
TERMINATED线程已经完成执行
TIMED_WAITING线程挂起执行一段指定的时间,例如当调用sleep()方法时就会处于这种状态。当调用wait()或join()方法的暂停版时,也会进入这种状态
WAITING线程因为等待某些动作而挂起执行。例如,因为调用非暂停版的wait()或join()方法而等待时,会处于这种状态

   下图显示了各种线程状态之间的联系:
线程状态
   对于给定的Thread实例,可以使用getState()方法获取线程的状态。例如,下面的代码判断调用线程thrd在调用getState()方法时是否处于RUNNABLE状态:

Thread.State ts = thrd.getState();
if(ts == Thread.State.RUNNABLE) // ...

   在调用getState()方法之后,线程的状态可能会发生变化,理解这一点很重要。因此,基于具体的环境,通过调用getState()方法获取的状态,可能无法反映之后一段较短的时间内线程的实际状态。由于该原因,getState()方法的目标不是提供一种同步线程的方法,而是主要用于调试或显示线程的运行时特征。

1.12 使用多线程

   有效利用Java多线程特性的关键是并发地而不是顺序地思考问题。例如,当程序中有两个可以并发执行的子系统时,可以在单独的线程中执行它们。通过细心地使用多线程,可以创建非常高效的程序。但是,如果创建的线程太多,实际上可以会降低程序的性能,而不是增强性能。请记住,线程上下文切换需要一定的开销。如果创建的线程过多,花费在上下文切换上的CPU时间会比执行程序的实际时间更长。最后一点:为了创建能够自动伸缩以尽可能利用多核系统中可用处理器的计算密集型应用程序,可以考虑使用新的Fork/Join框架。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值