热爱测试,主要研究性能测试和自动化测试方面的技术,希望与同样对测试有热情的你一同进步成长

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

上一篇 / 下一篇  2007-08-08 11:57:18 / 个人分类:软件开发

51Testing软件测试网q Ivu)d Oa

(转自)http://dev.csdn.net/author/axman/4794100c5de740a09b52415cb9dd4239.html仔细学习了一下,感觉讲的很生动,记录下来:51Testing软件测试网K E3si]4O+vZ

51Testing软件测试网} ^ ~B!g K7K

临界区模式 Critical Section Pattern 是指在一个共享范围中只让一个线程执行的模式.51Testing软件测试网O Ff ^Fb3jQ)I
它是所有其它多线程设计模式的基础,所以我首先来介绍它.
{C$iJ2Kl7c-a0把着眼点放在范围上,这个模式叫临界区模式,如果把作眼点放在执行的线程上,这个模式就叫51Testing软件测试网+w.e.Lc7C;?e'x|t
单线程执行模式.

'eg#Wb*_,b\0

M/~ i%I:t0首先我们来玩一个钻山洞的游戏,我 Axman,朋友 Sager,同事 Pentium4.三个人在八角游乐场51Testing软件测试网TC%|2l!F
循环钻山洞(KAO,减肥训练啊),每个人手里有一个牌子,每钻一次洞口的老头会把当前的次序,51Testing软件测试网D_pAJPz&J3Y
姓名,牌号显示出来,并检查名字与牌号是否一致.

3P;[ cjaeW:Q0

P7Naq6uS$Z0OK,这个游戏的参与者有游乐场老头Geezer,Player,就是我们,还有山洞 corrie.51Testing软件测试网v%w7C+gM+Tjv9x

F&r[#yEqj-g~$q1a0public class Geezer {
K(jSE5b;dup]].{0    public static void main(String[] args){
5h6@6QA"tn?(A?T0       
Q.Px r$Q-n0        System.out.println("预备,开始!");51Testing软件测试网QV F H R7P)]
        Corrie c = new Corrie();//只有一个山洞,所以生存一个实例后传给多个Player.51Testing软件测试网,Z.K9O yWm%j
        new Player("Axman","001",c).start();51Testing软件测试网Q S6u AD0q Q
        new Player("Sager","002",c).start();51Testing软件测试网h"j5m9fW$\ `
        new Player("Pentium4","003",c).start();51Testing软件测试网0Zf K\t1@ D
    }51Testing软件测试网@ h+m5S1q | i az6h
}51Testing软件测试网4D)G1g ^A?+Yx

m&@ \/Wn;U0这个类暂时没有什么多说的,它是一个Main的角色.

[` ~4^$l051Testing软件测试网8Xn;l"s#S DZ1r"Em

public class Player extends Thread{51Testing软件测试网-Z7@:t/m-q4I
    private final String name;51Testing软件测试网1UdL3g8|m
    private final String number;
H%J"GmnKI J[0    private final Corrie corrie;
T*X\2?]D]0    public Player(String name,String number,Corrie corrie) {
y,~PB;m`:q0        this.name = name;51Testing软件测试网 E y8c w6I0VT"h
        this.number = number;51Testing软件测试网Z/v"G iK%PNf
        this.corrie = corrie;51Testing软件测试网7T|@-b(l
    }51Testing软件测试网o'wg7\:S?St
   
s-e J BUFY L)E0    public void run(){51Testing软件测试网4],D-u3O g8T B
        while(true){
J6SR2AZn @0            this.corrie.into(this.name,this.number);51Testing软件测试网cY}3k.DI2eT
        }51Testing软件测试网 T4X6a;B }zS
    }
