0基础小白入行Java-Java之 Java多线程
小职 2021-07-30 来源 : 阅读 398 评论 0

摘要:本文主要是0基础小白入行Java-Java之 Java多线程,通过具体的内容向大家展现,希望对大家Java开发的学习有所帮助。

本文主要是0基础小白入行Java-Java之 Java多线程,通过具体的内容向大家展现,希望对大家Java开发的学习有所帮助。

0基础小白入行Java-Java之 Java多线程

你一定知道多任务处理,因为它实际上被所有的现代操作系统所支持。然而,多任务处理有两种截然不同的类型:基于进程的和基于线程的。认识两者的不同是十分重要的。对很多读者,基于进程的多任务处理是更熟悉的形式。进程(process)本质上是一个执行的程序。因此,基于进程(process-based)的多任务处理的特点是允许你的计算机同时运行两个或更多的程序。举例来说,基于进程的多任务处理使你在运用文本编辑器的时候可以同时运行Java编译器。在基于进程的多任务处理中,程序是调度程序所分派的最小代码单位。在基于线程(thread-based)的多任务处理环境中,线程是最小的执行单位。这意味着一个程序可以同时执行两个或者多个任务的功能。例如,一个文本编辑器可以在打印的同时格式化文本。所以,多进程程序处理“大图片”,而多线程程序处理细节问题。

多线程程序比多进程程序需要更少的管理费用。进程是重量级的任务,需要分配它们自己独立的地址空间。进程间通信是昂贵和受限的。进程间的转换也是很需要花费的。另一方面,线程是轻量级的选手。它们共享相同的地址空间并且共同分享同一个进程。线程间通信是便宜的,线程间的转换也是低成本的。当Java程序使用多进程任务处理环境时,多进程程序不受Java的控制,而多线程则受Java控制。

多线程帮助你写出CPU最大利用率的高效程序,因为空闲时间保持最低。这对Java运行的交互式的网络互连环境是至关重要的,因为空闲时间是公共的。举个例子来说,网络的数据传输速率远低于计算机处理能力,本地文件系统资源的读写速度远低于CPU的处理能力,当然,用户输入也比计算机慢很多。在传统的单线程环境中,你的程序必须等待每一个这样的任务完成以后才能执行下一步——尽管CPU有很多空闲时间。多线程使你能够获得并充分利用这些空闲时间。如果你在Windows98 或Windows 2000这样的操作系统下有编程经验,那么你已经熟悉了多线程。然而,Java管理线程使多线程处理尤其方便,因为很多细节对你来说是易于处理的。

Java线程模型     

Java运行系统在很多方面依赖于线程,所有的类库设计都考虑到多线程。实际上,Java使用线程来使整个环境异步。这有利于通过防止CPU循环的浪费来减少无效部分。

为更好的理解多线程环境的优势可以将它与它的对照物相比较。单线程系统的处理途径是使用一种叫作轮询的事件循环方法。在该模型中,单线程控制在一无限循环中运行,轮询一个事件序列来决定下一步做什么。一旦轮询装置返回信号表明,已准备好读取网络文件,事件循环调度控制管理到适当的事件处理程序。直到事件处理程序返回,系统中没有其他事件发生。这就浪费了CPU时间。这导致了程序的一部分独占了系统,阻止了其他事件的执行。 总的来说, 单线程环境, 当一个线程因为等待资源时阻塞(block,挂起执行),整个程序停止运行。

Java多线程的优点在于取消了主循环/轮询机制。一个线程可以暂停而不影响程序的其他部分。例如,当一个线程从网络读取数据或等待用户输入时产生的空闲时间可以被利用到其他地方。多线程允许活的循环在每一帧间隙中沉睡一秒而不暂停整个系统。在Java程序中出现线程阻塞,仅有一个线程暂停,其他线程继续运行。

线程存在于好几种状态。线程可以正在运行(running)。只要获得CPU时间它就可以运行。运行的线程可以被挂起(suspend),并临时中断它的执行。一个挂起的线程可以被恢复(resume), 允许它从停止的地方继续运行。一个线程可以在等待资源时被阻塞(block)。

在任何时候,线程可以终止(terminate),这立即中断了它的运行。一旦终止,线程不能被恢复。

线程优先级    

 

Java给每个线程安排优先级以决定与其他线程比较时该如何对待该线程。线程优先级是详细说明线程间优先关系的整数。作为绝对值,优先级是毫无意义的;当只有一个线程时,优先级高的线程并不比优先权低的线程运行的快。相反,线程的优先级是用来决定何时从一个运行的线程切换到另一个。这叫“上下文转换”(context switch)。决定上下文转换发生的规则很简单:

· 线程可以自动放弃控制。在I/O未决定的情况下,睡眠或阻塞由明确的让步来完成。在这种假定下,所有其他的线程被检测,准备运行的最高优先级线程被授予CPU。 

· 线程可以被高优先级的线程抢占。在这种情况下,低优先级线程不主动放弃,处理器只是被先占——无论它正在干什么——处理器被高优先级的线程占据。基本上,一旦高优先级线程要运行,它就执行。这叫做有优先权的多任务处理。 

当两个相同优先级的线程竞争CPU周期时,情形有一点复杂。对于Windows98这样的操作系统,等优先级的线程是在循环模式下自动划分时间的。对于其他操作系统,例如Solaris 2.x,等优先级线程相对于它们的对等体自动放弃。如果不这样,其他的线程就不会运行。

