第 10章 多线程线程( thread)是指程序中完成一个任务的从头到尾的执行线索。到现在为止,前面所涉及的程序都是单线程运行的。但现实世界中的很多过程其实具备多条线索同时执行的特点。如 Internet上的服务器可能需要同时响应多个客户机的请求。
多线程是指同时存在几个执行体,按几条不同的执行线索共同工作的情况。 Java语言允许在一个程序中并发地运行多个线程,使得编程人员可以很方便地开发具有多线程功能、能同时处理多个任务的功能强大的应用程序。虽然说线程是同步执行的,但在实际的情况是单处理器的计算机在任何给定的时刻只能执行多个线程中的一个。
多线程可以使程序反应更快、交互性更强,并能提高执行效率。
第 10章 多线程
Java中的线程
线程的生命周期
线程的优先级和调度管理
扩展 Thread类创建线程
Runnable接口
常用方法
线程同步
线程组
并发现象在现实生活中大量存在
人体(消化、运动)
计算机(同时运行多中程序)
多线程 —— 在一个程序中实现并发
编程语言一般提供了串行程序设计的方法
计算机的并发能力由操作系统提供
Java在语言级提供多线程并发的概念
10.1 Java中的线程以前所编写的程序,每个程序都有一个入口、一个出口以及一个顺序执行的序列,在程序执行过程中的任何指定时刻,
都只有一个单独的执行点。
事实上,在单个程序内部是可以在同一时刻进行多种运算的,这就是所谓的多线程(这与多任务的概念有相似之处)。
一个单独的线程和顺序程序相似,也有一个入口、一个出口以及一个顺序执行的序列,从概念上说,一个线程是一个程序内部的一个顺序控制流。
线程并不是程序,它自己本身并不能运行,必须在程序中运行。在一个程序中可以实现多个线程,这些线程同时运行,
完成不同的功能。
将军令 多线程
10.1 Java中的线程
10.1 Java中的线程从逻辑的观点来看,多线程意味着一个程序的多行语句同时执行,但是多线程并不等于多次启动一个程序,操作系统也不会把每个线程当作独立的进程来对待:
两者的粒度不同,是两个不同层次上的概念。进程是由操作系统来管理的,而线程则是在一个程序(进程)内。
不同进程的代码、内部数据和状态都是完全独立的,而一个程序内的多线程是共享同一块内存空间和同一组系统资源,有可能互相影响。
线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程切换的负担要小。
10.1 Java中的线程
进程:程序的一次执行。
程序代码
程序数据
程序资源
线程:进程中程序代码的一个执行序列。
程序调用堆栈
线程局部变量
可共享访问进程中的数据和资源
操作系统按线程来调度程序的执行
10.1 Java中的线程文件 输入输出装置各种系统资源数据区段程序区段只有一个地方在执行文件 输入输出装置各种系统资源数据区段程序区段同时有数个地方在执行传统的进程 多线程的任务
10.1 Java中的线程多线程的优势:
多线程编程简单,效率高( 能直接共享数据和资源,多进程不能)
适合于开发服务程序( 如 Web服务,聊天服务等)
适合于开发有多种交互接口的程序( 如聊天程序的客户端,网络下载工具)
适合于有人机交互又有计算量的程序( 如字处理程序 Word,Excel)
减轻编写交互频繁、涉及面多的程序的困难 (如监听网络端口)
程序的吞吐量会得到改善 (同时监听多种设备,如网络端口、串口、并口以及其他外设)
有多个处理器的系统,可以并发运行不同的线程 (否则,任何时刻只有一个线程在运行)
10.1 Java中的线程虽然各种操作系统( Unix/Linux,Windows系列等)都支持多线程,但若要用 C,C++或其他语言编写多线程程序是十分困难的,因为它们对 数据同步 的支持不充分。
对多线程的综合支持是 Java语言的一个重要特色,它提供了 Thread类来实现多线程。在 Java中,线程可以认为是由三部分组成的:
虚拟 CPU,封装在 java.lang.Thread类中,它控制着整个线程的运行;?
执行的代码,传递给 Thread类,由 Thread类控制顺序执行;?
处理的数据,传递给 Thread类,是在代码执行过程中所要处理的数据。
10.1 Java中的线程
10.2 线程的生命周期线程是程序内部的一个顺序控制流,它具有一个特定的生命周期。在一个线程的生命周期中,
它总处于某一种状态中。线程的状态表示了线程正在进行的活动以及在这段时间内线程能完成的任务。
new Thread()
New Thread Runnable
start() Not Runnable
stop() stop()
Dead
yield()
stop()
run()exit
.,
suspend()
sleep()
wait()
I/O流阻塞
resume()
notify()/notifyAll()
I/O指令
.
waiting
sleeping
suspending
blocked
ready
running
10.2 线程的生命周期
born
ready
running
waiting sleeping dead blocked
start()
dispatchquantum expiration
yield()
wait
sleep() run complete
issue I/O request
wait interval expires
notify()
notifyAll()
interrupt()
I/O complete
sleep interval expires
interrupt()
10.2 线程的生命周期
创建状态( new Thread)
当创建了一个新的线程时( myThread thd = new myThread(); ),它就处于创建状态,此时它仅仅是一个空的线程对象,系统不为它分配资源。处于这种状态时只能 启动 或 终止 该线程,调用除这两种以外的其它 线程状态相关的方法 都会失败并且会引起非法状态例外 IllegalThreadStateException(对于其它状态,若所调用的方法与状态不符,都会引起非法状态例外)。
10.2 线程的生命周期
可运行状态( Runnable)
当线程处于创建状态时,可以调用 start()方法( thd.start(); )
来启动它,产生运行这个线程所需的系统资源,安排其运行,
并调用线程体 run()方法,这样就使得该线程处于可运行
( Runnable )状态。
需要注意的是这一状态并不是运行中状态( Running ),因为线程也许实际上并未真正运行( Ready)。由于很多计算机都是单处理器的,所以要在同一时刻运行所有的处于可运行状态的线程是不可能的,Java运行系统必须实现调度来保证这些线程共享处理器。但是在大多数情况下,可运行状态也就是运行中。当一个线程正在运行时,它是可运行的,并且也是当前正运行的线程。
10.2 线程的生命周期
不可运行状态( Not Runnable)
线程处于 可运行状态时,当下面四种情况发生,线程就进入不可运行状态:
调用了 sleep()方法;
调用了 suspend()方法;
为等候一个条件变量,线程调用 wait()方法;
输入输出流中发生线程阻塞。
Thread myThread = new MyThreadClass();
myThread.start();
try
{
myThread.sleep(10000);
} catch (InterruptedException e){
}
10.2 线程的生命周期
不可运行状态( Not Runnable)
对于这四种使得线程处于不可运行状态的情况,都有特定的方法使线程返回可运行状态:
如果线程处于睡眠状态中,sleep()方法中的参数为休息时间,当这个时间过去后,线程即为可运行的;
如果一个线程被挂起,须调用 resume()方法来返回;
如果线程在等待条件变量,那么要停止等待的话,需要该条件变量所在的对象调用 notify()或 notifyAll()方法 ;
如果在 I/O流中发生线程阻塞,则特定的 I/O指令将结束这种不可运行状态。
需要注意的是每种方法都仅仅对相应的情况才有作用,例如当一个线程睡眠并且睡眠时间还没有结束时,调用 resume()
方法是无效的,并且还会引起非法状态例外。
10.2 线程的生命周期
死亡状态( Dead)
线程的终止一般可通过两种方法实现:自然撤消或是被停止。自然撤消是指从线程的 run()方法正常退出;而调用线程的实例方法 stop()则可以强制停止当前线程。
//线程自然撤销
public void run()
{
int i = 0;
while (i < 100)
{
i++;
System.out.println("i = " + i);
}
}
//线程被强行停止
myThread thd = new MyThread();
thd.start( );
try
{
thd.sleep(10000);
} catch (InterruptedException e){
}
thd.stop( );
10.2 线程的生命周期可以通过在其他线程中调用 stop()方法来终止线程,也可以由线程自己调用 stop()方法,自我终止。
如果希望线程正常终止,可采用标记来使线程中的 run()
方法退出。
//线程自我终止
public void run()
{
while ( true )
{
… //完成线程的特定功能
if( time_to_die )
{
Thread.currentThread().stop();
}
}
}
10.2 线程的生命周期
isAlive()方法,可以用来判断线程目前是否正在执行中。如果线程已被启动并且未被终止,那么 isAlive( )返回 true,但该线程是可运行或是不可运行的,是不能作进一步的分辨。如果返回 false,则该线程是新创建或是已被终止的(同样不能作进一步的分辨)。
join()方法,等待线程执行完毕。
yield()方法,将执行的权力交给其它优先级相同的线程,当前执行线程到可运行线程队列( ready)的最后等待,若队列空,该方法无效。
myThread thd = new MyThread();
thd.start();