u1]4AL1Xs0}51Testing软件测试网Y,L&z3\U|8T
在这里,我们把成员字段都设成final的,为了说明一个Player一旦构造,他的名字和牌号就不能改
$lV&l2[j\*P0变,简单说在游戏中,我,Sager,Pentium4三个人不会自己偷偷把自己的牌号换了,也不会偷偷地去
%@3O$V0R|4_!jO2v0钻别的山洞,如果这个游戏一旦发生错误,那么错误不在我们玩家.51Testing软件测试网|R\"er&s1A

$W0M0cj,t%v0import java.util.*;
'Dko/A;GT0public class Corrie {51Testing软件测试网 j6LPNf
    private int count = 0;51Testing软件测试网r)K:AHF!r
    private String name;51Testing软件测试网Q)P;\w|V}*KjS7\
    private String number;51Testing软件测试网9};B2_Mx3H:] x4{4z
    private HashMap lib = new HashMap();//保存姓名与牌号的库
1mZ4O"Xq`&a4q0   
2K d` w+s'|$cRb0    public Corrie(){
S:KC%s`i0       
6K }Z8@M&I4W]0        lib.put("Axman","001");51Testing软件测试网H}7@[!mK
        lib.put("Sager","002");
n'Z-se;]0        lib.put("Pentium4","003");51Testing软件测试网$UkUa(LBY4N$e qj
 
$_(P#HH,Z+V0    }
4gG(}f~nE Ps9i J0   
0F7ru qY CA0    public void into(String name,String number){
n'A$~ QH'} lz0        this.count ++;
,u1cA2N%h\w X0        this.name = name;51Testing软件测试网VI-u+p|6[!U1~R
        this.number = number;
y$scK(z.f(r0        if(this.lib.get(name).equals(number))
YAP3e'jYxE0 test():
3@"D:{ hut0    }51Testing软件测试网Y^5M?#te
   
.C6n+i6Z_fHO+J*G0    public String display(){
,Y~?3Y~7Dl3R0        return this.count+": " + this.name + "(" + this.number + ")";51Testing软件测试网eQ }K Ll
    }51Testing软件测试网*X0a s;gA] e

s9z |'E[0    private void test(){51Testing软件测试网 {Dy&W0z D
        if(this.lib.get(name).equals(number))
0S)},Fn.jm8x7Z0            ;
!u B{_0zJ0            //System.out.println("OK:" + display());51Testing软件测试网*j(p!p7Z B2H:y,_
        else51Testing软件测试网)^y2S _ EN
            System.out.println("ERR:" + display());51Testing软件测试网CzbVI6k
    }51Testing软件测试网/siji,xUr"C
}
na%j/~7v["r1P L0这个类中增加了一个lib的HashMap,相当于一个玩家姓名与牌号的库,因为明知道Corrie只有一个实例,51Testing软件测试网9ai-y/i5u`X
所以我用了成员对象而不是静态实例,只是为了能在构造方法中初始化库中的内容,从真正意义中说应51Testing软件测试网'AZT}+mU.t%y
该在一个辅助类中实现这样的数据结构封装的功能.如果不提供这个lib,那么在check的时候就要用51Testing软件测试网pA1a/t;N2O S-U~
if(name.equasl("Axman")){51Testing软件测试网2zVr-` h(Q8V z;r6|
 if(!number.equals("001")) //出错
A ?${es ?6rL.G0}
/|)Y]&{ Y}4a#m;O(W,L0else if .......
[~1lUa k%x%s4O0这样复杂的语句,如果player大多可能会写到手抽筋,所以用一个lib来chcek就非常容象.

3kRl6j bf~051Testing软件测试网 P*y[{_4f7i


7EY!Q1z+D~~q0运行这个程序需要有一些耐心,因为即使你的程序写得再差在很多单线程测试环境下也能可是正确的.
)YS&g Sf,O0而且多线程程序在不同的机器上表现不同,要发现这个例子的错识,可能要运行很长一段时间,如果你的
s6Dg*y)Dz5_0机器是多CPU的,那么出现错误的机会就大好多.

n M;ZM/@@051Testing软件测试网o4\^K#d r7R&s`0K3r

在我的笔记本上最终出现错误是在11分钟以后,出现的错误有几钟情况:51Testing软件测试网y0cv'i?Aw0F
1: ERR:Axman(003)
F| @ B&FJ02: ERR:Sager(002)
3s\8d*dB }+B0第一种情况是检查到了错误,我的牌号明明是001,却打印出来003,而第二种明明没有错误,却打印了错误.51Testing软件测试网!g,X&UPn

51Testing软件测试网.?[*q.pf gUiE

事实上根据以前介绍的多线程知识,不难理解这个例子的错误出现,因为into不是线程安全的,所以在其中51Testing软件测试网5JqeX9w[
一个线程执行this.name = "Axman";后,本来应该执行this.numner="001",却被切换到另一个线程中执行
$J;j.yx9`EE0_ t5XM0this.number="003",然后又经过不可预知的切换执行其中一个的if(this.lib.get(name).equals(number))51Testing软件测试网2|WIJCTv:qq O!^
而出现1的错误,而在打印这个错误时因为display也不是线程安全的,正要打印一个错误的结果时,由于51Testing软件测试网'l,r9jlm8u Q.fQ
this.name或this.number其中一个字段被修改却成了正确的匹配而出现错误2.

Ykm}7r4?\G0

T u l%E8M0另外还有可能会出现序号颠倒或不对应,但这个错误我们无法直观地观察,因为你根本不知道哪个序号"应该"
.d;Jn#H-~,\#tc)z0给哪个Player,而序号颠倒则有可能被滚动的屏幕所掩盖.51Testing软件测试网,VnMT/d(y2M/h

51Testing软件测试网W2J&ccuUw6s!QM)S

51Testing软件测试网z]9s},?(xGa
[正确的Critical Section模式的例子]
5b E2z}S0我们知道出现这些错误是因为Corrie类的方法不是线程安全的,那么只要修改Corrie类为线程安全的类就行51Testing软件测试网$\&Xq,I `|+K5a
了.其它类则不需要修改,上面说过,如果出现错误那一定不是我们玩家的事:

;@t:P/}|j/a051Testing软件测试网qB~ MP5y#]}O

import java.util.*;51Testing软件测试网g"O7]1{1A Cl
public class Corrie {
Z"F mC zB jS e0    private int count = 0;51Testing软件测试网/U0B:K*|P5~Lr4J'N
    private String name;51Testing软件测试网n)@,YkL:Zk s*o
    private String number;
2dVl|:O0    private HashMap lib = new HashMap();//保存姓名与牌号的库51Testing软件测试网Ll+[p'Im*n
   
+`/R+NQ?[q0    public Corrie(){51Testing软件测试网(C2R#H!]ZK F)|W
       51Testing软件测试网7V P9_:iW
        lib.put("Axman","001");51Testing软件测试网tem8yw y(?6~ }
        lib.put("Sager","002");
B,a1Fl(X5d j$W"m0        lib.put("Pentium4","003");
[(]7]x1W8q0j;w0 51Testing软件测试网'v&^B{dD's[i@7Y
    }
XOR(y8Cy+k#[K0   51Testing软件测试网d ^s%t @ A1\
    public synchronized void into(String name,String number){51Testing软件测试网5dHb2WGa
        this.count ++;51Testing软件测试网l N-x*K4@ KRX
        this.name = name;51Testing软件测试网:z2krKY3q e*xf
        this.number = number;51Testing软件测试网#Wde w `
 test();51Testing软件测试网YK&a,IJ!K.E6[/oz
    }
`0?/n3D7yN.v0   51Testing软件测试网)lS4AVB;r/y
    public synchronized String display(){51Testing软件测试网M5pK:[ih{
        return this.count+": " + this.name + "(" + this.number + ")";51Testing软件测试网*U+du&b"}"^H&E
    }51Testing软件测试网X#R```~P

aGJ;s+OqRo(]6MpY*z0    private void test(){
5Q y2sy a]kif0        if(this.lib.get(name).equals(number))51Testing软件测试网h(Yv9QCFP
            ;51Testing软件测试网Nz-ddA |Q
            //System.out.println("OK:" + display());
!Z4iTIUo0        else
km+f}#N-{b/e8f/~0            System.out.println("ERR:" + display());51Testing软件测试网/W8s wVO&em
    }51Testing软件测试网}n}R-I#p"v
}