警告:不同的操作系统下等优先级线程的上下文转换可能会产生错误。

同步性    

 

因为多线程在你的程序中引入了一个异步行为,所以在你需要的时候必须有加强同步性的方法。举例来说,如果你希望两个线程相互通信并共享一个复杂的数据结构,例如链表序列,你需要某些方法来确保它们没有相互冲突。也就是说,你必须防止一个线程写入数据而另一个线程正在读取链表中的数据。为此目的,Java在进程间同步性的老模式基础上实行了另一种方法:管程(monitor)。管程是一种由C.A.R.Hoare首先定义的控制机制。

你可以把管程想象成一个仅控制一个线程的小盒子。一旦线程进入管程,所有线程必须等待直到该线程退出了管程。用这种方法,管程可以用来防止共享的资源被多个线程操纵。

很多多线程系统把管程作为程序必须明确的引用和操作的对象。Java提供一个清晰的解决方案。没有“Monitor”类;相反,每个对象都拥有自己的隐式管程,当对象的同步方法被调用时管程自动载入。一旦一个线程包含在一个同步方法中,没有其他线程可以调用相同对象的同步方法。这就使你可以编写非常清晰和简洁的多线程代码,因为同步支持是语言内置的。

消息传递    

 

在你把程序分成若干线程后,你就要定义各线程之间的联系。用大多数其他语言规划时,你必须依赖于操作系统来确立线程间通信。这样当然增加花费。然而,Java提供了多线程间谈话清洁的、低成本的途径——通过调用所有对象都有的预先确定的方法。Java的消息传递系统允许一个线程进入一个对象的一个同步方法,然后在那里等待,直到其他线程明确通知它出来。

Thread 类和Runnable 接口    

 

Java的多线程系统建立于Thread类,它的方法,它的共伴接口Runnable基础上。Thread类封装了线程的执行。既然你不能直接引用运行着的线程的状态,你要通过它的代理处理它,于是Thread  实例产生了。为创建一个新的线程,你的程序必须扩展Thread  或实现Runnable接口。

Thread类定义了好几种方法来帮助管理线程。本章用到的方法如表13-1所示:

 0基础小白入行Java-Java之 Java多线程

 

到目前为止,本书所应用的例子都是用单线程的。本章剩余部分解释如何用Thread 和Runnable 来创建、管理线程。让我们从所有Java程序都有的线程:主线程开始。

主  线  程     

 

当Java程序启动时,一个线程立刻运行,该线程通常叫做程序的主线程(main thread),因为它是程序开始时就执行的。主线程的重要性体现在两方面:

· 它是产生其他子线程的线程

· 通常它必须最后完成执行,因为它执行各种关闭动作。

尽管主线程在程序启动时自动创建,但它可以由一个Thread对象控制。为此,你必须调用方法currentThread()获得它的一个引用,currentThread()是Thread类的公有的静态成员。

它的通常形式如下:

static Thread currentThread( )

该方法返回一个调用它的线程的引用。一旦你获得主线程的引用,你就可以像控制其他线程那样控制主线程。

让我们从复习下面例题开始:

// Controlling the mainThread.

class CurrentThreadDemo {

  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 threadinterrupted");

    }

  }

}

在本程序中,当前线程(自然是主线程)的引用通过调用currentThread()获得,该引用保存在局部变量t中。然后,程序显示了线程的信息。接着程序调用setName()改变线程的内部名称。线程信息又被显示。然后,一个循环数从5开始递减,每数一次暂停一秒。暂停是由sleep()方法来完成的。Sleep()语句明确规定延迟时间是1毫秒。注意循环外的try/catch块。

Thread类的sleep()方法可能引发一个InterruptedException异常。这种情形会在其他线程想要打搅沉睡线程时发生。本例只是打印了它是否被打断的消息。在实际的程序中,你必须灵活处理此类问题。下面是本程序的输出:

Current thread:Thread[main,5,main]

After name change: Thread[MyThread,5,main]


 

注意t作为语句println()中参数运用时输出的产生。该显示顺序:线程名称,优先级以及组的名称。默认情况下,主线程的名称是main。它的优先级是5,这也是默认值,main也是所属线程组的名称。一个线程组(thread group)是一种将线程作为一个整体集合的状态控制的数据结构。这个过程由专有的运行时环境来处理,在此就不赘述了。线程名改变后,又被输出。这次,显示了新的线程名。

让我们更仔细的研究程序中Thread类定义的方法。 sleep()方法按照毫秒级的时间指示使线程从被调用到挂起。它的通常形式如下:

 

static void sleep(longmilliseconds) throws InterruptedException

挂起的时间被明确定义为毫秒。该方法可能引发InterruptedException异常。

sleep()方法还有第二种形式,显示如下,该方法允许你指定时间是以毫秒还是以纳秒为周期。

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

第二种形式仅当允许以纳秒为时间周期时可用。

如上述程序所示,你可以用setName()设置线程名称,用getName()来获得线程名称(该过程在程序中没有体现)。这些方法都是Thread 类的成员,声明如下:

final void setName(StringthreadName)

final String getName( )

这里,threadName 特指线程名称。

创  建  线  程     

 

大多数情况,通过实例化一个Thread对象来创建一个线程。Java定义了两种方式:

· 实现Runnable 接口。

· 可以继承Thread类。

下面的两小节依次介绍了每一种方式。

实现Runnable接口    

 

创建线程的最简单的方法就是创建一个实现Runnable接口的类。Runnable抽象了一个执行代码单元。你可以通过实现Runnable接口的方法创建每一个对象的线程。为实现 Runnable 接口,一个类仅需实现一个run()的简单方法,该方法声明如下:

public void run( )

在run()中可以定义代码来构建新的线程。理解下面内容是至关重要的:run()方法能够像主线程那样调用其他方法,引用其他类,声明变量。仅有的不同是run()在程序中确立另一个并发的线程执行入口。当run()返回时,该线程结束。

在你已经创建了实现Runnable接口的类以后,你要在类内部实例化一个Thread类的对象。Thread 类定义了好几种构造函数。我们会用到的如下:

Thread(Runnable threadOb, String threadName)

该构造函数中,threadOb是一个实现Runnable接口类的实例。 这定义了线程执行的起点。

新线程的名称由threadName定义。建立新的线程后,它并不运行直到调用了它的start()方法,该方法在Thread 类中定义。本质上,start()  执行的是一个对run()的调用。 Start()方法声明如下:

void start( )

下面的例子是创建一个新的线程并启动它运行:

// Create a second thread.

class NewThread implements Runnable {

  Thread t;

 

  NewThread(){

    // Createa new, second thread

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

   System.out.println("Child thread: " + t);

   t.start(); // Start the thread

  }

 

  // This isthe entry point for the second thread.

