您好,登录后才能下订单哦!
本文版权归作者Ian Gao所有,如有转载请与作者联系并注明出处http://blog.csdn.net/Iangao/archive/2008/10/09/3041265.aspx。
一、基本同步原理
1.1 同步机制(synchronize mechanism)
1.1.1 同步
多线程开发过程中,我们经常会提到同步这个词,那么什么是同步呢?为什么会存在同步问题呢?我们知道一个多线程应用系统在操作系统的进程(线程)机制下可以同时有多个进程(线程)并发运行,这此进程(线程)要完成的任务可能是互不相关的,但也可能是有联系的。那么当一个进程(线程)要和另一个进程(线程)交流信息时同步就有可能发生了。为什么呢?如果您不清,看了下面这个例子也许就会明白了。
[@more@]同步示例: 进程同步:// http://www.csdn.net/blog/Iangao
在日常生活中我们经常会遇到定蛋糕的事,过程是什么样的呢?让我们一起来回顾一下。
首先是我们做我们的事,蛋糕师傅也做道自已的事,当我们想要买蛋糕时,我们就要和蛋糕师打交道了。
我们选好蛋糕样式,交款完款,把定单交给蛋糕师后由于时间制作时间很长,所以出去逛20分钟的街,然后回来拿着小票等待蛋糕被做好了。
蛋糕师则是一上班就在等待定单,当他拿到定单后,开始按照要求进行制作,做好后把这蛋糕放到货架上,然后根据定单上的连系方式通知我们来取货。
接到通知单后,我们便可以拿着蛋糕回家了。而蛋糕师还将继续做着他自已的事情。 货架,定单,取货通知单
顾客进程 师傅进程
顾客(){
选择蛋糕() // 1.(并行)
交款() // 2.(并行)
出示(定单) // 3. (同步1)
逛街(20分钟) // 4. (并行)
等待(取货) // 5. (同步2)
// http://www.csdn.net/blog/Iangao
取蛋糕(货架) // 7. (并行)
回家() // 8. (并行)
}// http://www.csdn.net/blog/Iangao 制作(){
准备制作() // 1(并行)
while(true){
等待(定单) // 2.(同步1)
制作(30分钟) // 4.(并行)
放入蛋糕(货架) // 5.
通知(取货) // 6. (同步2)
... // 7. (并行)
}
}
我们分析一下上面的例子,在例子中,我们可以看出由于没有定单,师傅进程在“同步1”处不得不停下来等待一个定单,直到有定单时它才可以制作蛋糕。同样的,由于蛋糕尚未做好,因此顾客线程也在“同步2”处停下来等待,直到接到取货通知单后才取蛋糕回家。现在我们可以总结一下:同步(synchronize)实际上就是让一个线程停下脚步处于等待状态直到另一个线程向它发出继续执行的信号后,两个线程再继续并发执行的一种线程间的通信机制。而通过上面的例子我们可以总结出“要想实现这种机制起码应该具备如下三个基本要素”:
一个用于通信的信号(signal)
一个用于等待信号的操作(wait)
一个用于发送信号的操作(notify)
这种机制在Solaris或POSIX线程中,它们通常被看做是条件变量(condition variable), 而在Windows系统中它们往往被看成是事件变量(event variable),不过两者还是有一些区别的,我们将在后面的章节中继续深入讨论。
1.1.2 临界区、锁
在同步问题中,有一类是由于对共享资源并发使用而引起的。众所周知一组资源(数据)被多个进程(线程)同时使用,往往会造成逻辑紊乱(有的在读,有的在改),为了避免这一情况,就要求对资源的使用加以控制,使得进程(线程)在访问资源期间,不允许被其他进程(线程)干扰。有干扰的线程必须处于等待状态中。通常我们把这段不受干扰的使用某一特定资源的段码段称为该资源的临界区(critical section)。而临界区就象对资源加了一把锁(Lock),进的时候加锁(lock),出的时解锁(unlock),这就可以把所有有干扰的进程(线程)锁都到临界区之外。下面我们分析一下用于定义临界区的锁操作都完成了什么功能,请看下面的一个最简单的功能性说明代码清单(下图参考自《操作系统》一书):
shared double balance, account; //共享变量
shared int lock=FALSE; //同步变量(Mutex): 用于实现控制对balance和account资源访问的锁
Program for P1 [ Credit(贷) ]
// http://www.csdn.net/blog/Iangao
...
enter(lock); // 1. 进入临界区(加锁)
balance=balance+account; // 2. <临界区A>
leave(lock); // 3. 离开临界区(解锁)
---------------------------------------------------------->
... // 4. 继续P1任务 Program for P1 [ Debit(借) ]
// http://www.csdn.net/blog/Iangao
...
enter(lock); // 2. 等待P1解锁
// http://www.csdn.net/blog/Iangao
balance=balance-account; // 4. <临界区B> (获取了P1释放的锁)
leave(lock); // 5. 解锁
...
1.1.3 互斥锁
随着后面不断深入的讨论,我们会发现有些临界区的锁是可以有限度的,它只阻止一部分可以引起相互干扰的进程(线程)进入临界区,而不会阻止互不干扰的进程(线程)进入,这时可能会有几个进程(线程)并发的使用。不过本节我们只讨论其中一种最简单的锁,一种会阻隔一切想要进入临界区内线程的锁,由于它具有极强的排它性,因此我们称它为互斥量(Mutex lock)。同样由于它具有排它性,我们一般会用它来定义一个原子(atomic)操作,因为它可以很好的保证对某一资源操作的不可分割性(individed)。下面我们简单定义了一个mutex的基本语义:
// 加锁:原子操作
enter(mutex){
while(mutex) wait(); // 等待
mutex=TRUE; // 标识资源忙
} // 解锁:原子操作
leave(mutex){
mutex=FALSE; // 标识资源可用
notify();
}
考虎到mutex是用于定义原子操作的,因此我们在实现时会对加锁和解锁操作进行一定的约束,约束如下:
最后,为了保证临界的原子性(atomic),通常对一个互斥量Mutex做解锁操作的进程(线程)一定是前面对其进行加锁操作的进程。 而不允许被其他进程(线程)解锁。下面的代码清单演示了临界区与锁的工作原理:如果P1运行到balance=balance+account时P2运行到了enter(lock),那么由于P1对"lock资源"已经加锁了,此时P2只处于等待状态。直到P1执行完exit(lock)后才释放了对"lock资源"的控制权,这时P2使可以执行enter(lock)操作,继而在独占lock资源的情况下完成临界区中的代码示意图。
1.2 Java中的同步机制
1.2.1 synchronized关键字
1 定义临界区:
Java为了实现同步机制提供了synchronized关键字,我们可以使用它来定义被同步的对象以及临界区,临界区的范围是由一组大括号来标识的。而进出临界区时必要的加锁和解锁操作则是由Java内置支持的。从功能上来说,我们可以认为左大括号起到enter(lock)的作用,而右大括号起到了exit(lock)的作用。下面清单中演示了synchronized与互斥锁理论功能上的的对应关系。
private double balance, account; //共享变量
private Object lock=new Object(); //同步对象
Program for P1
...
synchronized(lock){ // 获取资源并加锁
balance=balance+account; // <临界区>
} // 释放资源并解锁
... shared double balance, account; //共享变量
shared int lock=FALSE; //同步变量
Program for P1
...
enter(lock); // 获取资源并加锁
balance=balance+account; // <临界区>
exit(lock); // 释放资源并解锁
...
2 定义同步对象:
我们知道了如何定义临界区,但这还不够,我们还需要知道如何定义这段临界区要同步的对象(数据).在Java中有下面的两种使用synchronized的方式,我们分别看一下它们是如何定义被同步的对象的:
一种是直接“声明同步(synchronized)对象”: 这种方式的同步对象被明确的指定在了sychronized后的小括号,而其后的一组大括号内定义的正是临界区。这种方式使用起来很灵活,可以只对方法中的一段不可分割的代码做同步,因此临界区比较小,所以效率也就可以比较高了。下面清单中演示了这一用法的使用。
/**
* 声明同步对象示例,参看上节示例
* @author iangao
*/// http://www.csdn.net/blog/Iangao
public class Foo {
private double balance, amount; //共享资源
private Object synObject=new Object(); //同步对象
/**
* 参看上节: [Program for P1]
*/
public void f(){
...
// 进入 临界区,锁定对synObj的访问
synchronized(synObject){ // 临界区A
balance=balance+amount;
...// http://www.csdn.net/blog/Iangao
doSomethingInCriticalSectionA();
}
// 退出 临界区,释放对synObj的锁定// http://www.csdn.net/blog/Iangao
...
}// http://www.csdn.net/blog/Iangao
/**
* 参看上节: [Program for P2]
*/
public void g(){// http://www.csdn.net/blog/Iangao
...
synchronized(synObject){ // 临界区B
balance=balance-amount;
...
doSomethingInCriticalSectionB();
}
....// http://www.csdn.net/blog/Iangao
}
}
另一种是“声明同步(synchronized)方法”:这种方式是把整个方法的实现都划入了临界区.但它同步的是哪个对象呢?实际上这种方式是隐式的对this对象做同步的,相当于一进入方法就对this对象做同步操作[即synchronized(this){}],直到方法结束 再解除同步。不过因为是对整个方法 做同步,所以有时会显得效率不高,只是它用起来很方便。以下清单中描述了synchronized的这种用法,右边演示了重构成第一种使用方式时的样子。
/**
* 声明同步方法示例 (管程的Java实现)
* @author iangao
*/
public class Foo2 {// http://www.csdn.net/blog/Iangao
private double balance, amount; //共享资源
public synchronized void f(){ // 监界区C
balance=balance+amount;
}// http://www.csdn.net/blog/Iangao
public synchronized void g(){ // 监界区D
balance=balance-amount;
}
}// http://www.csdn.net/blog/Iangao /**
* 按照同步对象的方法重构Foo2后
* @author iangao
*/
public class Foo2 {
private double balance, amount; //共享资源
public void f(){
synchronized(this){ // 监界区C
balance=balance+amount;
}
}
public void g(){
synchronized(this){ // 监界区D
balance=balance-amount;
}
}
}
其实,通过sychronized关键字定义的同步对象,我们一般称它为管程对象(Monitor),在后面我们还会对管程进行详细讨论。在此不再多述。
3 同步测试
测试1.1.3.2中synchronized同步示例,为了达到测试的目的可以分别在临界区A,B,C,D中加入延时操作Thread.sleep(3000)并输出一些调试信息。这样就可以得到可见的同步效果显示了。测试代码可以参看下面的清单:
/**
* 同步对象测试
* @author iangao
*/
public class SynchronizedObjectTest {
public static void main(String[] args){
// 定义要同步的两段代码
new ThreadsTest(){
private Foo foo=new Foo();
void runInThread1() { // <反射>
foo.f(); // 在线程1中执行foo.f()
}// http://www.csdn.net/blog/Iangao
void runInThread2() { // <反射>
foo.g(); // 在线程2中执行foo.g()
}// http://www.csdn.net/blog/Iangao
}.execute(2); // 执行同步测试(启动2个线程)
}// http://www.csdn.net/blog/Iangao
} 测试结果:
[T1]: 准备进入: synObject临界区A
[T1]: 进入: synObject临界区A
[T1]: <执行: balance=balance+amount>
[T1]:
[T2]: 准备进入: synObject临界区B //等待T1解锁
[T1]: 离开: synObject临界区A
[T2]: 进入: synObject临界区B //T1已解锁
[T2]: <执行: balance=balance-amount>
[T2]:
[T1]: <已离开临界区,与T2并行>
[T1]: <结束>
[T2]: 离开: synObject临界区B
[T2]: <结束>
4. 对象锁与类锁
在Java中,用synchronized定义的都有两种锁,一个种是对象锁(每个对象有一把锁), 一种是类锁(每个class有一把锁),前面讨论的主要是对象锁,下我们用一个例子来研究对象锁与类锁的区别:
/**
* 类锁测试
* @author iangao
*/
public class ClassLockTest extends ThreadsTest{
public void runInThread1(){ g(); }
public void runInThread2(){ f(); }
// http://www.csdn.net/blog/Iangao
/**
* 同步的是类锁(class lock)
*/
public static synchronized void f(){
output("enter f");
sleep(1000);
output("leave f");
}// http://www.csdn.net/blog/Iangao
/**
* 同步的是对象锁(object lock),
* f,g必须同时为static或非static同步才会成立
*/
public synchronized void g(){
output("enter g");
sleep(1000);// http://www.csdn.net/blog/Iangao
output("leave g");
}// http://www.csdn.net/blog/Iangao
public static void main(String[] args){
new ClassLockTest().execute(2);
}// http://www.csdn.net/blog/Iangao
}
执行结果:
[T1]: enter g
[T2]: enter f
[T1]: leave g
[T2]: leave f
结论:
static方法定义的是类锁, 而非static方法定义的是对象锁. 因为两个方法同步的不是同一个锁,所以同步失效.
执行结果2(把g改成static后的):
[T1]: enter g
[T1]: leave g
[T2]: enter f
[T2]: leave f
结论:
此时两个方法都是同步的class锁, 同步成功!
1.2.2 wait-notify机制
1.2.2.1 wait、notify简介
Java语言为我们提供了一套用于线程间同步的通信机制即wait-nofity机制。前面我们知道了,sychronized关键字可以让我们把任何一个Object对象做为同步对象来看待,而Java为每个Object都实现了wait()和notify()方法。它们必须用在被sychronized同步的Object的临界区内。通过的wait()我们可以使得处于临界区内的线程进入阻塞状态,同时释放被同步对象的控制权,而notify操作可以唤醒一个因调用了wait操作而处于阻塞状态中的线程,使其进入就绪状态。被重新换醒的线程会试图重新获得临界区的控制权,并继续执行临界区内wait之后面的代码。如果发出notify操作时没有处于阻塞状态中的线程,那么该信号会被忽略.
/**
* Wait/Notify 机制测试
* @author iangao
*/// http://www.csdn.net/blog/Iangao
public class WaitNotifyTest {
public static void main(String[] args){
new ThreadsTest(){
Object obj=new Object();
void runInThread1()
throws InterruptedException{
waiter();
}// http://www.csdn.net/blog/Iangao
void runInThread2(){
sleep(100);
notifyer();
}// http://www.csdn.net/blog/Iangao
void runInThread3(){
sleep(1000);
notifyer();
}// http://www.csdn.net/blog/Iangao
void runInThread4()
throws InterruptedException{
sleep(6000);
waiter();
}// http://www.csdn.net/blog/Iangao
/**
* WAITER
*/
void waiter()
throws InterruptedException {
output("
synchronized(obj){
output("enter");
int i=0;
for(;i<3;i++)
output(""+i,500);
output("wait...");
obj.wait();
output("back");
for(;i<5;i++)
output(""+i,100);
output("leave");
}
}// http://www.csdn.net/blog/Iangao
/**
* NOTIFYER
*/
void notifyer() {
output("
synchronized(obj){
int i=0;
output("enter");
for(;i<2;i++)
output(""+i,1000);
output("notify");
obj.notify();
for(; i<4; i++)
output(""+i,100);
output("leave");
}
}// http://www.csdn.net/blog/Iangao
}.execute(4);
}// http://www.csdn.net/blog/Iangao
}// http://www.csdn.net/blog/Iangao
execute(2)执行结果:
[T1]:
[T1]: enter
[T1]: 0... (0.5秒)
[T2]:
[T1]: 1... (0.5秒)
[T1]: 2... (0.5秒)
[T1]: wait...
[T2]: enter [2]
[T2]: 0... (1.0秒)
[T2]: 1... (1.0秒)
[T2]: notify [3]
[T2]: 2... (0.1秒)
[T2]: 3... (0.1秒)
[T2]: leave
[T1]: back [4]
[T1]: 3... (0.1秒)
[T1]: 4... (0.1秒)
[T1]: leave
execute(4)执行结果:
[T1]:
[T1]: enter
[T1]: 0... (0.5秒)
[T2]:
[T1]: 1... (0.5秒)
[T1]: 2... (0.5秒)
[T3]:
[T1]: wait... [1]
[T2]: enter
[T2]: 0... (1.0秒)
[T2]: 1... (1.0秒)
[T2]: notify [2]
[T2]: 2... (0.1秒)
[T2]: 3... (0.1秒)
[T2]: leave
[T3]: enter [3]
[T3]: 0... (1.0秒)
[T3]: 1... (1.0秒)
[T3]: notify [4]
[T3]: 2... (0.1秒)
[T3]: 3... (0.1秒)
[T3]: leave
[T1]: back [5]
[T1]: 3... (0.1秒)
[T4]:
[T1]: 4... (0.1秒)
[T1]: leave
[T4]: enter
[T4]: 0... (0.5秒)
[T4]: 1... (0.5秒)
[T4]: 2... (0.5秒)
[T4]: wait... [6]
分析:
T2要进入临界区时会由于T1掌控临界区而等待。
wait()操作使得T1进入了阻塞状态并释放了通过syncrhonized同步的对象obj的控制权,这使得T2可以进入临界区
T2的notify()可以唤醒T1,但由于并不立即释放对obj的控制权, 因此这时的T1处于就绪状态
T1直到T2退出临界区后才重获控制权,并继续后面的业务
分析:
T1 wait时虽然有T2,T3在等待,但也只能有一个线程进入临界区.
由于在T2 notify之前T3已处于就绪状态中,加上notify后的T1就有两个处于就绪中的线程等待进入临界区了
在T2离开临界区时,有可能被T3先抢到临界区,这时虽然T1已被T2唤醒,但仍要继续等待。
T3发送notify信号时,系统并没有阻塞的线程,因此这个信号实际上根本没起作用。
T1终于在T3结束后重获了临界区的控制权,不过时这离T2的snotify操作已经很久了。
T4的wait操作由于没有notify来激活,因此它会一直等下去,至于先前T3的notify是不会对后面的wait起作用的。
通过上面一系列的测试与分析,我们可以得出一个得要结论,那就是“wait()对notify()的响应很有可能是不及时的”,之所以强调这一点是因为它往往会由于响应不及时而造成响应时的系统状态与发出notify时的不一致,所以在实际应用中我们要多加注意这一点,尤其是在那些与状态有关的同步应用中这点尤为重要。
1.2.2.2 notify 与 notifyAll
notify的只能唤醒一个通过调用wait而等待的线程, notifyAll可以唤醒所有调用wait或因synchronized关键字而等待的线程. 不过通过notifyAll唤醒的所有线程也必须按照上节分析的过程,依次进入临界区.
1.2.3 wait与sleep的比较
// http://www.csdn.net/blog/Iangao wait() sleep()
所属对象 Object Thread
阻塞当前线程 可以 可以
使用条件 必须在管程(Monitor)内使用,即所属Object必须已被sychronized同步。 随时
管程(Monitor)内使用效果
(synchronized临界区内) 释放对管程对象(Monitor)的控制权 不释放对管程对象(Monitor)控制权
唤醒 notify() 无
发表于 @ 2008年10月09日 11:52:00|评论(0)|收藏
新一篇: Java同步技术(二) |
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/Iangao/archive/2008/10/09/3041265.aspx
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。