thd.join(); //等待线程 thd执行完后再继续往下执行

join(int time):
最多等待 time所指定的时间。
10.2 线程的生命周期
sleep()方法可以暂停线程的执行,让其它线程得到机会。
但 sleep()要丢出例外 InterruptedException,必须抓住。
try
{
sleep(100)
}catch(InterruptedException e){
…// 本例外可不作处理
}
suspend()方法和 resume()可以用来暂停线程或恢复线程。
可以由线程自身在线程体中调用 suspend()方法暂停自己,也可以在其他线程中通过线程实例调用 suspend()方法暂停线程的执行。但是要恢复由 suspend()方法暂停的线程,只能在其他线程中通过线程实例调用 resume()方法。
10.2 线程的生命周期
Java提供一个线程调度器来监控程序中启动后进入可运行状态的所有线程。线程调度器按照线程的 优先级 决定调度哪些线程来执行,具有高优先级的线程会在较低优先级的线程之前得到执行。同时线程的调度是 抢先式 的,即如果当前线程在执行过程中,一个具有更高优先级的线程进入可执行状态,则该告优先级的线程会被立即调度执行。
多个线程运行时,若线程的优先级相同,由操作系统按时间片轮转 方式或 独占方式 来分配线程的执行时间。
10.3 线程的优先级和调度管理在 Java中线程的优先级是用数字来表示的,分为三个级别:
低优先级,Thread.MIN_PRIORITY,数值为 1 (2~4)
缺省优先级,Thread,NORM_PRIORITY,数值为 5
高优先级,Thread.MAX_PRIORITY,数值为 10 (6~9)
具有相同优先级的多个线程,若它们都为高优先级
Thread.MAX_PRIORITY,则每个线程都是独占式的,也就是说这些线程将被顺序执行;若该优先级不为高优先级,则这些线程将同时执行,也就是说这些线程的执行是无序的。
线程被创建后,其缺省的优先级是缺省优先级 Thread,
NORM_PRIORITY。可以用方法 int getPriority()来获得线程的优先级,同时也可以用方法 void setPriority( int p ) 在线程被创建后改变线程的优先级。
10.3 线程的优先级和调度管理
线程离开执行状态,
如果:
线程结束
线程处于 I/O等待
线程调用 sleep
线程调用 wait
线程调用 join
线程调用 yield
有更高优先级的线程
时间片用完
Priority 10
Priority 9
Priority 8
Priority 7
Priority 6
Priority 5
Priority 4
Priority 3
Priority 2
Priority 1
A B
C
D E F
G
H I
J K
ThreadPriority.java
10.3 线程的优先级和调度管理
10.4 扩展 Thread类创建线程
Java的线程是通过 Java的软件包 java.lang中定义的类 Thread来实现的。当生成一个 Thread类的对象之后,就产生了一个线程,通过该对象实例,可以启动线程、终止线程、或者暂时挂起线程等。
Thread类本身只是线程的 虚拟 CPU;
线程所执行的代码(或者说线程所要完成的功能)是通过方法 run( )(包含在一个特定的对象中)来完成的,方法 run( )
称为 线程体 。实现线程体的特定对象是在初始化线程时传递给线程的。
在一个线程被建立并初始化以后,Java的运行时系统就自动调用 run( )方法,正是通过 run( )方法才使得建立线程的目的得以实现。
通常,run( )方法是一个循环,例如一个播放动画的线程要循环显示一系列图片。有时,run( )方法会执行一个时间较长的操作,例如下载并播放一个 JPEG格式的电影。
10.4 扩展 Thread类创建线程先来看看线程对象的初始化,类 Thread的构造方法如下:
public Thread( ThreadGroup group,Runnable target,String name)
group指明了线程所属的线程组;
target是线程体 run()方法所在的对象;
name是线程的名称。
target必须实现接口 Runnable。在接口 Runnable中只定义了一个方法 void run()作为线程体。任何实现接口 Runnable的对象都可以作为一个线程的目标对象。
类 Thread本身也实现了接口 Runnable(空实现),因此,
上述构造方法中各参数都可以为 null。
10.4 扩展 Thread类创建线程从 Thread类的构造方法可以看出,用户可以有两种方法构造自己的 run( )方法。
创建线程方法一:
定义一个线程类,它继承类 Thread并重写其中的方法
run()。这时在初始化这个类的实例时,目标对象 target可以为 null,表示这个实例本身具有线程体。由于 Java只支持单继承,用这种方法定义的类不能再继承其他类。
10.4 扩展 Thread类创建线程
class SimpleThread extends Thread
{
public SimpleThread(String str)
{
super(str);
}
public void run()
{
for (int i = 0; i < 10; i++)
{
System.out.println(i + " " + getName());
try{
sleep((int)(Math.random() * 1000));
} catch (InterruptedException e){
}
}
System.out.println("DONE! " + getName());
}
}
10.4 扩展 Thread类创建线程 — 创建线程的方式示例创建线程方法二:
提供一个实现接口 Runnable的类作为线程的目标对象。
在初始化一个 Thread类或子类生成线程实例时,把目标对象传递给这个线程实例,由该目标对象提供线程体 run()方法。
这时,实现接口 Runnable的类还可以再继承其他类,即实现接口 Runnable的类可以不单纯是提供线程体。
10.5 Runnable接口
run ()方法是运行线程的主体,启动线程时,由 java直接调用 public void run()。
使用 Runnable接口可以将线程的虚拟 CPU、代码和数据分开,形成一个比较清晰的模型。
使用 Runnable接口使得包含线程体的类还可以继承其他类。
直接继承 Thread类以后不能再继承其他类,但编写简单,
并可直接操纵线程;使用 Runnable接口时,若要在 run()方法中操纵线程,必须使用 Thread.currentThread()方法。
在具体应用中,采用哪种方法来构造线程体要根据具体情况而定。通常,当一个线程体所在的类已经继承了另一个类时,就应该用实现 Runnable接口的方法。
10.5 Runnable接口
Thread类的方法
void run()
线程所执行的代码
void start() throws IllegalThreadStateException
使程序开始执行,多次调用会产生例外
void sleep(long milis)
让线程睡眠一段时间,此期间线程不消耗 CPU资源
void interrupt()
中断线程
static boolean interrupted()
判断当前线程是否被中断(会清除中断状态标记)
boolean isInterrupted()
判断指定线程是否被中断线程状态控制方法
10.6 常用方法
Thread类的方法
boolean isAlive()
判断线程是否处于活动状态(即已调用 start,但 run还未返回)
static Thread currentThread()
返回当前线程对象的引用
void setName(String threadName)
改变线程的名字
String getName()
获得线程的名字
String toString()
例子,Thread[Thread-0,1,test]
void join([long millis[,int nanos]])
等待线程结束
10.6 常用方法
Thread类的方法
void destroy()
销毁线程
void stop()
终止线程的执行
void suspend() / void resume()
挂起线程 /
static void yield()
暂停当前线程,让其他线程执行
void setDaemon(boolean on) / void setPriority(int p)
获得线程的名字
notify() / notifyAll() / wait() - 从 Object继承而来
10.6 常用方法由于 Java支持多线程,具有并发功能,从而大大提高了计算机的处理能力。在各线程之间,不存在共享资源的情况下,几个线程的执行顺序可以是随机的。但是,当两个或两个以上的线程需要共享同一资源时,线程之间的执行顺序就需要协调,并且在某个线程占用这一资源时,其他线程只能等待。
如生产者和消费者的问题,只有当生产者生产出产品并将其放入商店货架后,消费者才能从货架上取走产品进行消费。当生产者没有生产出产品时,消费者是没法消费的。同理,当生产者生产的产品堆满货架时,应该暂停生产,等待消费者消费。
10.7 线程同步由于 Java支持多线程,具有并发功能,从而大大提高了计算机的处理能力。在各线程之间,不存在共享资源的情况下,几个线程的执行顺序可以是随机的。但是,当两个或两个以上的线程需要共享同一资源时,线程之间的执行顺序就需要协调,并且在某个线程占用这一资源时,其他线程只能等待。
如生产者和消费者的问题,只有当生产者生产出产品并将其放入商店货架后,消费者才能从货架上取走产品进行消费。当生产者没有生产出产品时,消费者是没法消费的。同理,当生产者生产的产品堆满货架时,应该暂停生产,等待消费者消费。
10.7 线程同步在处理线程同步时,访问资源的程序段使用关键字 synchronized
来修饰,并通过一个称为监控器的系统软件来管理。当执行被
synchronized修饰的程序段时,监控器将这段程序访问的资源加锁,此时,称该线程占有资源。在这个程序段调用执行完毕之前,其他占有 CPU资源的线程一旦调用这个程序段时就会引发堵塞,堵塞的线程要一直等到堵塞的原因消除,再排队等待
CPU资源,以便使用这个程序段。
关键字 synchronized修饰程序段的语法格式如下:
10.7 线程同步
synchronized [类 ] 方法或语句块一个线程组( thread group)是线程的一个集合。 Java系统的每个线程都属于某一个线程组。有些程序包含相当多的具有类似功能的线程,采用线程组结构后,可以将它们作为一个整体进行操作。例如,可以同时启动、挂起或者唤醒一个线程组中的所有线程。
多数情况下,一个线程属于哪个线程组是由编程人员在程序中指定,若编程人员没有指定,则 Java系统会自动将这些线程归于
main线程组。 main线程组是 Java系统启动时创建的。一个线程组不仅可以包含多个线程,而且线程组中还可以包含其他的线程组,构成树形结构。一个线程可以访问本线程组的有关信息,但无法访问本线程组的父线程组。
10.8 线程组