s4rJ&rn?e r_051Testing软件测试网0` q C)vl:FVA bp,_

运行这个例子,如果你的耐心,开着你的机器运行三天吧.虽然测试100天并不能说明第101天没有出错,51Testing软件测试网g,I$[BN$^7G.x
at least,现在的正确性比原来那个没有synchronized 保护的例子要可靠多了!51Testing软件测试网2wp6eU3g-TY}+N

'w Q-@ i$MSHi$L u p0到这里我们对Critical Section模式的例程有了直观的了解,在详细解说这个模式之前,请想一下,test51Testing软件测试网F U]-Z:rX
方法安全吗?为什么?51Testing软件测试网 BCG'i7I f\J,_|

51Testing软件测试网G"OdP @)f4g

所谓模式就是脱离特定的例子使用更一般化的,通用化的表达方式来察看,描述,总结相同的问题.现在
f?"Mw2|&]0我们来研究这个模式:51Testing软件测试网5]"Y7fm K;cxeJ v
51Testing软件测试网7H0H,Q.T6O
共享资源(sharedResource)参与者:
|ze#P N H Z0在临界区模式中,一定有一个或一个以上的共享资源角色的参与.在上面这个例子中就是山洞(Corrie).51Testing软件测试网%@ kg[z4M a3m

_p:@}q`\n:K0共享资源参与者会被多个线程访问,这个角色的访问方法有两种类型,一种是多个线程访问也不会发生问51Testing软件测试网.[V1c$]7tRGD
题的方法,称为线程安全的方法,另一种就是在多个线程同时访问时会发生问题需要保护的方法,称为不安51Testing软件测试网(m$n:U!Koa
全的方法.
MG:?H3S)Q051Testing软件测试网Rj+Us0V:n
51Testing软件测试网X4}{3|8x ^ }^
这里所说的线程安全和不安全的方法,不用多说大家都知道是指公开的方法.对上节最后我留下的问题而
C#s1M*r H1u0言,test方法是安全的,因为它是private的,只会被into方法调用,而into方法是同步的,简单说test中的51Testing软件测试网D3kgM.x0u$@
代码一定会在同步块中执行,而display方法是public的,有可能被任何线程调用,所以它需要同步.51Testing软件测试网 N0a0V$V s
51Testing软件测试网}p2IB7j
对于线程安全的方法,不需要多说.而对于不安全的方法,只要定义为synchronized的就可以达到保护的51Testing软件测试网 K U*XK4^~
目的.也就是多个线程同时执行该段代码时,只有一个线程有机会执行,具体机制我们在多线程中同步对象51Testing软件测试网3MPuh3{
锁中已经说明过.我们把这种只有一个线程能进入的程序范围,称为[临界区]51Testing软件测试网Q8dF&z+S/U2H3}

