多线程编程的设计模式 临界区模式(三)

来源:互联网 发布:金华美工培训 编辑:程序博客网 时间:2024/04/28 14:14

[高级主题:关于synchronized]

其实在多线程编程基础部分,我已经谈过synchronized相关的内容.但临界区模式是其它多线程编程模式的基
础,所以在这里继续深入一下谈谈synchronized相关的一些内容.

只要见到synchronized关键字,第一要想到的问题就是,synchronized在保护谁?

在上面的例子中,synchronized保护的是Corrie对象的counter,name,number三个字段不被"交叉赋值",

也就是这三个字段同一时间只能被一个线程访问.


其次我们要考虑的问题是:这些对象都被妥善地保护了吗?

这是非常重要的问题.无论你花巨资打造一把高安全性锁,把自己的家门牢牢地锁住,可是你却把门旁边的窗子
敞开着,那么你花巨资打造的锁又要什么意义呢?所以要确保从任何一个通道访问被保护的对象都被加锁控制
的,比如字段是否都private或protected的,对于protected的子类中的扩展方法是否能保护被保护对象.

对于上面的例子因为display有可能被外面的方法单独调用,所以它也必须是同步的.而test方法只会在into中
调用,简单说它只是所有通道被加了锁的大房子中的一个小单元,所以不必担心有人会从外部访问它.

要注意保护的范围是三个同时需要保护的字段,如果它们被分别放在synchronized方法中保护,并不能保证它们
本个字段同时只有一个线程访问.

 

那么我们就有一个问题,获取谁的锁呢?

 

要保护一个对象,当然直接获取这个对象的锁,我们上面的例子可以理解为要同时保护三个对象,那么其实就是
要保护这三个对象的所在的容器.也就是它们所在的实例.如果不相关的三个对象要同时保护,一定要放在同时容纳
它们的容器中,否则无法同时保护它们的状态.对于上面的例子我们同样可以理解为要保护的是Corrie的实例,
因为这个实例是这三个字段的容器.所以我们用synchronized方法就是等同于synchronized(this){.......}
如果这个游戏中有多个山洞,而只有一块显示牌,那以我们就需要保护多个实例的三个字段同时只被一个线程
访问,我们就需要synchronized(Corrie.class)来保证多个实例被多个线程访问时只有一个对程能同时对三个
字段访问.


所以获取谁的锁定也是一个很重要的问题,如果你选择了错误的对象,就象你花巨资打了一把锁却锁了别人的
门.

 

 

synchronized就是原子操作,简单说在一个线程进行同步块中的代码时其它线程不能进入,这是很明显的.但同时,

多个同步方法或多个获取同一对象的同步块在同一时候也只能一个线程能访问其中之一,因为控制谁能访问的是要
获得那个同步对象的锁.如:
class C{
 synchronized  void a(){}
 synchronized  void b(){}
}

当一个线程进入同步方法a后那么其它线程当然不能进入a,同时也不能进入b,因为能进入的条件是获取this对
象的锁.一个结程进入a后this对象的锁被这个线程获取,其它线程进入b也同样要获取这个锁,而不仅仅是进入
a要获取这个锁.这一点一定要理解.

如果我们想所有线程只能有一个线程在同一时间访问a(),而这时也可以只有另一个线程可以访问b();就是a()和

b()身本都是同步的,但a()和b()之间不产生互斥相,那么就要为a()和b()分别创建用户于同步的对象,这样的

对象我们习惯叫虚拟锁。

class C{

 Object aL = new Object();
 Object bL = new Object();

 void a(){

     synchronized (aL){......} }
 void b(){

     synchronized (bL){......}

}
}

 

理解上面的知识我们再回过头来看原子操作.

JLS规定对于基本类型(除long和double)以外的赋值和引用都是原子操作,并且对于引用类型的赋值和引用也是
原子操作.

注意这里有两个方面的知识点:

1.对于long和double的操作非原子性的.需要说明这只是JLS的规定,但大多数JVM的实现其实已经保证了long和
double的赋值和引用也是原子性的,只是允许某种实现可以不是原子性的操作.

对于其它基本类型如int,如果一个线程执行x = 1;另一个线程执行x = 2;
由于可见性的问题(多线程编程系统中已经介绍),x要么就是1,要么就是2,看谁先同步到主存储区.

但对于long,l = 1;l = 2;分别由两个线程执行的结果有可能不是你想象的,它们有可能是0,或1,或2,或一个其
它的随机数,简单说两上线程中l的值的部分bit位可能被另一个线程改写.所以最可靠的是放在synchronized中
或用volatile 保护.当然这里说的是有“非常可靠”的需要。

2.我们看到,对于引用对象的赋值和引用也是原子的.

我们还是看javaworld上dcl的例子.

 那个错误的例子误了好多人,(JAVA与模式的作者就是受害人),我们先不说JAVA内存模型的原因(前面我已经从
JAVA内存模型上说明了那个例子是错误的,我是说对那个例子的分析是错误的).单从对于"引用对象的赋值和引
用也是原子的"这句话,就知道对于引用字段的赋值,绝对不可能出现先分配空间,然后再还没有被始化或还没有
调构造方法之前又被别的线程引用.因为当一个线程在执行赋值的时候是原子性的操作,其它线程的引用操作也是

原子性的操作 的,在赋值操作没有完成之前其它线程根本不可能见到"分配了空间却没有初始化或没有调用构造方法"

的这个对象.

不知道什么原因,这样的一个例子从它诞生开始竟然是所有人都相信了,也许有人责疑过但我不知道.如果你有足
够的基础知识,就不必跟着别人的感觉走!

因为这是一个最最基础的模式,暂时不介绍它与其它模式的关系.在以后介绍其它模式时反过来再和它进行比较.

而一些复杂的模式都是在这个简单的模式的基础上延伸的.