实战篇秘籍 (一)
本来这几节的内容想拿出来单独放在秘籍篇中.但由于我在实战篇五的结尾处说:
关于这个例子仍然还有很多可说的内容,所以经由这句话所引出的秘籍篇,作为实战篇的
继续,就叫它实战秘籍.
在实战篇五中,有两个非常关键的重点需要在本节介绍的,之所以称之为秘籍,不客气地说
就是很多高手根本没了解的(不是不理解,是压根就不知道)甚至一些在全球小有名气的大牛
都是不很清楚.当然,还是有不少人是非常清楚的,否则我的知识又从哪里来?
在前面的例子中,我们曾经有一个用多个线程打印一个实例字段,以及我在实战篇莫名其妙
地提到double check,如果说打印那个实例字段的目的只是为了说明产生一个实例的多个线程,
那么确切地说,提出double check和在boolean isInterrupted = false;的注释则是一个非常
重要的关注点.
事实上,如果按现有的线程知识,你根本看不出来double check以及目前的
boolean isInterrupted = false;有什么问题,但是如果你在多线程环境下,说不定什么时候
就会发生你意想不到的问题,也许你永远碰不到它的发生,但事实上它确实有问题.
引起问题的关键就是[Java内存模型]
Java是在语言级提供对线程的支持,所以Java的内存模型分为主存储器和工作存储器.
[Main memory]主存储器就是实例所在的存储区域,所有实例本身都被放在主存储器中,当然这
句话本身就说明了实例的字段也在主存储器中,主存储器被实例的所有线程所共有.
[working memory] 工作存储器当然就是每个线程所专有的工作区域,当然其中有它们共有的
主存储器中的一些必要的如实例字段等数据的COPY
当然你千万要知道Java是运行在虚拟系统上的,我们说的主存储器和工作存储器都是在物理内存
中虚拟出来的.是在JVM内部而言的.
在JLS中,对字段的存取被规定为read/write,use/assign,lock/unlock 六个最小的操作(action)
对于取而言,当一个线程一次访问实例字段时,首先从主存储器复制该字段的值到工作存储器,
然后线程引用该值.
但是如果同一线程多次访问该字段,是否每次都是从主存储器复制到工作存储器再引用,这由具体
的jvm环境决定.
简单说有可能不从主存储器中复制而直接引用工作区的COPY
同样对于存,如果一个线程一次赋值实例字段,那么会在工作存储器中进行,然后由jvm决定什么时
候映射到主存储器.
而同一个线程如果多次反复对实例字段赋值,那么有可能只对工作存储器的COPY进行,只把最后的
结果同步到主存储器,当然也有可能是每次都把工作存储器和主存储器同步的.这也由具体的JVM决
定.
所以如果多个线程反复对同一实例字段存取,就有可能一个线程对这个字段的改变没有及时反映给
其它线程。因为只有当字段被工作存储器同步到主存储器,字段的改变才能让其它线程知道,而在
线程专有的工作存储器中的值其它线程是不可访问的.
所以boolean isInterrupted = false;这一句有可能一个线程中断后在这个线程工作的范围内已经
设为true,但还没有立即被映射到主存储器时,其它线程还不知道.
同样,对于double check,我们来看它的问题:
在java与模式一书中,作者这对个问题的说明根本没有正确地理解.而且他一再说明是错误的例子
其实是正确的.
public MyObject{
private static MyObect obj;
public static getInstance(){
if(obj == null){
synchronized(MyObj.class){
if(obj == null)
obj = new MyObject();
}
}
return obj;
}
}
这个例子根本不会存在问题.因为线程的同步保证了不会在同步块外部发生多线程调,而在同步块中
只有一个线程能执行,其赋值的结果在离开同步块时会强制映射到主工作区,也就是对obj的赋值一定
会在主工作区反映出来.所以作者举这个例子说明他根本没有理解为什么double check是不安全的.
我们来看下面的例子:
public MyObject{
private static MyObect obj;
private Date d = new Data();
public Data getD(){return this.d;}
public static MyObect getInstance(){
if(obj == null){
synchronized(MyObect .class){
if(obj == null)
obj = new MyObject();//这里
}
}
return obj;
}
}
一个线程A运行到"这里"时,对于A的工作区中,肯定已经产生一个MyObect对象,而且这时这个对象已经
完成了Data d.现在线程A调用时间到,执行权被切换到另一个线程B来执行,会有什么问题呢?
如果obj不为null,线程B获得了一个obj,但可能obj.getD()却还没有初始化.
为什么?既然obj已经可见了(线程A还没有离开同步块),而d却不可见呢?
如果d不可见,那么obj也应该为null啊?线程B应该等待A释放同步块啊?
事实上,对于"这里"这条语句,线程A还没有离开同步块.
因为没有"离开同步块"这个条件,线程a的工作区没有强制与主存储器同步,这时工作区中有两个字段
obj,d 到底先把谁同步到主存储区,没有条件限制,虽然在线程A的工作区obj和d都是完整的,但有JSL
没有强制不允许先把obj映射到主存储区,如果哪个jvm实现按它的优化方案先把工作存储器中的obj
同步到主存储器了,这时正好线程B获取了,而d却没有同步过去,那么线程B就获取了obj的引用却找不能
obj.getD();
这个问题是否真的会发生?
我个人的意见,从理论上是会发生的,所以如果是设计银行交易系统你就没有必要为提高一些性能而放
弃安全性,而如果只是一般的应用,发生这种情况的机率本来就非常发生也不会有太多的危害,我认为
可以不去介意.比如获取数据库连结,即使获取不到也不过让访问者再刷新一次重新查询而已.事实上有
可能几天,几个月,一年都不会发生一次.知道小行星有可能会撞地球的,但你不要太在意而影响你正常
的生活.如果你不是做银行交易系统的你不要太担心,而大多数JVM实现本身就会保证这种安全的.也就
在把obj同步到主存储器会先同步d,但万一发生相反的顺序,你就无权责备你所用的JVM.只是这种万一
机会不是很多.
当然如果是贪婪式调用,静态实例的初始化由ClassLoader经由[Thread Safe]来完成,当然就不会有这
个问题了:
public MyObject{
private static MyObect obj = new MyObject();
private Date d = new Data();
public Data getD(){return this.d;}
public static getInstance(){
return obj;
}
}
那么如何保证实例字段能在工作存储区能被即时映,下一节我们来讲最不常用的关键字:
[volatile]
volatile 变量
volatile 是用来保证[内存同步]的关键字,内存同步是说在某个线程中修改某实例字段能够及时地
更新到主存储区,而某线程如果需要引用该字段也能及时地从主存储区中得到最新的数据.(修改时:
赋值和同步到组内存被作为一个原子操作(原子操作[最小的操作(action)]是虚拟机中最小单位,
不会被线程切换打断);引用时:内存同步到工作区和从工作区取值也是一个原子操作;但是修改应用后放着不管不会自动同步,是半自动的。)
简单说它是当前线程的工作存储区和主存储区对某字段的及时同步,所以我们说它是[内存同步],但它
不是线程同步.
也就是说,一个线程对 volatile字段进行更新时,它只会把更新后的值及时地同步到主存储区,而并不
保证其它线程中该字段的值得到及时更新.(不过其它线程如果要对该字段访问的话还会及时从主存储
区COPY的).
因为只保证当前线程中对volatile字段的更新会及时与主存储区同步,所以volatile并不能达到同步的
功能.比如线程A和B最初的 volatile int x = 10;当线程A奖x +1后,线程A的工作存储区的x和主工作存
储区的x都为11,但这时线程B中的x仍然为10,只有当线程B需要访问x时才会从主存储区中COPY x的值使
x为11.但是如果线程B没有对x进行操作,而在某种条件下如离开同步块,进行工作存储区与主存储区同步
时,就会把当前值10覆盖到主存储区中.
所以 volatile 关键字并不能保证多个线程同时对主存储区字段的"同步".只有在某种条件下,最合适的
情况就是只有某一线程修改某一变量后,其它线程只是对其进行引用而不再更新而且在同步块内(离开同
步块会自动同步).比如我们设置的线程
退出标记.
volatile boolean exit = false;
这样的变量用于在所有线程中判断是否退出,其中任何一个线程修改为true后,会及时更新主存储区中
该变量的值,而其它线程也能及时获取到该值的变化而及时地结束自己的工作.
volatile 可以保证 double和long操作是原子操作.
原子操作(atomic)是指对变量从主存储COPY到线程工作存储区时,经及从线程的工作存储区写回到主存储
区时,不会因为其它线程的同时操作而改变变量的值.
对于普通变量,double和long在设计的时候并不是原子型操作,也就是对地这两种变量的上述操作在一定情
况下可能会因为其它线程而覆盖我们的操作.那么volatile可以保证double和long象int一样的atomic操作.
另外,从JSR133以后,由于对JAVA内存模型进行的修复,volatile 变量有了更大的作用.
对于前面讨论的比检锁问题,如果(这里只是假设,事实上我们不会这样做,后面说原因)我们对instance(obj)变量声明为
volatile 的,那么在第一个线程初始化后,如果线程a中的Date d是可访问的,那么对于线程B中也是可见的.也就是说它修补了双验锁的缺陷,使JAVA的双检锁不再存在陷井.
那么真的这么简单地就修补了这个陷井吗?从理论上说,是的.但实际上.它没有真正达到目的.因为这样的保证消耗了大量的性能.因为一量声明obj为 volatile ,你不能在第一次被始化成功后再将instance恢复成非volatile 的,那么每次对install的引用都会引用大量的同步工作.这实际上比贪婪式声明static MyObject obj = new MyObect (); 消耗更多的性能,基本与public static MyObect getInstance()的同步方法相当,如此我们为什么要修改这样的双检锁呢?
分享到:
相关推荐
《Microsoft ®Office2003 实战秘笈》是一本旨在帮助用户提升 Office2003 使用效率的书籍,尤其关注 Excel、Word 和 PowerPoint 的实用技巧。以下是对书中的几个关键知识点的详细解释: 1. **Excel 分行填写**:在 ...
新媒体APP小程序营销实战秘籍.pdf新媒体APP小程序营销实战秘籍.pdf新媒体APP小程序营销实战秘籍.pdf新媒体APP小程序营销实战秘籍.pdf新媒体APP小程序营销实战秘籍.pdf新媒体APP小程序营销实战秘籍.pdf新媒体APP小...
股票实战秘籍主要涵盖了几大理论以及买卖时机的选择策略,对于投资者来说,这些知识是理解股票市场运作规律和制定投资决策的重要依据。 首先,能量理论强调了成交量在判断股票趋势中的关键作用。当股价上涨,成交量...
成功销售实战秘籍.ppt
潘勇主讲的《成功销售实战秘籍》深入探讨了如何成为优秀的销售人员,并提供了突破困境的策略。 首先,我们要认识到新时代下销售人员所面临的挑战。随着学历的普及,经验的普遍化,以及竞争的加剧,销售人员面临着...
质量人实战秘籍,主要探讨的是在实际工作中质量管理所面临的挑战和解决之道。在这个文档中,几位质量人,包括SQE(供应链质量工程师)、QE(质量工程师)、内审员、外审员以及质量经理,围绕质量问题展开了一场深入...
公关新闻策划实战秘籍.pdf
2012关于微信营销的8条实战秘笈.pdf
期货投资交易实战秘籍洪凯PPT学习教案.pptx
【公关新闻策划实战秘籍】 公关新闻策划是企业或组织运用新闻媒体,通过精心设计和策划,提升品牌形象、传播信息、影响公众舆论的一种策略。它并非简单地制造热点,而是一个系统的过程,包括新闻调查分析、新闻规划...
【公关新闻策划实战秘籍】是公关活动中的一种重要策略,旨在通过精心设计的新闻事件来提升品牌形象、扩大影响力。以下是对这一主题的详细解析: 1. **新闻调查分析**: 新闻调查是策划的基石,它涉及对市场、产品...
这份压缩包提供了五本关于树莓派的电子书,涵盖了从入门到实战的多个方面,旨在帮助读者全面了解和掌握树莓派的操作与应用。 首先,"爱上Raspberry Pi"由(美)MATT RICHARDSON,SHAWN WALLACE著,李凡希译,科学...
本资源“公关新闻策划实战秘籍”提供了一份详细的操作指南,旨在帮助公关从业者和市场营销人员掌握这一领域的核心技巧。 一、公关新闻策划的定义与目标 公关新闻策划是指根据企业的战略目标,利用媒体平台,制定并...
《数字化转型实战宝典合集》是一份涵盖了企业数字化转型全过程的综合资源包,它旨在为企业提供实战指导,推动业务的现代化升级。这份压缩包包含了丰富的案例分析、标准规范以及专业书籍,帮助读者深入理解并实施数字...
教育精品资料
《90天实战SEO高手秘籍》是一门专为网络推广新手设计的SEO(Search Engine Optimization,搜索引擎优化)课程,旨在通过系统的学习和实践,帮助学员在90天内掌握SEO的核心技巧,提升网站在搜索引擎中的排名,从而...
4G+(VOLTE)实战经验VoLTE秘籍精品资料50个合集: LTE_VOLTE专题.pdf LTE语音解决方案--VOLTE调度机制的研究-5.24.pdf PDSCH功率-PaPb(精).pdf SEQ分析VOLTE实战操作指导书.pdf TD-LTE VoLTE语音质量(MOS)测试说明书....