R/ry"f0JeU:f5y0
#qbA.d4q9P0尽管JDK5以后提供了很多功能更强,语义更准确的并发控制的接口供程序员调用,但我还是极力推荐在大
y LrI2]B0多数情况下(除非需要有效的控制)还是使用synchronized来保护临界区,因为synchronized块的开始和结
Wh_o%[&Ff*zp0束是自动控制的,在离开同步块时会自动释放同步对象锁.而使用java的lock对象时,你不得不每时每刻小
e6[?}]8hIw ~0心地在finally从句中调用lock对象的unlock方法,这比在finally从句中释放数据库连结更重要!
v(H`3L%mC,Oa051Testing软件测试网%m+|#Cy!x N LP2S
[适用环境]
:z;d(}[.Se051Testing软件测试网M@ QS0eP;x)U
1.单线程环境:单线程环境中肯定只有一个线程执行,无论是否在临界区中反正只有一个线程执行,所以没51Testing软件测试网(D~dD/SG
有必要用synchronized保护,当然如果你非想用synchronized保护没有问题,只是会引起性能的降低,但不
(WBxs#b3z%ox6u0会降低太大.这就象一个人在家里已经关上了大门,还关着卧室的小门,除了会给你带来一些不便之处,没有51Testing软件测试网v S }D0S~;_!EB
什么太大的损失.
^Mr@P/P/I d pK0
&^6y^ Y^(a4I02.多线程环境:如果这些多线程环境中各自完全独立地运行,当然没有问题.但如果多个线程可能访问同一51Testing软件测试网y/@C|%X;z%F8W
SharedResource对象时,就需要使用临界区模式来保护.有时管理线程的环境会提供一种SafeThread环境来
i5~;VS%M'{z%e6Q;R~0确保线程的独立,这种情况就不需要使用临界区模式.51Testing软件测试网5n'DrZ n)\+qu

/gp"?2g4\ ]6z*Wo03.SharedResource的状态会发生改变的情况才需要使用这个模式,如果SharedResource对象一经生成就不
$SC[7E"q;H P3FKO0会改变,当然不需要保护.(只读模式)51Testing软件测试网/x_3pAE1{TT
51Testing软件测试网|0S v(Q\ N.]2lv5q
4.在必要的确保安全性的时候使用这个模式.比如java数据结构类大多数都不是线程安全的.因为很多情况51Testing软件测试网3T@"{ f:`#Ff'qn
下发生多个线程共享冲突对程序本身并无大碍,比如用一个ArrayList或HashMap存放在线人数,对于在线
0D Zx mO0v0人数这种数据本来就不可能精确地计算,只是相对时间内的一个概数,所以多个线程访问对产生冲突对其几51Testing软件测试网6VZ,F,u3Nt0}7D$`
乎没有影响.51Testing软件测试网&]a-ja{L@
但是对于需要确保线程安全的时候,java仍然提供了大量的线程安全的数据结构的封装,由Collections类51Testing软件测试网(|O9Glo _{w
提供的synchronizedXXX()方法可以将传入的数据结构封装为线程安全的.51Testing软件测试网P?K4C ~ i

J,RB;e8e2U*} p2t0
$?7l.m7I$zN&w6e0[性能因素]51Testing软件测试网x@Z%Kv U.M8Q}8|V
在程序设计中,大多数情况下,各种优点无法共存,事实上如果使用一个模式能给其它方面的优点也带来提
E.kDV/M(y8}0升那简单就没有理由不使用该模式了.对于安全性的提升往往要以牺牲性能为代价,所以临界区模式会带来51Testing软件测试网X,D Lr/@`^
一些性能方面的损失.如何权衡这它们之间的比例,要看程序运行的环境,目的等各方面的因素.51Testing软件测试网9E2S&eB4WmI$J

