专注于互联网--专注于架构

最新标签
网站地图
文章索引
Rss订阅

首页 »Java教程 » java多线程:Java 线程/内存模型的缺陷和增强 »正文

java多线程:Java 线程/内存模型的缺陷和增强

来源: 发布时间:星期三, 2008年12月17日 浏览:73次 评论:0
本文是由JR主持写作J2SE进阶部分章节整理而成J2SE进阶正在写作、完善阶段您阅读后有任何建议、批评请和我联系或在这儿留言J2SE进阶写作项目组感谢您阅读本文 Java在语言层次上实现了对线程支持它提供了Thread/Runnable/ThreadGroup等系列封装类和接口员可以高效开发Java多线程应用为了实现同步Java提供了synchronize关键字以及objectwait/noty机制可是在简单易用背后应藏着更为复杂玄机很多问题就是由此而起 、Java内存模型 在了解Java同步秘密的前先来看看JMM(Java Memory Model) Java被设计为跨平台语言在内存管理上显然也要有个统模型而且Java语言最大特点就是废除了指针员从痛苦中解脱出来不用再考虑内存使用和管理方面问题 可惜世事总不尽如人意虽然JMM设计上方便了但是它增加了虚拟机复杂程度而且还导致某些编程窍门技巧在Java语言中失效 JMM主要是为了规定了线程和内存的间些关系对Java员来说只需负责用synchronized同步关键字其它诸如和线程/内存的间进行数据交换/同步等繁琐工作均由虚拟机负责完成如图1所示:根据JMM设计系统存在个主内存(Main Memory)Java中所有变量都储存在主存中对于所有线程都是共享每条线程都有自己工作内存(Working Memory)工作内存中保存是主存中某些变量拷贝线程对所有变量操作都是在工作内存中进行线程的间无法相互直接访问变量传递均需要通过主存完成 > 图1 Java内存模型举例图 线程若要对某变量进行操作必须经过系列步骤:首先从主存复制/刷新数据到工作内存然后执行代码进行引用/赋值操作最后把变量内容写回Main MemoryJava语言规范标准(JLS)中对线程和主存互操作定义了6个行为分别为loadsavereadwriteassign和use这些操作行为具有原子性且相互依赖有明确先后顺序具体描述请参见JLS第17章 我们在前面章节介绍了synchronized作用现在从JMM角度来重新审视synchronized关键字 假设某条线程执行个synchronized代码段其间对某变量进行操作JVM会依次执行如下动作: (1) 获取同步对象monitor (lock) (2) 从主存复制变量到当前工作内存 (read and load) (3) 执行代码改变共享变量值 (use and assign) (4) 用工作内存数据刷新主存相关内容 (store and write) (5) 释放同步对象锁 (unlock) 可见synchronized另外个作用是保证主存内容和线程工作内存中数据致性如果没有使用synchronized关键字JVM不保证第2步和第4步会严格按照上述次序立即执行根据JLS中规定线程工作内存和主存的间数据交换是松耦合什么时候需要刷新工作内存或者更新主内存内容可以由具体虚拟机实现自行决定如果多个线程同时执行段未经synchronized保护代码段很有可能某条线程已经改动了变量但是其他线程却无法看到这个改动依然在旧变量值上进行运算最终导致不可预料运算结果 2、DCL失效 这节我们要讨论个让Java丢脸话题:DCL失效在开始讨论的前先介绍下LazyLoad这种窍门技巧很常用就是指个类包含某个成员变量在类时候并不立即为该变量个例子而是等到真正要使用到该变量时候才化的 例如下面代码: 代码1 Foo { private Resource res = null; public Resource getResource { (res null) res = Resource; res; }} 由于LazyLoad可以有效减少系统资源消耗提高整体性能所以被广泛使用连Java缺省类加载器也采用这种思路方法来加载Java类 在单线程环境下切都相安无事但如果把上面代码放到多线程环境下运行那么就可能会出现问题假设有2条线程同时执行到了(res null)那么很有可能res被化2次为了避免这样Race Condition得用synchronized关键字把上面思路方法同步起来代码如下: 代码2 Class Foo { Private Resource res = null; Public synchronized Resource getResource { If (res null) res = Resource; res; }} 现在Race Condition解决了切都很好 N天过后好学你偶然看了本Refactoring魔书深深为的打动准备自己尝试这重构些以前写过于是找到了上面这段代码你已经不再是以前Java菜鸟深知synchronized过思路方法在速度上要比未同步思路方法慢上100倍同时你也发现只有第该思路方法时候才需要同步旦res化完成同步完全没必要所以你很快就把代码重构成了下面样子: 代码3 Class Foo {Private Resource res = null; Public Resource getResource { If (res null){ synchronized(this){ (res null){ res = Resource;}} } res; }} 这种看起来很完美优化窍门技巧就是Double-Checked Locking但是很遗憾根据Java语言规范标准上面代码是不可靠 造成DCL失效原因的是编译器优化会调整代码次序只要是在单个线程情况下执行结果是正确就可以认为编译器这样“自作主张调整代码次序”行为是合法JLS在某些方面规定比较自由就是为了让JVM有更多余地进行代码优化以提高执行效率而现在CPU大多使用超流水线技术来加快代码执行速度针对这样CPU编译器采取代码优化思路方法的就是在调整某些代码次序尽可能保证在执行时候不要让CPU指令流水线断流从而提高执行速度正是这样代码调整会导致DCL失效为了进步证明这个问题引用DCL Broken Declaration文章中例子: 设行Java代码: Objects[i].reference = Object; 经过Symantec JIT编译器编译过以后最终会变成如下汇编码在机器中执行: 0206106A mov eax,0F97E78h0206106F call 01F6B210 ;为Object申请内存空间 ; 返回值放在eax中02061074 mov dword ptr [ebp],eax ; EBP 中是objects[i].reference地址 ; 将返回空间地址放入其中 ; 此时Object尚未化02061077 mov ecx,dword ptr [eax] ; dereference eax所指向内容 ; 获得新创建对象起始地址02061079 mov dword ptr [ecx],100h ; 下面4行是内联构造0206107F mov dword ptr [ecx+4],200h 02061086 mov dword ptr [ecx+8],400h0206108D mov dword ptr [ecx+0Ch],0F84030h 可见Object构造尚未但是已经能够通过objects[i].reference获得Object对象例子引用 如果把代码放到多线程环境下运行某线程在执行到该行代码时候JVM或者操作系统进行了次线程切换其他线程显然会发现msg对象已经不为空导致Lazy load判断语句(objects[i].reference null)不成立线程认为对象已经建立成功随的可能会使用对象成员变量或者该对象例子思路方法最终导致不可预测 原因的 2是在共享内存SMP机上每个CPU有自己Cache和寄存器共享同个系统内存所以CPU可能会动态调整指令执行次序以更好进行并行运算并且把运算结果和主内存同步这样代码次序调整也可能导致DCL失效回想下前面对Java内存模型介绍我们这里可以把Main Memory看作系统物理内存把Thread Working Memory认为是CPU内部Cache和寄存器没有synchronized保护Cache和寄存器内容就不会及时和主内存内容同步从而导致条线程无法看到另条线程对些变量改动 结合代码3来举例介绍说明假设Resource类实现如下: Class Resource{ Object obj;} 即Resource类有个obj成员变量引用了Object个例子假设2条线程在运行其状态用如下简化图表示: > 图2 现在Thread-1构造了Resource例子化过程中改动了obj些内容退出同步代码段后采取了同步机制Thread-1所做改动都会反映到主存中接下来Thread-2获得了新Resource例子变量res由于没有使用synchronized保护所以Thread-2不会进行刷新工作内存操作假如的前Thread-2工作内存中已经有了obj例子份拷贝那么Thread-2在对obj执行use操作时候就不会去执行load操作,这样来就无法看到Thread-1对obj改变这显然会导致运算结果此外Thread-1在退出同步代码段时刻对ref和obj执行写入主存操作次序也是不确定所以即使Thread-2对obj执行了load操作也有可能只读到obj初试状态数据(注:这里load/use均指JMM定义操作) 有很多人不死心试图想出了很多精妙办法来解决这个问题但最终都失败了事实上无论是目前JMM还是已经作为JSR提交JMM模型增强DCL都不能正常使用在William Pugh论文Fixing the Java Memory Model中详细探讨了JMM些硬伤更尝试给出个新内存模型有兴趣深入研究读者可以参见文后参考资料 如果你设计对象在中只有个例子即singleton种可行解决办法来实现其LazyLoad:就是利用类加载器LazyLoad特性代码如下: Class ResSingleton {public Resource res = Resource;} 这里ResSingleton只有个静态成员变量当第次使用ResSingleton.res时候JVM才会个Resource例子并且JVM会保证结果及时写入主存能让其他线程看到这样就成功实现了LazyLoad 除了这个办法以外还可以使用ThreadLocal来实现DCL思路方法但是由于ThreadLocal实现效率比较低所以这种解决办法会有较大性能损失有兴趣读者可以参考文后参考资料 最后要介绍说明对于DCL是否有效个人认为更多种带有学究气推断和讨论而从纯理论角度来看存取任何可能共享变量(对象引用)都需要同步保护否则都有可能出错但是处处用synchronized又会增加死锁发生几率苦命员如何来解决这个矛盾呢?事实上在很多Java开源项目(比如Ofbiz/Jive等)代码中都能找到使用DCL证据我在具体实战中也没有碰到过因DCL而发生异常个人偏好是:不妨先大胆使用DCL等出现问题再用synchronized逐步排除的也许有人偏于保守认为稳定压倒那就不妨先用synchronized同步起来我想这是个见仁见智问题而且得针对具体项目具体分析后才能决定还有个办法就是写个测试案例来测试下系统是否存在DCL现象附带光盘中提供了这样个例子感兴趣读者可以自行编译测试不管结果怎样这样讨论有助于我们更好认识JMM养成用多线程思路去分析问题习惯提高我们设计能力 3、Java线程同步增强包 相信你已经了解了Java用于同步3板斧:synchronized/wait/noty它们确简单而有效但是在某些情况下我们需要更加复杂同步工具有些简单同步工具类诸如ThreadBarrierSemaphoreReadWriteLock等可以自己编程实现现在要介绍是牛人Doug LeaConcurrent包这个包专门为实现Java高级并行所开发可以满足我们绝大部分要求更令人兴奋这个包公开源代码可自由下载且在JDK1.5中该包将作为SDK部分提供给Java开发人员 Concurrent Package提供了系列基本操作接口包括syncchannelexecutor,barrier,callable等这里将对前 3种接口及其部分派生类进行简单介绍 sync接口:专门负责同步操作用于替代Java提供synchronized关键字以实现更加灵活代码同步其类关系图如下: 图3 Concurrent包Sync接口类关系图 Semaphore:和前面介绍代码类似可用于pool类实现资源管理限制提供了acquire思路方法允许在设定时间内尝试锁定信号量若超时则返回false Mutex:和Javasynchronized类似和的区别synchronized同步段只能限制在个思路方法内而Mutex对象可以作为参数在思路方法间传递所以可以把同步代码范围扩大到跨思路方法甚至跨对象 NullSync:个比较奇怪东西其思路方法内部实现都是空可能是作者认为如果你在实际中发现某段代码根本可以不用同步但是又不想过多改动这段代码那么就可以用NullSync来替代原来Sync例子此外由于NullSync思路方法都是synchronized所以还是保留了“内存壁垒”特性 ObservableSync:把sync和observer模式结合起来当sync思路方法被把消息通知给订阅者可用于同步性能调试 TimeoutSync:可以认为是个adaptor其构造如下: public TimeoutSync(Sync sync, long timeout){…} 具体上锁代码靠构造传入sync例子来完成其自身只负责监测上锁操作是否超时可和SyncSet合用 Channel接口:代表种具备同步控制能力容器你可以从中存放/读取对象区别于JDK中Collection接口可以把Channel看作是连接对象构造者(Producer)和对象使用者(Consumer)的间根管道如图所示: 图4 Concurrent包Channel接口示意图 通过和Sync接口配合Channel提供了阻塞式对象存取思路方法(put/take)以及可设置阻塞等待时间offer/poll思路方法实现Channel接口类有LinkedQueueBoundedLinkedQueueBoundedBufferBoundedPriorityQueueSynchronousChannelSlot等 图5 Concurrent包Channel接口部分类关系图 使用Channel我们可以很容易编写具备消息队列功能代码举例如下: 代码4 Package org.javaresearch.j2seimproved.thread;Import EDU.oswego.cs.dl.util.concurrent.*;public TestChannel { final Channel msgQ = LinkedQueue; //log信息队列 public void (String args) { TestChannel tc = TestChannel; For( i = 0;i < 10;i ){ Try{ tc.serve; Thread.sleep(1000); }catch(InterruptedException ie){ } } } public void serve throws InterruptedException { String status = doService;//把doService返回状态放入Channel后台logger线程自动读取的 msgQ.put(status); } private String doService { // Do service here "service completed OK! "; } public TestChannel { // start background thread Runnable logger = Runnable { public void run { try { for (; ; ) .out.prln("Logger: " + msgQ.take); } catch (InterruptedException ie) {} } }; Thread(logger).start; }} Excutor/ThreadFactory接口: 把相关线程创建/回收/维护/调度等工作封装起来而让者只专心于具体任务编码工作(即实现Runnable接口)不必显式创建Thread类例子就能异步执行任务 使用Executor还有个好处就是实现线程“轻量级”使用前面章节曾提到即使我们实现了Runnable接口要真正创建线程还是得通过 Thread来完成在这种情况下Runnable对象(任务)和Thread对象(线程)是1对1关系如果任务多而简单完全可以给每条线程配备个任务队列让Runnable对象(任务)和Executor对象变成n:1关系使用了Executor我们可以把上面两种线程策略都封装到具体Executor实现中方便代码实现和维护 具体实现有: PooledExecutorThreadedExecutorQueuedExecutorFJTaskRunnerGroup等 类关系图如下: 图6 Concurrent包Executor/ThreadFactory接口部分类关系图 下面给出段代码使用PooledExecutor实现个简单多线程服务器 代码5 package org.javaresearch.j2seimproved.thread;import java.net.*;import EDU.oswego.cs.dl.util.concurrent.*;public TestExecutor { public void (String args) { PooledExecutor pool = PooledExecutor( BoundedBuffer(10), 20); pool.createThreads(4); try { ServerSocket = ServerSocket(9999); for (; ; ) { final Socket connection = .accept; pool.execute( Runnable { public void run { Handler.process(connection); } }); } } catch (Exception e) {} // die } Handler { void process(Socket s){ } }}
0

相关文章

读者评论

发表评论

  • 昵称:
  • 内容: