java,concurrence,thread

最近读完<Java并发编程实践>,无疑这是一本好书,它使得我对线程,锁,以及与并发相关的
一些知识体系更加完备,闲话不多说,下面我系统梳理了下里面的一些理论要点,相信实践上
面,大家或多或少都会有体会,那么用理论的知识点将实践体会笼络起来,将会形成更加牢固
和完备的知识体系,至少我这么认为,写这一篇笔记也是用以以后wen故(因为'温'字是和谐字
,不益多讲)

一 线程安全的概念
在多线程的程序中,共享对象(共享信息)会被多个线程以不可预知的方式访问,这些线程
有的会修改共享对象的一些信息,有的会读一些信息,如果我们不加以控制,那么线程的所有
在这个共享对象上的行为将都不可信,
比如线程A修改了共享对象O,下一步准备保存数据,但在保存之前,操作系统调用了线程B
的指令进行执行(这种情况出现的机率相当大的,因为保存数据可能涉及IO密集型操作,在进行
IO准备的间隙,CPU周期是空闲的,但操作系统不会让CPU闲着,会分配给B的指令,结果B又修
改了O,那么线程A将B改过的结果保存了,还以为是自己改过的结果--如果类比于数据库中的
概念这就是"写覆盖",类似的"脏读","幻读"什么的都是差不多,因为无论是程序还是数据库中
锁的概念和用途都是一样的.

二 线程安全的基本原则
那么要编写线程安全的对象需要做些什么,或者说遵循什么基本原则呢?
一个线程安全的对象需要保证以下两个原则:
一),对象属性的可见性
"可见性"是指线程B能感知A对O的修改,因为在上述例子中,可能B需要将O拿来与某旧值
比较,如果一致,就修改,否则不作修改,然而在B得取O值后,A立即又对O进行了修改,这时B是
无法知道手上的O是过期数据的,所以才会发生"写覆盖"这样的杯具,不然B知道了A已经修改了
O,那么B可能什么也不做,A随后的保存操作也是一致的.
二),对象状态的约束性
对象状态是由多个(可以是一个)对象属性表达的,"约束性"要求对象处于任何状态必须是
有效的,合法的,举个例子来说:一个List有一个size属性,有一个保存数据的数组, 那么在增加
一个元素时,应该将元素放入数组,然后size++,但是由于这是两个操作,中间必定存在不一致
的时刻,但这种情况是无法避免的,但可以通过加锁等手段保证该操作的原子性.
可见,"可见性"是对单个属性的要求,"约束性"则是对属性间的关联要求.
另外,上面List的例子是共享对象下的要求,大多数情况下,我们是在线程栈里使用的,并不
需要考虑这些问题.(后面会提到类似问题)

三 可以采用的手段
那么我们有哪些可用的手段来实现上面的原则呢?(下面的手段可能只能保证其中一条原则)
我们可以使用的手段包括:锁,volatile变量,automicXXX类,以及纯程封闭.再次强调,这几
种手段并不是每种都能完全保证上面的二个原则.下面对每种做一个简要介绍,后面将重点介绍
锁.
一),锁:相信都十分熟悉了,包括内部锁(对象锁,类锁),显示锁,锁的作用主要有二点:1.互斥
2.可见性.所以锁是满足上面两个原则的
二),volatie变量:详细请见http://docs.google.com/View?id=dfk9z579_36dhvfkbfs这个是我从IBM上搜刮到docs里的,如果
想见原文,可以google一下,老实说到目前为止,搜了很资料,仍然不敢肯定自己对volatile变量的理解
是正确的, 因此在此不做解释,以免误导.
三),automicXXX类:单个类都满足上述二个原则,因此是线程安全,另外提醒一点:因为这些类
是可变对象,这与对应的简单类型包装类(不可变对象)不同,因此最好不要将之用于hashmap的key
四),线程封闭:线程封闭是一种思想,意思是如果场景允许,那么我们可以将可变对象临时性变为
线程私有对象,你必须保证每个时刻最多有且仅有一个线程会占有该对象, 对象池技术(线程池)便
是基于这种思想,还有一个ThreadLocal类用于实现线程私有数据(不过暂时还没有深入了解,到
目前为止只见过二个地方使用过,其中一个是hibernate使用它来绑定Session)

四 锁
锁是使用最广泛并且比较通用的线程安全手段.但是锁也有它自已的问题--活跃度问题.
包括
一),死锁:死锁大概分为两类
a.锁顺序死锁:当线程互相等待对方持有的锁,并且谁都不会主动释放自己的锁时,就会发生.
这提醒我们在依次获取两个锁的时候,一定要小心,比如下面的场景:
public void doSomething(Object A, Object B){
get lock on A;
get lock on B;
do something;
release lock on B;
release lock on A;
}
表面上看是按照一定顺序来进行加锁的,实际上我们无法保证调用者传入的参数的顺序
比如线程A发生调用doSomething(A, B),而线程B发生调用doSomething(B, A)那么就有
潜在的危险了.
对于这种情况我们一般采取两种方式来避规风险:
1.对加锁顺序按照一定规则排序:
public void doSomething(Object A, Object B){
if(compare(A,B)<0)
{
Object tmp = A;
B = tmp;
A = B;
}
get lock on A;
get lock on B;
do something;
release lock on B;
release lock on A;
}
2.在获取锁失败时,便放弃所有已经获得的锁:显然这是一种消极的加锁方式,但是简单的
失败总比使系统进入一个危险的死险要好得多.(显然,如果使用内部锁,是没办法采取这种手段的)
3.还有一种方式便是依据场景需求,适当提高加锁粒度,这样可以减小编码复杂度,降低出错
的可能性,而且一定程度上也减少加锁开销,虽然本书中没提到这种方式,
但是我认为在很多情况下,这种思想是行得通的,而且并不会给性能带来太大的问题.(比如mysql中
行锁,页锁,表锁在不同情况下工作得都非常好)
b.资源死锁: 当线程互相等待对方持有的资源,并且谁都不会主动释放自己的资源时,就会发生.
(将a中的锁换成资源就是了),如果把锁也看成是一种资源,那么a属于b的一种特例
思路仍然同a)中解决办法一致
二.活锁
活锁是指线程在处理事情的时候发生了失败,而程序对失败的措施是简单的重试,这样这个线程
就永远在这个地方循环下去,像被锁住一样,无法终止. 还有另一种情况是两个线程互相修改对方的
状态(信息)导致双方无法继续下去,一直循环切换双方的状态,这好比两个有礼貌的人互相让路,结果
老是碰到一边去,永远无法避开.(这个例子十分形象).
还有一些属于并发程序中可能碰到的其它活跃度问题:如线程饥饿(CPU调度策略问题),弱响应性
问题.
五 锁的性能
锁的性能问题主要产生自对锁的竞争,竞争越激烈,性能越低,
而锁竞争的激烈程度又取决于1.持有锁的时间;2.请求锁的频率
主要手段有:
a.缩小锁的范围,主要表现在缩小锁住的代码块,快进快出
b.减小锁的粒度,分拆锁,分离锁,
对于分拆锁,分离锁我是这样理解的,分拆锁相当于数据库中常见的
垂直分割,也就是将大粒度的锁从逻辑上划分开成多个独立的锁;分离锁
相当于sharding,ConcurrentHashMap就是采用这种方式.
以上是针对锁竞争的一些基本建议原则,但有时候我们需要逆向思维
(反模式)比如有时候
c.提升锁的粒度,将多个关联性较强的锁合并为同一个锁,这并不是
没有道理的,因为过细的粒度引入过多的锁,以及相关的加锁,释放以及
重新激活所带来的开销,N个线程所引入的开销在最坏情况下是o(n^2)
六 锁的实施
了解了这么多关于锁的话题,那么我们应该怎样使用锁呢,我们将使用
语义上的锁概念,(书中原文指出所有的锁在语义上是相等的,按照我的理解
应该是指JVM对锁的处理是一致)
通常会象下面这样来使用锁
get a lock
if(check condition)
dosomething
release the lock
然而仅仅这样是不够的,当我们试图获取锁的时候会有多种情况,成功
拿到锁的线程往往需要检查一些条件,比如资源是否可得,等等,如果资源
还没准备好怎么办呢?,其它的线程未能获得锁的线程又该何去何从呢?
对于未能获得锁的线程
理所当然的被操作系统(或JVM)挂起,直到再次被唤醒,
(当然这并不是一定的,对于象读写锁这样的扩展锁概念来讲),
对于拿到锁但条件还未满足的线程
可以选择如下几种方式处理失败情况:
1.轮循, 用循环不停地检查,通常这种方式会比较低效,但并不是绝对的
2.自旋等待(轮循+sleep), 也是不停地检查,不过检查一次歇一会,
有效利用cpu周期,因为在1的方式中,大部分情况下可以预料到是在浪费
CPU处理周期
3.挂起(wait), 资源没好,那线程就挂起休息,同时让出锁,等待别人叫醒,
一般情况来讲这当然是比较好的方式,但同样这也不是绝对的,后面将提及
通常来讲3比1更好,不会浪费很多不必要的CPU资源,然而在某些场景下
1却会比3更有效,比如资源往往很快就可以准备好,如果简单将线程挂起,马
上又不得不唤醒,在并发量大的情况下,往往会降低效率,这取决于对资源
的评估与线程的切换开销的比例,
2的方式是在1,3的一个折衷,一般情况下我们应该优先考虑使用2的方案
,如果2无法满足要求,再考虑使用3,毕竟3的编码比2要复杂些,出错的机率也
大得多
(一般情况是适用大多数的普遍情况,但并不是所有的,编程的世界没有'银弹'
,但我们需要了解特殊情况,对比其中的差异,这些差异往往能解释我们心中
的疑惑)
我们可以通过synchronized同步块来获得内部锁,或者使用显示锁类
来获得锁.但上面这三种方案是需要自己来实现的,(这并不复杂),当我们
处理完后通常需要释放锁和通知其它等待锁的线程,当代码离开同步块时,
JVM会代我们完成这种事,当使用显示锁时,我们就不得不自己做这样的事
然而这个地方我始终有个地方有点疑惑,在使用显示锁时
...
lock.lock();
try{
...
lock.notifyAll();
}finally{
lock.unlock();
}
这样看起来是先做的唤醒操作,然后才释放的锁?[这个疑问暂未解决]
--------------
所以我们使用锁时往往是这样的(3的方案)

...
lock.lock();
try{
while(条件不满足) //这个地方可不是轮循/或自旋等待
lock.wait(); //因为当再次被唤醒时,必须再次检查条件
...
lock.notifyAll();
}finally{
lock.unlock();
}

...
synchronized(lock){
while(条件不满足) //这个地方可不是轮循/或自旋等待
lock.wait(); //因为当再次被唤醒时,必须再次检查条件
...
}
(这两种在最终的指令上我想应该没太大区别,表面上的区别只是因为
JVM帮我们做了一些准备和善后工作而已.)

六 CAS(比较并交换)和非阻塞算法
关于这个还没有深入去查资料,此书上也只是讲了基本原理,简单来说
CAS就是利用硬件的支持,将比较并交换这样原本两个独立的CPU指令
合并成完整的CPU指令(可以理解为在CPU指令级别上实现原子性操作)
现代的JVM(5.0以后)就可以利用机器的这种特性来实现AutomicXXX类
如果在不支持这种特性的机器上,JVM会自动切换成自旋等待或轮循的方式
来实现
非阻塞算法便是建立在CAS的概念之上的,
这个以后还要找资料看下.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值