1n:s*DB2y(G01.获取对象锁的操作本身是要花时间的.一个线程在获取同步对象锁时,其实就是一个全局对象的自旋锁,这51Testing软件测试网"BUss3N
个全局对象是要注册到线程管理系统中的.这个过程本身需要一定的时间.但这个过程性能影响并不大.
+I-SL ]{051Testing软件测试网5J m`8A ef Bd`
2.同步对象锁被其它线程占用时需要等待.当一个线程进入同步块时,获取该同步对象的锁,如果该锁被其它51Testing软件测试网Gw`Ss(Oq0P j
线程拥有测当前线程必须等待,从而降低性能,这方面性能的降低较大.
W1Re#S0nC X,n0

Gl"[VC NJ5j5|051Testing软件测试网9\zhI1b$^t!m

提高性能的方法一是尽量减少共享资源的数量.二是尽量减小临界区的范围.双检锁模式就是减小临界区范

HI.K sicF7o(?0

v ~0D:Z8rqv1^K0围的一种手段.51Testing软件测试网(WA@x!D]J

51Testing软件测试网 p%c `kv.B


3k&X [/pK.u4V3`(\r0[死锁问题]51Testing软件测试网G1bKT?"M
临界区模式中非常重要的一点是多线程程序的生命指数.再安全的程序如果运行一定时间就结束自己的生命51Testing软件测试网 F+P6x0W? M*mV
而不能继续运行,那就根本不能达到设计的目的.除去系统突发因素,影响生命指数的最大原因就是死锁.51Testing软件测试网.P Lu"IGKR
对于大家都熟悉的五个哲学家(好象是故意调侃哲学家)吃面条的例子,我们用最简单的模型简单为两个哲学51Testing软件测试网@mR.exF3L
家.然后从中抽象出死锁的最一般的条件:
-i4fYm~6[#c0
3q){y;|0i01.有多个共享资源被多线程共享.对于两个吃面的哲学家而言就是刀和叉两上以上的共享资源.51Testing软件测试网2JI$N ]&Y6L-IF]

7OgZK(y4T l*M02.对一个共享资源的占用还没有释放锁又获取另一个共享资源.占用了刀的时候又要获取叉.51Testing软件测试网Vy p$b)e5mR

L:L@"fP!A9a`D03.对共享资源的占用顺序是不固定的.如果哲学家按一定顺序使用刀和叉,一个用完了思考时再让给另一个
,F&a.F]diZ0用那就能很好地完成目标而不会发生死锁,正时因为对共享资源占用的顺序是无法确定的.当一个结程占用51Testing软件测试网.e R0m"~*|:n)\
一个共享资源时,要获取另一个线程占用的共享资源,而另一个线程释放这个共享资源的条件是以获取被原51Testing软件测试网W)m l+F(K/v.D R \
先被占用的共享资源时,才会发生死锁.
&Fp0\'Z`q J_0
y.j~1UrX$i'y2K0所以如果我们破坏上面其中之一的条件就不会发生死锁问题,也就是在设计时要考虑不要同时发生上面的51Testing软件测试网e8{1p&RFi
三程情况.
p#c+p&Fq051Testing软件测试网 b4Gk ZZY6^X~(Wl
[嵌套锁定]51Testing软件测试网&r n:{b!x*O
对于同一对象的嵌套锁定,例子如下:
z*u"@ Ac1Ag(sG0synchronized(this){//151Testing软件测试网Wy;_7^Q6[t4W6[WN?
    System.out.println("outter");51Testing软件测试网8N:L'[ Prp}MO
    synchronized(this){//251Testing软件测试网 md1PUD)O8U%HK
        System.out.println("inner");51Testing软件测试网3v(rr"E*JNj
    }
8v4Z3Q(w1NsW0}
W@*?h5C1{E7kvD{0这个例子能运行吗?答案是可以很好地运行.51Testing软件测试网 _[e,KeP
一般以为线程运行到1时,获取了当前对象锁,打印outter后,运行到2,又要获取当前对象锁,而此时当前对象51Testing软件测试网+q t J/dz(kS
锁还没有释放,所以线程一直等在这儿发生死锁.
F7i"\at-S y`!D0其实java是一种smart language,在编译的时候,它就会检查对同一对象的嵌套锁定.因为不可能发生在层同51Testing软件测试网,M'rd%V.Ljf
步块中有多个线程进入而其中一个线程要进入内层同步块的情况,也就是外层同步块本身就可以保证只有一51Testing软件测试网HW6}7qf8Z
个线程获取同步对象的锁,所以内层同一对象的同步块在编译的时候已经失去它的作用.
0y$R V%Jk%|+f051Testing软件测试网3o)`U[-M!p Mu
[继承和扩展]51Testing软件测试网7h7K/cq0?"Q tL
对于临界区模式而言,即使我们已经使用synchronized方法对共享资源进行保护,但是子类在扩展接口时很可
l7d@,_^{hsQ0能将共享资源以不安全方式暴露出去.这是非常值得注意的问题.设计时应该尽时将对共享资源的访问方法加
+T%sp8i P&jA0UL0以保护,可以使用private和final等限制,另外在子类设计时也要充分考虑对父类共享资源的访问.51Testing软件测试网zM9p srRW5d


TAG: 软件开发

 

评分:0

我来说两句

Open Toolbar