为什么单例对象的并发调用需要同步?
关于“单例同步”:
一直有人在问单例对象的并发调用是否需要同步,基本属于“月经帖”了,答案是现成的满天下都是,但真正能让人心里踏实下来的解释寥寥无几。实际上,只要学习了一些JVM的运行原理,解释这个问题就不难了。
如果一个类是单例的,比如某些DAO的设计,那么所有的线程来访问这个类的实例的时候,它们获得的都将是同一个对象,这是不言自明的。如果这些线程的当前 操作是“互斥”的,那么每个线程就必须在取得该实例的访问资格的时候为该对象上锁,以独享该对象直到当前操作结束,以免在操作中途被其它线程介入而产生不 可预知的结果。问题是,什么样的操作是“互斥”的呢?
简单地说,互斥操作就是两个操作企图对它俩共享的某个资源进行修改,而修改的结果是不可预知的。于是问题就变成了,什么才是“共享的资源”?从纯粹 java语法的角度这个问题没法解释,因为它遵循的是当前java虚拟机的规范描述。现假设两个线程正企图同时访问一个单例对象的方法,如,
- int method1(int i) {
- int j = 3;
- return i+j;
- }
一个规范的虚拟机线程在调用method1()的时候是这样做的:
1) 把method1()的局部变量,包括参数,压入当前线程的栈;
2) 从当前线程栈弹出变量j,并赋予数值3;
3) 从当前线程栈弹出参数i,与j执行加法运算;
4) 从当前线程栈中释放当前方法占用的栈帧,并把method1()的结果压入当前线程栈。
需要说明的是,当前线程栈是当前线程独有的,绝对不会被其它线程访问到。这样,只要你在method1()里面使用的全都是局部变量或参数,那就不需要为多线程的并发调用发愁,因为每个线程都有自己的栈帧,各不相干。
复杂一点,如果method1()是这样定义的:
- int method1(int i, SingletonClass singleObj) {
- singleObj.intValue ++;
- int j = i + singleObj.intValue;
- return j;
- }
这下我们就不得不考虑线程同步问题了,这个方法显然包含了一个互斥的操作“singleObj.intValue ++;”。 前面说过,方法的参数会被压入当前线程私有的栈直到方法结束,但这里要注意的是,singleObj只是一个引用地址而非真正的对象实例,因此,尽管 singleObj这个引用值是被压入线程私有栈去的,但真正的对象实例却是在堆里存放的,栈虽然是线程私有的,堆却是所有线程共享的,因此 singleObj的成员变量intValue是完全有可能在当前线程执行第二行代码前被其它线程修改了的。比如说,线程1调用mothod1()的时候 singleObj.intValue的值是1, i的值是2,那么正确的情况下,method1()的返回值应该是4。但当线程1和线程2几乎同时调用method1(),线程2恰好在线程1把 intValue变成2之后的一瞬间又执行了一次singleObj.intValue ++,由于singleObj是单例,两个线程遇到的singleObj是同一个对象,因此这次运算将把intValue变成3。接下来线程1继续第二行 代码,结果j的结果变成了i+3 = 2+3 = 5 。 如此一来,线程1调用method1()的返回结果究竟会是 4 还是 5 是无法确定的,只能凭运气,寄望线程2在线程1从调用method1()到取得返回值之间的这段时间打盹。在绝大多数情况下,这种“凭运气”的做法是不能 接受的,我们需要向线程1保证,在它调用method1()期间绝不会收到线程2的干扰。做法如下:
- int method1(int i, SingletonClass singleObj) {
- int j = 0;
- synchronize(singleObj) {
- singleObj.intValue ++;
- j = i + singleObj.intValue;
- }
- return j;
- }
这个写法仍然有缺陷,因为线程2很可能在线程1执行int j = 0 的时候修改singleObj的intValue,所以比较可靠的应该在调用method1()之前锁住singleObj:
- synchronize(singleObj) {
- int result = obj.method1(2, singleObj);
- }
小小总结一下,“一个方法如果涉及对某个共享对象(或堆对象)的写操作,那么它必须同步该对象”这个说法在大多数情况下都对,但还有些失之笼统,或许这样说比较准确些,“如果一个方法对某共享对象的写操作会造成其它线程返回值的不确定性,则该方法应该同步该对象。”
更正 :本文出现的书名应该是《Inside the JVM》,之前误写作《Deep Into JVM》了,感谢fantasybei网友提出来。本书是Java世界的经典著作,有兴趣的网友可以用书名在网上找到一大堆资料,其中文译名是《深入Java虚拟机》
-------------------------------------------------------------------------------------------------------------------------------
最近在阅读 《Inside the JVM》 这本书,结合一些日常工作学习中的感想,随便写一些东西,蜻蜓点水,不必有章法。
曾经很在意C++和Java之间的优劣比较,有一段时间尤其注意在网上搜索二者比较的文章,并不时参加一些口水战,比如下面这个帖子:
http://www.diybl.com/course/3_program/c++/cppsl/2008520/117228.html
在论坛里绝对是个口水飞溅潜力帖。现在想起来很好笑,其实Java跟C++几乎是不同领域的东西,它们之所以存在是因为各自领域的需要,比较一下 有助于C++程序员转移到Java去或相反,但优劣之说只能误人子弟。帖子的作者显然不懂虚拟机规范,因此会有所谓“骗局”一说,这里无意驳斥这个作者早 已无从考证的老文章,仅就虚拟机规范说几句。
《Inside the JVM》 一开始就指出,所谓“虚拟机”实际上在不同的语境下有不同的涵义。有时候它指的是虚拟机的“规范”(spec),有时候指的是虚拟机的具体实现(如Sun JDK, BEA JRockit),有时候指的是正在运行着的一个虚拟机的实例(你启动Tomcat或者JBoss,甚至一个j2se程序,都同时启动了一个虚拟机实 例)。书中如无特别说明,指的一般都是“规范”。
虚拟机规范规定了.class文件的格式、类装载的规则、运行时内存的逻辑区块、方法调用时栈的动作等等。一旦某个虚拟机的具体实现(如Open JDK)声称它实现了Java虚拟机规范,那么也就是同时声称它在运行时的外部行为跟规范中所描述的是一样的。对于Java程序员而言,他的“平台”只有 一个,就是虚拟机规范,只要他的.class编译完,无论到哪一个虚拟机上,Open JDK也好,JRockit也好,Sun JDK也好,甚至芯片级实现的JVM也好,都应该能够正常运行。这就是Java跨平台的真正涵义(当然,实际项目中,100%的跨平台项目是很少的,比如
有些没有完全遵守规范写出的bug,在这个虚拟机上运行的时候或许能糊弄过去,但在另一个虚拟机上却未必能够)。帖子作者认为Java的跨平台是个“设计 巧妙的骗局”,实在是言重了,我们不能天真地认为,“跨平台”就可以没有平台,事实上无论怎样我们总需要至少一个的。此外,即使是“跨平台”也是相对的, 一个平台因为没有JVM的实现而“跨”不上去,太正常了,世上哪有什么绝对的事。