  public voidrun() {

    try {

      for(inti = 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 ThreadDemo {

  publicstatic void main(String args[]) {

    newNewThread(); // create a new thread

 

    try {

      for(inti = 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毫秒。

这就使子线程在主线程结束之前先结束。简而言之,你将看到等待线程结束的更好途径。

扩展Thread    

 

创建线程的另一个途径是创建一个新类来扩展Thread类,然后创建该类的实例。当一个类继承Thread时,它必须重载run()方法,这个run()方法是新线程的入口。它也必须调用start()方法去启动新线程执行。下面用扩展thread类重写前面的程序:

// Create a second thread by extending Thread

class NewThread extends Thread {

 

  NewThread(){

    // Createa new, second thread

   super("Demo Thread");

   System.out.println("Child thread: " + this);

    start();// Start the thread

  }

 

  // This isthe entry point for the second thread.

  public voidrun() {

    try {

      for(inti = 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 {

  publicstatic void main(String args[]) {

    newNewThread(); // create a new thread

 

    try {

      for(inti = 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类派生。注意NewThread  中super()的调用。该方法调用了下列形式的Thread构造函数:

public Thread(String threadName)

这里,threadName指定线程名称。

选择合适方法    

 

到这里,你一定会奇怪为什么Java有两种创建子线程的方法,哪一种更好呢。所有的问题都归于一点。Thread类定义了多种方法可以被派生类重载。对于所有的方法,惟一的必须被重载的是run()方法。这当然是实现Runnable接口所需的同样的方法。很多Java程序员认为类仅在它们被加强或修改时应该被扩展。因此,如果你不重载Thread的其他方法时,最好只实现Runnable  接口。这当然由你决定。然而,在本章的其他部分,我们应用实现runnable接口的类来创建线程。

创建多线程     

 

到目前为止,我们仅用到两个线程:主线程和一个子线程。然而,你的程序可以创建所需的更多线程。例如,下面的程序创建了三个子线程:

// Create multiple threads.

class NewThread implementsRunnable {

  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 MultiThreadDemo {

  public static void main(String args[]) {

    newNewThread("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 threadexiting.");

  }

}

程序输出如下所示:

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

Three: 2

Two: 2

One: 1

Three: 1

Two: 1

One exiting.

Two exiting.

Three exiting.

Main thread exiting.

如你所见,一旦启动,所有三个子线程共享CPU。注意main()中对sleep(10000)的调用。这使主线程沉睡十秒确保它最后结束。

使用isAlive()和join()     

 

如前所述,通常你希望主线程最后结束。在前面的例子中,这点是通过在main()中调用sleep()来实现的,经过足够长时间的延迟以确保所有子线程都先于主线程结束。然而,这不是一个令人满意的解决方法,它也带来一个大问题:一个线程如何知道另一线程已经结束?幸运的是,Thread类提供了回答此问题的方法。

有两种方法可以判定一个线程是否结束。第一,可以在线程中调用isAlive()。这种方法由Thread定义,它的通常形式如下:

final boolean isAlive( )

如果所调用线程仍在运行,isAlive()方法返回true,如果不是则返回false。 但isAlive()很少用到,等待线程结束的更常用的方法是调用join(),描述如下:

final void join( ) throwsInterruptedException

该方法等待所调用线程结束。该名字来自于要求线程等待直到指定线程参与的概念。

join()的附加形式允许给等待指定线程结束定义一个最大时间。下面是前面例子的改进版本。运用join()以确保主线程最后结束。同样,它也演示了isAlive()方法。

// Using join() to wait forthreads to finish.

class NewThread implementsRunnable {

  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 = newNewThread("One");

    NewThread ob2 = newNewThread("Two");

    NewThread ob3 = new NewThread("Three");

 

    System.out.println("Thread One isalive: "

                        + ob1.t.isAlive());

    System.out.println("Thread Two isalive: "

                        + ob2.t.isAlive());

    System.out.println("Thread Three isalive: "

                        + ob3.t.isAlive());

    // wait for threads to finish

   try {

      System.out.println("Waiting forthreads to finish.");

      ob1.t.join();

      ob2.t.join();

      ob3.t.join();

    } catch (InterruptedException e) {

      System.out.println("Main threadInterrupted");

    }

 

    System.out.println("Thread One isalive: "

                        + ob1.t.isAlive());

    System.out.println("Thread Two isalive: "

                        + ob2.t.isAlive());

    System.out.println("Thread Three isalive: "

                        + ob3.t.isAlive());

 

    System.out.println("Main threadexiting.");

  }

}

程序输出如下所示:

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

Waiting for threads to finish.

One: 5

Two: 5

Three: 5

One: 4

Two: 4

Three: 4

One: 3

Two: 3

Three: 3

One: 2

Two: 2

Three: 2

One: 1

Two: 1

Three: 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()后返回,线程终止执行。

线程优先级     

 

线程优先级被线程调度用来判定何时每个线程允许运行。理论上,优先级高的线程比优先级低的线程获得更多的CPU时间。实际上,线程获得的CPU时间通常由包括优先级在内的多个因素决定(例如,一个实行多任务处理的操作系统如何更有效的利用CPU时间)。

一个优先级高的线程自然比优先级低的线程优先。举例来说,当低优先级线程正在运行,而一个高优先级的线程被恢复(例如从沉睡中或等待I/O中),它将抢占低优先级线程所使用的CPU。

理论上,等优先级线程有同等的权利使用CPU。但你必须小心了。记住,Java是被设计成能在很多环境下工作的。一些环境下实现多任务处理从本质上与其他环境不同。为安全起见,等优先级线程偶尔也受控制。这保证了所有线程在无优先级的操作系统下都有机会运行。实际上,在无优先级的环境下,多数线程仍然有机会运行,因为很多线程不可避免的会遭遇阻塞,例如等待输入输出。遇到这种情形,阻塞的线程挂起,其他线程运行。

但是如果你希望多线程执行的顺利的话,最好不要采用这种方法。同样,有些类型的任务是占CPU的。对于这些支配CPU类型的线程,有时你希望能够支配它们,以便使其他线程可以运行。

设置线程的优先级,用setPriority()方法,该方法也是Tread 的成员。它的通常形式为:

final void setPriority(int level)

这里,level指定了对所调用的线程的新的优先权的设置。Level的值必须在MIN_PRIORITY到MAX_PRIORITY范围内。通常,它们的值分别是1和10。要返回一个线程为默认的优先级,指定NORM_PRIORITY,通常值为5。这些优先级在Thread中都被定义为final型变量。

你可以通过调用Thread的getPriority()方法来获得当前的优先级设置。该方法如下:

final int getPriority( )

当涉及调度时,Java的执行可以有本质上不同的行为。Windows 95/98/NT/2000 的工作或多或少如你所愿。但其他版本可能工作的完全不同。大多数矛盾发生在你使用有优先级行为的线程,而不是协同的腾出CPU时间。最安全的办法是获得可预先性的优先权,Java获得跨平台的线程行为的方法是自动放弃对CPU的控制。

下面的例子阐述了两个不同优先级的线程,运行于具有优先权的平台,这与运行于无优先级的平台不同。一个线程通过Thread.NORM_PRIORITY设置了高于普通优先级两级的级数,另一线程设置的优先级则低于普通级两级。两线程被启动并允许运行10秒。每个线程执行一个循环,记录反复的次数。10秒后,主线程终止了两线程。每个线程经过循环的次数被显示。

// Demonstrate thread priorities.

class clicker implements Runnable {

  int click =0;

  Thread t;

  privatevolatile boolean running = true;

 

  publicclicker(int p) {

    t = newThread(this);

   t.setPriority(p);

  }

 

  public void run() {

    while(running) {

     click++;

    }

  }

 

  public voidstop() {

    running =false;

  }

 

  public voidstart() {

   t.start();

  }

}

 

class HiLoPri {

  publicstatic void main(String args[]) {

   Thread.currentThread().setPriority(Thread.MAX_PRIORITY);

    clickerhi = new clicker(Thread.NORM_PRIORITY + 2);

    clickerlo = new clicker(Thread.NORM_PRIORITY - 2);

 

   lo.start();

   hi.start();

    try {      Thread.sleep(10000);

    } catch(InterruptedException e) {

       System.out.println("Main thread interrupted.");

    }

 

   lo.stop();

   hi.stop();

 

    // Waitfor child threads to terminate.

    try {

     hi.t.join();

     lo.t.join();

    } catch(InterruptedException e) {

      System.out.println("InterruptedExceptioncaught");

    }

 

   System.out.println("Low-priority thread: " + lo.click); 

   System.out.println("High-priority thread: " + hi.click);

  }

}

该程序在Windows 98下运行的输出,表明线程确实上下转换,甚至既不屈从于CPU,也不被输入输出阻塞。优先级高的线程获得大约90%的CPU时间。

Low-priority thread: 4408112

High-priority thread: 589626904

当然,该程序的精确的输出结果依赖于你的CPU的速度和运行的其他任务的数量。当同样的程序运行于无优先级的系统,将会有不同的结果。

上述程序还有个值得注意的地方。注意running前的关键字volatile。尽管volatile 在下章会被很仔细的讨论,用在此处以确保running的值在下面的循环中每次都得到验证。

while (running) {

  click++;

}

如果不用volatile,Java可以自由的优化循环:running的值被存在CPU的一个寄存器中,每次重复不一定需要复检。volatile的运用阻止了该优化,告知Java running可以改变,改变方式并不以直接代码形式显示。

线  程  同  步     

 

当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用。达到此目的的过程叫做同步(synchronization)。像你所看到的,Java为此提供了独特的,语言水平上的支持。

同步的关键是管程(也叫信号量semaphore)的概念。管程是一个互斥独占锁定的对象,或称互斥体(mutex)。在给定的时间,仅有一个线程可以获得管程。当一个线程需要锁定,它必须进入管程。所有其他的试图进入已经锁定的管程的线程必须挂起直到第一个线程退出管程。这些其他的线程被称为等待管程。一个拥有管程的线程如果愿意的话可以再次进入相同的管程。

如果你用其他语言例如C或C++时用到过同步,你会知道它用起来有一点诡异。这是因为很多语言它们自己不支持同步。相反,对同步线程,程序必须利用操作系统源语。幸运的是Java通过语言元素实现同步,大多数的与同步相关的复杂性都被消除。

你可以用两种方法同步化代码。两者都包括synchronized关键字的运用,下面分别说明这两种方法。

使用同步方法    

 

Java中同步是简单的,因为所有对象都有它们与之对应的隐式管程。进入某一对象的管程,就是调用被synchronized关键字修饰的方法。当一个线程在一个同步方法内部,所有试图调用该方法(或其他同步方法)的同实例的其他线程必须等待。为了退出管程,并放弃对对象的控制权给其他等待的线程,拥有管程的线程仅需从同步方法中返回。

为理解同步的必要性,让我们从一个应该使用同步却没有用的简单例子开始。下面的程序有三个简单类。首先是Callme,它有一个简单的方法call( )。call( )方法有一个名为msg的String参数。该方法试图在方括号内打印msg  字符串。有趣的事是在调用call( )  打印左括号和msg字符串后,调用Thread.sleep(1000),该方法使当前线程暂停1秒。

下一个类的构造函数Caller,引用了Callme的一个实例以及一个String,它们被分别存在target  和 msg  中。构造函数也创建了一个调用该对象的run( )方法的新线程。该线程立即启动。 Caller类的run( )方法通过参数msg字符串调用Callme实例target的call( )  方法。 最后,Synch类由创建Callme的一个简单实例和Caller的三个具有不同消息字符串的实例开始。Callme的同一实例传给每个Caller实例。

// This program is not synchronized.

class Callme {

  voidcall(String msg) {

   System.out.print("[" + msg);

    try {

     Thread.sleep(1000);

    }catch(InterruptedException e) {

     System.out.println("Interrupted");

    }

   System.out.println("]");

  }

}

 

class Caller implements Runnable {

  String msg;

  Callmetarget;

  Thread t;

 

  publicCaller(Callme targ, String s) {

    target =targ;

    msg = s;

    t = newThread(this);

   t.start();

  }

 

  public voidrun() {

   target.call(msg);

  }

}

 

class Synch {

  publicstatic void main(String args[]) {

    Callmetarget = new Callme();

    Callerob1 = new Caller(target, "Hello");

    Callerob2 = new Caller(target, "Synchronized");

   Caller ob3= new Caller(target, "World");

 

    // waitfor 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( )方法允许执行转换到另一个线程。该结果是三个消息字符串的混合输出。该程序中,没有阻止三个线程同时调用同一对象的同一方法的方法存在。这是一种竞争,因为三个线程争着完成方法。例题用sleep( )使该影响重复和明显。

在大多数情况,竞争是更为复杂和不可预知的,因为你不能确定何时上下文转换会发生。这使程序时而运行正常时而出错。为达到上例所想达到的目的,必须有权连续的使用call( )。也就是说,在某一时刻,必须限制只有一个线程可以支配它。为此,你只需在call() 定义前加上关键字synchronized,如下:

class Callme {

    synchronizedvoid call(String msg) {

    ...

这防止了在一个线程使用call( )时其他线程进入call( )。 在synchronized加到call( )前面以后,程序输出如下:

[Hello]

[Synchronized]

[World]

任何时候在多线程情况下,你有一个方法或多个方法操纵对象的内部状态,都必须用synchronized关键字来防止状态出现竞争。记住,一旦线程进入实例的同步方法,没有其他线程可以进入相同实例的同步方法。然而,该实例的其他不同步方法却仍然可以被调用。

同步语句    

 

尽管在创建的类的内部创建同步方法是获得同步的简单和有效的方法,但它并非在任何时候都有效。这其中的原因,请跟着思考。假设你想获得不为多线程访问设计的类对象的同步访问,也就是,该类没有用到synchronized方法。而且,该类不是你自己,而是第三方创建的,你不能获得它的源代码。这样,你不能在相关方法前加synchronized修饰符。怎样才能使该类的一个对象同步化呢?很幸运,解决方法很简单:你只需将对这个类定义的方法的调用放入一个synchronized块内就可以了。

下面是synchronized语句的普通形式:

synchronized(object) {

   // statements tobe synchronized

  }

 

其中,object是被同步对象的引用。如果你想要同步的只是一个语句,那么不需要花括号。一个同步块确保对object成员方法的调用仅在当前线程成功进入object管程后发生。

下面是前面程序的修改版本,在run( )方法内用了同步块:

// This program uses a synchronized block.

class Callme {

  voidcall(String msg) {

    System.out.print("["+ msg);

    try {

     Thread.sleep(1000);

    } catch(InterruptedException e) {

     System.out.println("Interrupted");

    }

   System.out.println("]");

  }

}

 

class Caller implements Runnable {

  String msg;

  Callmetarget;

  Thread t;

 

  publicCaller(Callme targ, String s) {

    target =targ;

    msg = s;

    t = newThread(this);

   t.start();

  }

 

  //synchronize calls to call()

  public voidrun() {

   synchronized(target) { // synchronized block

      target.call(msg);

    }

  }

}

 

class Synch1 {

  publicstatic void main(String args[]) {

    Callmetarget = new Callme();

    Callerob1 = new Caller(target, "Hello");

    Callerob2 = new Caller(target, "Synchronized");

    Callerob3 = new Caller(target, "World");

 

    // waitfor threads to end

    try {

      ob1.t.join();

      ob2.t.join();

      ob3.t.join();

    } catch(InterruptedException e) {

     System.out.println("Interrupted");

    }

  }

}

 

这里,call( )方法没有被synchronized修饰。而synchronized是在Caller类的run( )方法中声明的。这可以得到上例中同样正确的结果,因为每个线程运行前都等待先前的一个线程结束。

线程间通信     

 

上述例题无条件的阻塞了其他线程异步访问某个方法。Java对象中隐式管程的应用是很强大的,但是你可以通过进程间通信达到更微妙的境界。这在Java中是尤为简单的。

像前面所讨论过的,多线程通过把任务分成离散的和合乎逻辑的单元代替了事件循环程序。线程还有第二优点:它远离了轮询。轮询通常由重复监测条件的循环实现。一旦条件成立,就要采取适当的行动。这浪费了CPU时间。举例来说,考虑经典的序列问题,当一个线程正在产生数据而另一个程序正在消费它。为使问题变得更有趣,假设数据产生器必须等待消费者完成工作才能产生新的数据。在轮询系统,消费者在等待生产者产生数据时会浪费很多CPU周期。一旦生产者完成工作,它将启动轮询,浪费更多的CPU时间等待消费者的工作结束,如此下去。很明显,这种情形不受欢迎。

为避免轮询,Java包含了通过wait(),notify( )和notifyAll( )方法实现的一个进程间通信机制。这些方法在对象中是用final方法实现的,所以所有的类都含有它们。这三个方法仅在synchronized方法中才能被调用。尽管这些方法从计算机科学远景方向上来说具有概念的高度先进性,实际中用起来是很简单的:

· wait( )  告知被调用的线程放弃管程进入睡眠直到其他线程进入相同管程并且调用notify( )。

· notify( ) 恢复相同对象中第一个调用 wait( ) 的线程。

· notifyAll( ) 恢复相同对象中所有调用 wait( ) 的线程。具有最高优先级的线程最先运行。

这些方法在Object中被声明,如下所示:

final void wait( ) throws InterruptedException

final void notify( )

final void notifyAll( )

wait( )存在的另外的形式允许你定义等待时间。

下面的例子程序错误的实行了一个简单生产者/消费者的问题。它由四个类组成:Q,设法获得同步的序列;Producer,产生排队的线程对象;Consumer,消费序列的线程对象;以及PC,创建单个Q,Producer,和Consumer的小类。

// An incorrect implementation of a producer andconsumer.

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);

  }

}

 

class Producer implements Runnable {

  Q q;

 

  Producer(Qq) {

    this.q =q;

    newThread(this, "Producer").start();

  }

 

  public voidrun() {

    int i =0;

 

   while(true) {

     q.put(i++);

    }

  }

}

 

class Consumer implements Runnable {

  Q q;

 

  Consumer(Qq) {

    this.q =q;

    newThread(this, "Consumer").start();

  }

 

  public voidrun() {

   while(true) {

     q.get();

    }

  }

}

 

class PC {

  publicstatic void main(String args[]) {

    Q q = newQ();

    new Producer(q);

    newConsumer(q);

 

   System.out.println("Press Control-C to stop.");

  }

}

尽管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后,消费者依次获得同样的1五次。生产者在继续生成2到7,消费者没有机会获得它们。

用Java正确的编写该程序是用wait()和notify( )来对两个方向进行标志,如下所示:

// A correct implementation of a producer andconsumer.

class Q {

  int n;

  booleanvalueSet = false;

 

 synchronized int get() {

   if(!valueSet)

      try {

       wait();

 

     }catch(InterruptedException e) {

       System.out.println("InterruptedException caught");

      }

 

     System.out.println("Got: " + n);

     valueSet = false;

      notify();

      returnn;

  }

 

 synchronized void put(int n) {

   if(valueSet)

      try {

       wait();

      }catch(InterruptedException e) {

       System.out.println("InterruptedException caught");

      }

 

      this.n= n;

      valueSet = true;

     System.out.println("Put: " + n);

     notify();

  }

}

 

class Producer implements Runnable {

  Q q;

 

  Producer(Qq) {

    this.q =q;

    newThread(this, "Producer").start();

  }

 

  public voidrun() {

    int i =0;

 

   while(true) {

     q.put(i++);

    }

  }

}

 

class Consumer implements Runnable {

  Q q;

 

  Consumer(Q q) {

    this.q = q;

    newThread(this, "Consumer").start();

  }

 

  public voidrun() {

   while(true) {

     q.get();

    }

  }

}

 

class PCFixed {

  publicstatic void main(String args[]) {

    Q q = newQ();

    newProducer(q);

    newConsumer(q);

 

   System.out.println("Press Control-C to stop.");

  }

}

内部get( ), wait( )被调用。这使执行挂起直到Producer  告知数据已经预备好。这时,内部get( )  被恢复执行。获取数据后,get( )调用notify( )。这告诉Producer可以向序列中输入更多数据。在put( )内,wait( )挂起执行直到Consumer取走了序列中的项目。当执行再继续,下一个数据项目被放入序列,notify( )被调用,这通知Consumer它应该移走该数据。

下面是该程序的输出,它清楚的显示了同步行为:

Put: 1

Got: 1

Put: 2

Got: 2

Put: 3

Got: 3

Put: 4

Got: 4

Put: 5

Got: 5

 

死锁    

 

需要避免的与多任务处理有关的特殊错误类型是死锁(deadlock)。死锁发生在当两个线程对一对同步对象有循环依赖关系时。例如,假定一个线程进入了对象X的管程而另一个线程进入了对象Y的管程。如果X的线程试图调用Y的同步方法,它将像预料的一样被锁定。而Y的线程同样希望调用X的一些同步方法,线程永远等待,因为为到达X,必须释放自己的Y的锁定以使第一个线程可以完成。死锁是很难调试的错误,因为:

· 通常,它极少发生,只有到两线程的时间段刚好符合时才能发生。

· 它可能包含多于两个的线程和同步对象(也就是说,死锁在比刚讲述的例子有更多复杂的事件序列的时候可以发生)。

为充分理解死锁,观察它的行为是很有用的。下面的例子生成了两个类,A和B,分别有foo( )和bar( )方法。这两种方法在调用其他类的方法前有一个短暂的停顿。主类,名为Deadlock,创建了A和B的实例,然后启动第二个线程去设置死锁环境。foo( )和bar( )方法使用sleep()强迫死锁现象发生。

// An example of deadlock.

class A {

 synchronized void foo(B b) {

    Stringname = Thread.currentThread().getName();

 

   System.out.println(name + " entered A.foo");

 

    try {

     Thread.sleep(1000);

    }catch(Exception 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");

  }

}

 

class B {

 synchronized void bar(A a) {

    Stringname = Thread.currentThread().getName();

   System.out.println(name + " entered B.bar");

 

    try {

     Thread.sleep(1000);

    }catch(Exception 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");

  }

}

 

class Deadlock implements Runnable {

  A a = newA();

  B b = newB();

 

  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");

  }

 

  public voidrun() {

    b.bar(a);// get lock on b in other thread.

   System.out.println("Back in other thread");

  }

 

  public staticvoid main(String args[]) {

    newDeadlock();

  }

}

运行程序后,输出如下:

MainThread entered A.foo

RacingThread entered B.bar

MainThread trying to call B.last()

RacingThread trying to call A.last()

因为程序死锁,你需要按CTRL-C来结束程序。在PC机上按CTRL-BREAK (或在Solaris下按CTRL-\)你可以看到全线程和管程缓冲堆。你会看到RacingThread在等待管程a时占用管程b,同时,MainThread占用a等待b。该程序永远都不会结束。像该例阐明的,你的多线程程序经常被锁定,死锁是你首先应检查的问题。

挂起、恢复和终止线程     

 

有时,线程的挂起是很有用的。例如,一个独立的线程可以用来显示当日的时间。如果用户不希望用时钟,线程被挂起。在任何情形下,挂起线程是很简单的,一旦挂起,重新启动线程也是一件简单的事。

挂起,终止和恢复线程机制在Java 2和早期版本中有所不同。尽管你运用Java 2的途径编写代码,你仍需了解这些操作在早期Java环境下是如何完成的。例如,你也许需要更新或维护老的代码。你也需要了解为什么Java 2会有这样的变化。因为这些原因,下面内容描述了执行线程控制的原始方法,接着是Java2的方法。

Java 1.1或更早版本的线程的挂起、恢复和终止    

 

先于Java2的版本,程序用Thread定义的suspend() 和 resume()  来暂停和再启动线程。

它们的形式如下:

final void suspend( )

final void resume( )

下面的程序描述了这些方法:

// Using suspend() and resume().

class NewThread implements Runnable {

  Stringname; // name of thread

  Thread t;

 

 NewThread(String threadname) {

    name =threadname;

    t = newThread(this, name);

   System.out.println("New thread: " + t);

   t.start(); // Start the thread

  }

 

  // This isthe entry point for thread.

  public voidrun() {

    try {

      for(inti = 15; i > 0; i--) {

       System.out.println(name + ": " + i);

       Thread.sleep(200);

      }

    } catch(InterruptedException e) {

     System.out.println(name + " interrupted.");

    }

   System.out.println(name + " exiting.");

  }

}

 

class SuspendResume {

  publicstatic void main(String args[]) {

    NewThreadob1 = new NewThread("One");

    NewThreadob2 = new NewThread("Two");

 

    try {

     Thread.sleep(1000);

     ob1.t.suspend();

     System.out.println("Suspending thread One");

     Thread.sleep(1000);

     ob1.t.resume();

     System.out.println("Resuming thread One");

     ob2.t.suspend();

     System.out.println("Suspending thread Two");

     Thread.sleep(1000);

     ob2.t.resume();

     System.out.println("Resuming thread Two");

    } catch(InterruptedException e) {

     System.out.println("Main thread Interrupted");

    }

 

    // waitfor 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("Mainthread exiting.");

  }

}

程序的部分输出如下:

New thread: Thread[One,5,main]

One: 15

New thread: Thread[Two,5,main]

Two: 15

One: 14

Two: 14

One: 13

Two: 13

One: 12

Two: 12

One: 11

Two: 11

Suspending thread One

Two: 10

Two: 9

Two: 8

Two: 7

Two: 6

Resuming thread One

Suspending thread Two

One: 10

One: 9

One: 8

One: 7

One: 6

Resuming 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.

Thread类同样定义了stop() 来终止线程。它的形式如下:

void stop( )

一旦线程被终止,它不能被resume()  恢复继续运行。

Java 2中挂起、恢复和终止线程    

 

Thread定义的suspend(),resume()和stop()方法看起来是管理线程的完美的和方便的方法,它们不能用于新Java版本的程序。下面是其中的原因。Thread类的suspend()方法在Java 2中不被赞成,因为suspend()有时会造成严重的系统故障。假定对关键的数据结构的一个线程被锁定的情况,如果该线程在那里挂起,这些锁定的线程并没有放弃对资源的控制。其他的等待这些资源的线程可能死锁。

Resume()方法同样不被赞同。它不引起问题,但不能离开suspend()方法而独立使用。

Thread类的stop()方法同样在Java 2中受到反对。这是因为该方法可能导致严重的系统故障。设想一个线程正在写一个精密的重要的数据结构且仅完成一个零头。如果该线程在此刻终止,则数据结构可能会停留在崩溃状态。

因为在Java 2中不能使用suspend(),resume()和stop()方法来控制线程,你也许会想那就没有办法来停止,恢复和结束线程。其实不然。相反,线程必须被设计以使run()方法定期检查以来判定线程是否应该被挂起,恢复或终止它自己的执行。有代表性的,这由建立一个指示线程状态的标志变量来完成。只要该标志设为“running”,run()方法必须继续让线程执行。如果标志设为“suspend”,线程必须暂停。若设为“stop”,线程必须终止。

当然,编写这样的代码有很多方法,但中心主题对所有的程序应该是相同的。 下面的例题阐述了从Object继承的wait()和notify()方法怎样控制线程的执行。该例与前面讲过的程序很像。然而,不被赞同的方法都没有用到。让我们思考程序的执行。

NewTread  类包含了用来控制线程执行的布尔型的实例变量suspendFlag。它被构造函数初始化为false。Run()方法包含一个监测suspendFlag的同步声明的块。如果变量是true,wait()方法被调用以挂起线程。Mysuspend()方法设置suspendFlag为true。Myresume()方法设置suspendFlag为false并且调用notify()方法来唤起线程。最后,main()方法被修改以调用mysuspend()和myresume()方法。

// Suspending and resuming a thread for Java2

class NewThread implements Runnable {

  Stringname; // name of thread

  Thread t;

  booleansuspendFlag;

  

 NewThread(String threadname) {

    name =threadname;

    t = newThread(this, name);

   System.out.println("New thread: " + t);

   suspendFlag = false;

   t.start(); // Start the thread

  }

 

  // This isthe entry point for thread.

  public voidrun() {

    try {

      for(inti = 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.");

  }

 

  voidmysuspend() {

   suspendFlag = true;

  }

 

 synchronized void myresume() {

   suspendFlag = false;

    notify();

  }

}

 

class SuspendResume {

  publicstatic void main(String args[]) {

    NewThreadob1 = new NewThread("One");

    NewThreadob2 = new NewThread("Two");

 

    try {

     Thread.sleep(1000);

     ob1.mysuspend();

     System.out.println("Suspending thread One");

      Thread.sleep(1000);

     ob1.myresume();

     System.out.println("Resuming thread One");

     ob2.mysuspend();

     System.out.println("Suspending thread Two");

     Thread.sleep(1000);

     ob2.myresume();

     System.out.println("Resuming thread Two");

    } catch(InterruptedException e) {

     System.out.println("Main thread Interrupted");

    }

 

    // waitfor 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.");

  }

}

 

该程序的输出与前面的程序相同。此书的后面部分,你将看到用Java 2机制控制线程的更多例子。尽管这种机制不像老方法那样“干净”,然而,它是确保运行时不发生错误的方法。它是所有新的代码必须采用的方法。

 

使用多线程机制     

 

如果你和大多数程序员一样, 那么在语言中加入多线程支持对你来说是很新鲜的事物。

有效运用这种支持的关键是并发思考而不是连续思考。例如,当你的程序有两个可以并行执行的子系统,创建它们各自的线程。仔细的运用多线程,你能编写出非常有效的程序。

然而要注意:如果你创建太多的线程,你可能会减弱而不是加强程序的性能。记住,上下文转换是需要开销的。如果你创建了太多的线程,更多的CPU时间会用于上下文转换而不是用来执行程序。


本文由 @小职 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程