§ 2.3互斥与同步
内容提要
前面章节仅介绍了独立的、非同步的线程。也就
是说,每一个线程在运行时与其他线程的状态和
行为无关。但是,在实际应用中,多个线程之间
往往会共享一些数据,并且各线程之间的状态、
行为是互相影响的。线程间的相互影响有两种:
一种是线程间的互斥,另一种是线程间的同步。本节您将学习以下知识点,
– 线程安全
– 线程间的互斥
– 线程间的同步
线程安全( Thread- Safe)
线程之间的关系大致可以分为以下二类,
?线程之间通过对资源的竞争,包括共享的数据
和硬件资源,所产生的相互制约关系,这类线程
间的主要问题是互斥和死锁问题,这类关系被称
为 互斥 关系。
?线程之间互相协同合作,彼此之间直接知道对
方的存在,并了解对方的名字,这类进程常常需
要通过“进程间通信”方法来协同工作,这类关
系被称为 同步 关系。
线程安全( Thread- Safe)
? 共享资源 是指在程序中并发运行的若干个线程
所操作的同一个数据资源。
? 并发运行的若干个线程在操作共享资源时可能
会出错,通常把这种现象称为非线程安全。 反
之,则称为线程安全
线程安全( Thread- Safe)
? 示例 2.3.1 非线程安全程序示例
线程安全( Thread- Safe)
private int data=0;
//通过二次加法运算,得到一个偶数
public int nextData(){
data++;
data++;
return data; //返回所生成的偶数
}
while(true){
int val=data.nextData();//获取一个新生成的偶数
if(val%2!= 0){//如果生成的是奇数,则报错退出
System.out.println ("出现共享数据访问冲突,"+val);
System.exit(0);
}
}
线程安全( Thread- Safe)
?运行结果
2.3.2线程间的互斥
? 出现共享资源访问冲突的实质是线程 没有互斥
的使用共享资源,即单个线程需要独占性的使
用共享资源。
? 共享资源也称为“临界资源” (critical
resource),而将线程中访问共享变量的代码段
称为 临界段( critical sections)。
2.3.2线程间的互斥
为了使系统中并行线程正确而有效的访问临界资源,对
线程互斥使用临界段有以下原则,
①在共享同一个临界资源的所有线程中,每次只允许有
一个线程处于它的临界段之中。也就是说强制所有这些
线程中,每次只允许其中的一个线程访问该共享变量。
②线程只应在临界段内逗留有限时间。
③若有多个线程同时要求进入它们的临界段时,就在有
限的时间内让其中之一进入临界段,而不应相互阻塞,
以至于各线程均无法进入临界段。
2.3.2线程间的互斥
? 为了能够对临界段实现互斥,计算机科学家
提出使用“同步原语”来访问“信号量”的
方式来解决。
?,信号量( semaphore)”是指用于协调多
个相互关联线程操作的信号,当一个线程需
要访问临界段时,将被强制地停在临界段的
访问入口处,直到收到一个专门的信号,才
能进入临界段。
2.3.2线程间的互斥
? ―同步原语” 通常用来保证临界段代码执行 的原子性,
即不可中断性。
? Java中关键字,synchronized‖用于实现语句的同步操作,
即给共享资源加互斥锁
? 如下列动画所示,62-2.swf
2.3.2线程间的互斥
? 为共享资源加互斥锁的方法有两种
① 锁定一个对象和一段代码
声明格式为,
synchronized(<对象名 >){
<语句组 >

2.3.2线程间的互斥
?示例 2.3.2 使用 synchronized关键字解决共享资源
访问冲突(锁定对象)
while(true){
synchronized (data){//锁定共享资源
val =data.nextData();//获取一个新生成的偶数
}
if(val%2!= 0){//如果生成的是奇数,则报错退出
System.out.println ("出现共享数据访问冲突,"+val);
System.exit(0);
}
}
运行程序,
2.3.2线程间的互斥
② 锁定一个方法
声明格式为,
synchronized <方法声明 >{
<方法体 >

2.3.2线程间的互斥
示例 2.3.3 使用 synchronized关键字解决共享资源访
问冲突 ( 锁定方法 )
public synchronized int nextData(){//锁定方法
data++; //进行第一次加法
data++; //进行第二次加法
return data; //返回所生成的偶数
}
2.3.2线程间的互斥
?无论是对方法加互斥锁,还是对对象加互斥锁,其实
质都是实现对共享资源的互斥访问
?“互斥”操作是以降低应用程序的并发程度为代价的
?因此,在编写多线程程序中,对,synchronized‖的使
用就遵循以下 2个原则,
– 不需要在多个线程中使用共享资源时,那么就没有必要使用
该关键字;
– 如果某个方法只是返回对象的值,而不去修改对象的值时,
那么也没有必要使用该关键字。
线程间的同步
? 通常一个任务可能被分为若干个可并行执行的
线程,这些线程是异步的、相对独立的向前推
进。但由于它们是协同地完成一个共同的任务,
所以它们之间应保持一定的联系,以便协调地
完成任务,这种联系就是指在线程间交换一定
数量的信息 ——称为线程间通信。
? 线程间的同步关系可以用生产者和消费者问题
来说明
线程间的同步
?生产者和消费者问题是荷兰计算机科学家
Dijkstra于 1968年提出的。消费者是一个使用
资源的线程,如执行取数据操作的线程,而生
产者则是一个制造并释放资源的线程,如执行
修改数据操作的线程。那么如何在二者之间进
行协同操作的问题就称为生产者和消费者问题。
线程间的同步
在生产者和消费者问题中,有以下一些假定,
1,生产者在生产时,受仓库容量或生产成本的限
制,在生产一定货物后,必须等消费者将已生
产的产品消费完毕,才能继续生产。
2,消费者在消费时,只有在生产者生产产品后,
才能进行消费。
线程间的同步
示例 2.3.4 用计算机模拟生产者和消费者问题
(非同步)
类图,
线程间的同步
代码,
运行结果,
线程间的同步
从程序的运行结果可以看出存在二类错误,
? 消费者在生产者重新写入新数据之前又一次读
取数据所造成的重复读取错误,如消费者连续
3次消费数据 1。
? 生产者在消费者读出前一个数据之前,又将新
的数据写入所造成的数据丢失错误,如生产者
连续生成数据 3,4,则数据 3将被丢失
线程间的同步
Java中 Object类提供了一组关于线程同步的方法
public final void wait(long timeout)
这个方法将使得执行该方法的线程对象被阻塞
注意,Tread类的 sleep方法和 Object类的 wait方法,均可
以用于将线程的状态由运行状态转为不可运行状态,
但二者在等待时间上是有区别的,sleep()方法的等待
时间是确定的,到时由系统唤醒,而 wait方法的等待时
间是不确定的,需要由线程通过 notify()/notifyAll()方法
来唤醒。
线程间的同步
public final void notify()
唤醒被 wait方法阻塞的单个线程
public final void notifyAll()
唤醒 全部 被 wait方法阻塞的线程
线程间的同步
线程同步的设计方法是
①设定信号量
②改写生产者的生产方法
满足条件,则生产,否则等待
③改写消费者的消费方法
满足条件,则消费,否则等待
线程间的同步
示例 2.3.5 用计算机模拟生产者和消费者问题 (同步 )
① 设定信号量
要使两者进行同步,其实质就是要使两者在执
行操作时,判断操作执行的条件是否得到满足,
因此需要增加一个变量作为判断条件
private boolean empty=true;//仓库是否为空
线程间的同步
② 改写生产者的生产方法
public synchronized void put(int goods){
while(!empty) {//如果不为空,则消费者还未消费产品
try{
System.out.println ("消费者还未消费产品,请等待,..");
wait();//等待,直到消费者消费产品才被唤醒
}catch(InterruptedException ie){
ie.printStackTrace();
}
… //生产条件满足,执行相应代码

empty=false;//生产者已生产产品,将空标记设为不空
notify();//通知其它可能被阻塞的线程
线程间的同步
③ 改写消费者的消费方法
public synchronized int get(){
while(empty) {//如果仓库为空,则生产者还未生产产品
try{
System.out.println ("生产者还未生产产品,请等待,..");
wait();//等待,直到生产者生产产品才被唤醒
}catch(InterruptedException ie){
ie.printStackTrace();
}
}
…// 消费条件满足,执行相应代码

empty=true;//消费者已消费产品,将空标记设为空
notify();//通知其它可能被阻塞的线程
线程间的同步
代码清单
运行结果
精练
在 HNS图书管理系统的文件读写模块中有 1个类
FileManager,用于执行用户记录文件的读取和写
入,现发现在多线程的应用情况下,通过该类读
写的文件,经常发生更新丢失错误,即某些在
UserDB中更新的信息,并没有记录在文件中。
任务:请您根据本节所学的线程同步知识,改写
该类的 readUser和 SaveUser方法。
小结
线程安全
?多个线程之间通常会共享一些数据,并且各线程之间的状态、行为是
互相影响的。线程之间的关系大致可以分为二类。
1,线程之间通过对资源的竞争,包括共享的数据和硬件资源,所产
生的互斥关系;
2,线程之间互相合作协同合作所产生的同步关系。
?共享资源 是指在程序中并发运行的若干个线程所操作的同一个数据资
源。
?并发运行的若干个线程在操作共享资源时可能会出错的现象称为非线
程安全,反之称为线程安全。
小结 (续 )
线程的互斥
?出现共享资源访问冲突的实质是线程 没有互斥
的使用共享资源,即单个线程需要独占性的使用
共享资源。
?为了能够对临界段实现互斥,计算机科学家提
出使用“同步原语”来访问“信号量”的方式来
解决。
小结 (续 )
―信号量( semaphore)”是指用于协调多个相互
关联线程操作的信号,
“同步原语”是指执行时不可被打断的语句或语
句块。
在 Java中通过使用,synchronized‖来实现互斥操
作。为共享资源加互斥锁的方法有两种
①锁定一个对象和一段代码
②锁定一个方法
小结 (续 )
线程间的同步
?线程之间互相合作协同完成同一个任务的关系称为线程
间的同步。
?线程间的同步关系可以用生产者和消费者问题来说明。
?未经同步的生产者和消费者操作,将会产生重复读取和
更新丢失两类错误。
?解决生产者和消费者问题的关键是为共享资源设置一个
信号量,并使用 wait()和 notify()方法来进行线程间通信。
小结 (续 )
?线程的优先级 (Thread Priority)
?线程的优先级表示线程的重要程度,
?在默认条件下,一个线程将继承其父线程的优先级。
?线程优先级的使用原则与操作系统有着密切的联系因此在
JAVA中的线程的调度是完全受其所运行平台的操作系统的
线程调度程序控制的,
?可以使用 getPriority()和 setPriority()方法来获取与设定线
程的优先级。
小结 (续 )
?线程组 (Thread Group)
?线程组提供了一个统一管理多个线程而不需单
独管理的机制
?在 JAVA语言中用 java.lang.ThreadGroup类来支
持线程组的实现。
?当多个线程分为一组时,可以用一个方法启动
或挂起线程组中的所有线程。
小结 (续 )
?守护线程 (Daemon Threads)
?守护线程,也称后台线程,是指在程序运行的时候在后
台提供一种通用服务的线程,
?守护线程不会阻止程序的终止。
?使用 setDaemon方法设定一个线程为守护线程。
?要创建一个守护线程必须在调用它的 start()方法之前设