-
单例模式并发的问题!30
public static Singleton getInstance() { if(instance == null) { synchronized(Singleton.class) { if(instance == null) { instance = new Singleton(); } } } return instance; }
在一本并发书上看见的,说这个单例会造成有一些问题,具体原因是因为java对读写共享对象的域时并不保证可线性化性,甚至不保证顺序一致性。原因在于,如果严格遵循顺序一致性,那么将会导致已被广泛采用的编译优化技术变得无效。
大牛们,解释下?说实话,我是看的一头雾水。
书中指出这段代码有误,但是紧跟的内容感觉和这段牛头不对马嘴,紧跟的内容大致讨论了同步块在线程中的作用,以及对线程cache拷贝和共享存储器之间的关系。
书名:多处理编程的艺术 - P42
------------
顺便说两句闲话,发帖到论坛被弄成隐藏贴。我想知道为什么会被隐藏。我感觉这个问题超出了问题的本生,很有高度的一个问题。
当然也有可能很菜的我,不能理解各位大神的思想。
问题补充:这其实是最常用的一种惰性初始化。问题是书中说这个有隐患,但终究不知隐患处在哪。
代码是先判断实例存在不存在,如果不存在,那么再去占有锁,但是可能在占有锁的同事,也就是第一个if的下一行,有多个线程跑进来,所以在锁里再次判断。
这样做的目的是达到最大吞吐量,因为能同时跑进第一个if块里面的几率只有那么一些。
问题补充:BruceXX 写道我的理解:
在访问这个单例对象的时候,这个单例方法是public static级别的,即共享对象,而外部(多线程)在首次访问这个方法体的时候,用synchronized 锁住这个对象,让多个线程排队(线性化)等待读写,理论上,我们要求第一个到达的线程会初始这个对象, 然而public static级别 的方法体(读写共享对象)并没有如他所说的(线性化),而是(java对读写共享对象的域时并不保证可线性化性,甚至不保证顺序一致性),所以有可能在还没在第一个线程初始化的时候,第二个线程就已经开始要访问了。。。所以有可能出错。。
(如果严格遵循顺序一致性,那么将会导致已被广泛采用的编译优化技术变得无效。)这句话,应该是共享对象在锁的级别上是拥有共享锁,让速度更优policy,而没有考虑synchronized 这种独享锁的概念,
以上我只是个人理解,仅供参考。。。
还是不太理解,既然synchronized (锁住这个对象了,也意味进入同步块),那么不管后面排队的线程谁先到,谁后到,当任意线程获取进入的时候,instance缓存始终会被刷新,这样if判断的时候就应该没问题啊。
如果按照你的推断,(所以有可能在还没在第一个线程初始化的时候,第二个线程就已经开始要访问了),那么意味着他们同时进入了同步块。是这样吗?
问题补充:anranran 写道在一本并发书上看见的,说这个单例会造成有一些问题,具体原因是因为java对读写共享对象的域时并不保证可线性化性,甚至不保证顺序一致性。原因在于,如果严格遵循顺序一致性,那么将会导致已被广泛采用的编译优化技术变得无效.
是这样:
虽然instance = new Singleton();只有一条语句,但可能分成CPU指令就会有好几个.JVM为了性能,这些指令可能并行执行.
看这个语句,构建对像,然后 置instance为非空,即指向这个对像所在地址.但是如果先置instance为非空,但还没有完成对像构建,而发了线程切换.线程2就会因为if(instance == null)这个条件不满足,而返回instance引用,但对像是不完整的,导至异常发生.
恩,我觉得应该是这个理,但是这样又应发出另外一个问题:public volatile Map cache = new ConcurrentHashMap(); public void destory() { cache = new ConcurrentHashMap(); }
比如,现在这个cache,每天晚上重置一次,我不是调用的map.clear().而是直接给个新对象,如果是这样,按照上面的假定,即这个对象还没有构造玩,线程切换了,但是cache已经引用到了这个未构造完的对象,这个时候别的并发线程就会对这个尚未构造完的对象进行操作,导致异常?
谢谢!
问题补充:piao_bo_yi 写道这道题答案不是很重要,思路比较重要,上面有的人说的答案也对,但是不精确(有时候不精确不会产生大的问题,有时候就会,下面会解释
这个观点),所以LZ最好亲自试试下面的过程。另外,本题涉及几个知识点。我详细解释下。
1.如果你是想在JAVA代码级别解释这个问题,那么你是在浪费时间。这个问题必须到JVM生成的代码级别讨论(很多问题都是这个样子,在
JAVA代码级别讨论不仅浪费时间,而且没有意义,记得有人跟我说过一句话:在你所处理的层面,问题根本还没有浮现(非编程问题))。
2.public class TestJVM { public static void main(String[] args) { TestJVM abc = new TestJVM(); } }
代码用javap -c 命令反编译TestJVM.class文件后(我建议你自己试试),生成
...public TestJVM(); Code: 0: aload_0 1: invokespecial #8; //Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #1; //class TestJVM 3: dup 4: invokespecial #16; //Method "<init>":()V 7: astore_1 8: return ...
解释这段代码是这道问题的第一步,建议你大概查阅下JVM规范,因为我也刚查了。
(1) new 的含义是创造一块内存,并且在堆栈上压入指向这块内存的引用。
(2) dup的含义是将栈顶复制,并压入栈。(所以现在有了两个指向刚才分配内存的引用)
(3) invokespecial意思是将分配的内存中初始化对象。
(4) astore_1是将栈顶压入本地变量。
(这段过程,我建议你自己多画几遍,体会下JVM"面向堆栈"的概念,JVM规范第一章最好看看)
3.上面的四个步骤(绝对的物理过程),其实就是三件事(体会一下原子语句的含义):
a.给实例分配内存。
b.初始化构造器
c.将引用指向分配的内存空间(注意到这步引用就非null了)。
一般来说,我们期望执行的步骤是a->b->c,然而,由于JVM乱序执行的特性(自己查查这句话在哪,别轻易相信别人,虽然有时候文档也是会
骗人的-!-),可能执行的顺序是a->c->b。当a->c->b这样执行时候,假如刚执行完c,这样线程2访问这个引用,发现引用不为空,他就对相
应的内存做操作,这样就会发生错误,这种错误想必不容易发现(那是不是不容易发生?取决于具体的应用环境。)。
4.问题的关键用一句话来概括,就是这个意思:if(instance==null),如果instance !=null,那么instance就真的准备好了么?
所以,最原始的写法虽然慢,但是不会产生这种问题,因为原始写法把判断是否等于null的语句,也给锁起来了。只有得到锁,才有资格判断
。
5.上面的几条,你也许看了第四条,或者大概明白前几条,你的问题就能解答了。不精确的了解似乎也能回答,但是,有好多误解就产生了。
比如,有人说,加了valatile类型修饰(JVM1.5以后)符可以将LZ的写法变对,如private volatile static Singleton instance = null;
其实这是不对的,valatile(LZ想想为什么valatile影响效率?理解下寄存器和内存的效率差别)无非说的就是线程是不能保留共享对象的本地
拷贝(正常情况线程是可以保留的),那是不是每次去内存中取,就能保证单例对象的正常初始化呢?很明显,这完全是两个问题。
6.很多细节问题(编程方面),你都得查查英文文档,得自己写试试,中文大家说的话都非常像(因为都是同一本书里面说的,再加上第一个
人的翻译水平不咋样),很多误解就此产生。
受教了,非常感谢!!终于搞清楚问题的关键了。那么接下来总结下:
1.java寄存器读写是无序的,这也是问题的根源。
2.针对这个问题就是在于最外层的if语句不在同步块中,所以即使下面的同步块是正确的,且同步块具有可线性化特性,但是这些都是语言级的功能,换句话说,是java来保证这些同步操作是一个原子操作。所以没在同步块中的if有可能拿到一个没有初始化完全的对象。
3.volatile 只是具备同步刷新寄存器于缓存之间的功能,这个步骤是原子操作。以上面为例,cache = new ConcurrentHashMap(); 这个操作不保证ConcurrentHashMap初始化以完成。但保证指针指向的分配区域所有线程可见。
所以这句话写成
Map temp = new ConcurrentHashMap();
cache = temp;
同样存在问题。
piao_bo_yi 不知我的理解可对?太感谢你了!
问题补充:piao_bo_yi 写道别客气,我也是晚上刚好有时间,呵呵。
1.java不是寄存器读写是乱序的,是指java字节码(刚才javap -c生成的那个)的执行是乱序的(具体咋实现的,我也很好奇),不过我明白你意思了。
2和3说的的基本正确。
但是这样,我如何保证在多线程中改变一个引用呢?
如果是这样,岂不是每一次改变引用都需同步起来?就比如我上面那个cache的例子,有一个定时器负责调度重置,平时有多个线程去读。如果这些读线程可能拿到一个未初始化好的实例,岂不是有问题?
问题补充:其实我觉得我进入了一个误区.比如:
private Map reset() {
Map temp = new HashMap();
cache = temp;
return cache;
}
这句话如果按照javap来反编译后读到指令集,类似于piao_bo_yi你上次提到的那一段。
但试想一下,如果return这个指令先执行会有什么结果呢?如果是这样,单线程也无法保证程序的正确性,因为按照前面的思路return后的同样可能是未被初始化的内存区域。但是显然这是不正确的。
我猜测,可能是一条语句所执行的指令集,或者假设JVM视为一个个的事件,这一个个的事件是由一组命令来完成的,他们不保证这些一组中的指令无序,但保证静态的调用顺序。
所以我觉得我理解进入了一个误区。算了,这些都太抽象了。等我把书先看完,有个清晰的轮廓后,在辅助一些资料以后再来开贴讨论。
在这里感谢各位回帖的朋友,希望大家越来越牛X。
特别感谢piao_bo_yi,以后恐怕还有问题要请教你,还望日后多多给与帮助。
谢谢大家!结贴!2010年1月28日 11:41
7个答案 按时间排序 按投票排序
-
采纳的答案
这道题答案不是很重要,思路比较重要,上面有的人说的答案也对,但是不精确(有时候不精确不会产生大的问题,有时候就会,下面会解释
这个观点),所以LZ最好亲自试试下面的过程。另外,本题涉及几个知识点。我详细解释下。
1.如果你是想在JAVA代码级别解释这个问题,那么你是在浪费时间。这个问题必须到JVM生成的代码级别讨论(很多问题都是这个样子,在
JAVA代码级别讨论不仅浪费时间,而且没有意义,记得有人跟我说过一句话:在你所处理的层面,问题根本还没有浮现(非编程问题))。
2.public class TestJVM { public static void main(String[] args) { TestJVM abc = new TestJVM(); } }
代码用javap -c 命令反编译TestJVM.class文件后(我建议你自己试试),生成
...public TestJVM(); Code: 0: aload_0 1: invokespecial #8; //Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #1; //class TestJVM 3: dup 4: invokespecial #16; //Method "<init>":()V 7: astore_1 8: return ...
解释这段代码是这道问题的第一步,建议你大概查阅下JVM规范,因为我也刚查了。
(1) new 的含义是创造一块内存,并且在堆栈上压入指向这块内存的引用。
(2) dup的含义是将栈顶复制,并压入栈。(所以现在有了两个指向刚才分配内存的引用)
(3) invokespecial意思是将分配的内存中初始化对象。
(4) astore_1是将栈顶压入本地变量。
(这段过程,我建议你自己多画几遍,体会下JVM"面向堆栈"的概念,JVM规范第一章最好看看)
3.上面的四个步骤(绝对的物理过程),其实就是三件事(体会一下原子语句的含义):
a.给实例分配内存。
b.初始化构造器
c.将引用指向分配的内存空间(注意到这步引用就非null了)。
一般来说,我们期望执行的步骤是a->b->c,然而,由于JVM乱序执行的特性(自己查查这句话在哪,别轻易相信别人,虽然有时候文档也是会
骗人的-!-),可能执行的顺序是a->c->b。当a->c->b这样执行时候,假如刚执行完c,这样线程2访问这个引用,发现引用不为空,他就对相
应的内存做操作,这样就会发生错误,这种错误想必不容易发现(那是不是不容易发生?取决于具体的应用环境。)。
4.问题的关键用一句话来概括,就是这个意思:if(instance==null),如果instance !=null,那么instance就真的准备好了么?
所以,最原始的写法虽然慢,但是不会产生这种问题,因为原始写法把判断是否等于null的语句,也给锁起来了。只有得到锁,才有资格判断
。
5.上面的几条,你也许看了第四条,或者大概明白前几条,你的问题就能解答了。不精确的了解似乎也能回答,但是,有好多误解就产生了。
比如,有人说,加了valatile类型修饰(JVM1.5以后)符可以将LZ的写法变对,如private volatile static Singleton instance = null;
其实这是不对的,valatile(LZ想想为什么valatile影响效率?理解下寄存器和内存的效率差别)无非说的就是线程是不能保留共享对象的本地
拷贝(正常情况线程是可以保留的),那是不是每次去内存中取,就能保证单例对象的正常初始化呢?很明显,这完全是两个问题。
6.很多细节问题(编程方面),你都得查查英文文档,得自己写试试,中文大家说的话都非常像(因为都是同一本书里面说的,再加上第一个
人的翻译水平不咋样),很多误解就此产生。
2010年1月28日 20:09
-
有人认为valatile能够保持原子操作,其实是很片面的(JVM只是保证对valatile的变量不做任何优化,也就是保证不保留线程对其的副本,也许将来JVM会将其实现为真正的原子操作,但是这得不到保证,而且,如果你依赖于某版本的JVM,你的代码将不具有可有移植性)。
所以安全的做法是(摘自THINGKING IN JAVA):
(1)将类中所有方法用synchronized同步(虽然你的目的是为某个方法同步),如果忽略了其中一个,你很难保证没有负面效应。
(2)去除同步控制时,你得非常小心。这主要基于性能上的考虑(但是JVM已经优化了同步的代码),所以只有当你用性能工具测试其真正为性能瓶颈时,才能这么做。2010年1月29日 11:58
-
别客气,我也是晚上刚好有时间,呵呵。
1.java不是寄存器读写是乱序的,是指java字节码(刚才javap -c生成的那个)的执行是乱序的(具体咋实现的,我也很好奇),不过我明白你意思了。
2和3说的的基本正确。2010年1月28日 20:59
-
在一本并发书上看见的,说这个单例会造成有一些问题,具体原因是因为java对读写共享对象的域时并不保证可线性化性,甚至不保证顺序一致性。原因在于,如果严格遵循顺序一致性,那么将会导致已被广泛采用的编译优化技术变得无效.
是这样:
虽然instance = new Singleton();只有一条语句,但可能分成CPU指令就会有好几个.JVM为了性能,这些指令可能并行执行.
看这个语句,构建对像,然后 置instance为非空,即指向这个对像所在地址.但是如果先置instance为非空,但还没有完成对像构建,而发了线程切换.线程2就会因为if(instance == null)这个条件不满足,而返回instance引用,但对像是不完整的,导至异常发生.
2010年1月28日 14:38
-
是的,因为它说 共享对象 的访问不是线性的,所以我做了这个假设..
在本机试了下,始终没出现第二个 thread先到达锁的。。
code==>
public class T { private String str = ""; private T() { this.str = Thread.currentThread().getName() + ".readlll"; } private static String waitIn = ""; private static T instance; public static T getInstance() { if (instance == null) { waitIn += Thread.currentThread().getName() + ","; synchronized (T.class) { System.out.println("wait in threads:" + waitIn); if (instance == null) { instance = new T(); } } } return instance; } public static void main(String args[]) { // ExecutorService pool=Executors.newCachedThreadPool(); for (int i = 0; i < 1000; i++) { // pool.execute(t); Thread t = new Thread(new Runnable() { public void run() { T t = T.getInstance(); System.out.println(Thread.currentThread().getName() + "===>" + t.str); } }); t.yield(); t.start(); } } } run: wait in threads:Thread-0, Thread-0===>Thread-0.readlll wait in threads:Thread-0,Thread-1, Thread-1===>Thread-0.readlll Thread-2===>Thread-0.readlll Thread-4===>Thread-0.readlll ....
2010年1月28日 14:13
-
我的理解:
在访问这个单例对象的时候,这个单例方法是public static级别的,即共享对象,而外部(多线程)在首次访问这个方法体的时候,用synchronized 锁住这个对象,让多个线程排队(线性化)等待读写,理论上,我们要求第一个到达的线程会初始这个对象, 然而public static级别 的方法体(读写共享对象)并没有如他所说的(线性化),而是(java对读写共享对象的域时并不保证可线性化性,甚至不保证顺序一致性),所以有可能在还没在第一个线程初始化的时候,第二个线程就已经开始要访问了。。。所以有可能出错。。
(如果严格遵循顺序一致性,那么将会导致已被广泛采用的编译优化技术变得无效。)这句话,应该是共享对象在锁的级别上是拥有共享锁,让速度更优policy,而没有考虑synchronized 这种独享锁的概念,
以上我只是个人理解,仅供参考。。。2010年1月28日 12:46
相关推荐
### 多线程单例模式并发访问 #### 一、多线程基础概念 在讨论多线程单例模式及并发访问之前,我们先来了解一些基本概念。 **进程**和**线程**是计算机科学中的两个核心概念,它们之间的关系紧密而复杂。 - **进程...
单例模式是一种设计模式,旨在确保一个类只有一个实例,并提供全局访问点。在单例模式中,类的构造函数是私有的,防止外部直接创建对象,而是通过静态方法获取该类的唯一实例。单例模式的唯一性通常是在进程范围内,...
考虑到并发访问时数据同步的问题,通常会采用单例模式实现计数器逻辑。这种方式能够确保即使在高并发环境下,也能准确地记录每一次访问。 **4. 应用程序的日志处理** 在开发和运维阶段,日志记录对于追踪错误和...
在C++编程中,单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点。在这个特定的场景中,我们讨论的是一个实现了单例模式的日志类,该类专为多线程环境设计,具备日志等级控制、...
单例模式是软件设计模式中的一种基础且广泛应用的模式,它的主要目的是确保一个类只有一个实例,并提供一个全局访问点。这种模式在系统中需要频繁创建和销毁对象,且对象创建成本较高,或者需要共享资源的情况下非常...
这种写法的问题是它没有考虑线程安全问题,在并发环境下很可能出现多个 Singleton1 实例。 2、懒汉式单例(加同步) public class Singleton2 { private Singleton2() {} private static Singleton2 single = ...
- **线程安全问题**:懒汉式单例模式在多线程环境下可能会导致创建多个实例,因此需要采用同步机制保证线程安全,例如使用`synchronized`关键字。 - **静态内部类方式** - **实现**: ```java class Single3 {...
双重检查锁单例模式(Doubly Checked Locking Singleton)是懒汉式单例模式的一种改进版,既实现了延迟加载,又解决了多线程安全问题,同时也减少了同步的开销。具体实现如下: ```java public class UserService { ...
- **并发问题**:在多线程环境中,单例模式可能会导致线程安全问题,需要额外处理同步机制。 #### 五、实例解析 在给定的内容中提到的`Martin`类就是一个典型的单例模式实现案例。它通过将构造器私有化以及提供一...
单例模式讲解说明与实例 单例模式是 Java 中一种常见的设计模式,分为懒汉式单例、饿汉式单例和登记式单例三种。单例模式有以下特点: 1. 单例类只能有一个实例。 2. 单例类必须自己创建自己的唯一实例。 3. 单例...
该资源是多线程并发下的单例模式-源码,几乎包含了所有方式实现的单例模式,并且能够确保在多线程并发下的线程安全性。 读者可结合本人博客 http://blog.csdn.net/cselmu9?viewmode=list 中的《线程并发之单例模式...
由于单例模式涉及到全局共享资源的访问,因此在多线程环境下需要特别注意同步问题。下面是一些常见的实现方式: 1. **懒汉式,线程不安全**:这种方式简单,但是不支持多线程,容易引发竞态条件。 ```java ...
单例模式是软件设计模式中的一种经典模式,它在Java编程中被广泛使用。这个模式的主要目的是确保一个类只有一个实例,并提供一个全局访问点。这样做的好处包括资源管理(如数据库连接)、性能优化(如缓存服务)以及...
### 单例模式详解 #### 一、单例模式简介 单例模式(Singleton Pattern)是一种常用的软件设计模式,属于创建型模式之一。其目的是确保某个类只有一个实例,并提供一个全局访问点。单例模式的核心在于确保在系统...
细心整合和单例模式和工厂模式的几种模型,懒汉式,饿汉式,如何并发操作模式,等都有详细讲解
单例模式具有一定的“防并发作用”,由于单例模式只生成一次实例化对象,可以减少系统内存的开销,特别是对于多线程单例,即可以在系统启动时完成实例化,避免对资源的重复占用。 单例模式可以作为程序中的“全局锁...
本资源"Qt单例模式MySQL连接池.rar"提供了一个使用Qt框架并结合C++单例模式实现的MySQL数据库连接池模板,旨在优化Qt数据库开发的效率。 首先,我们来理解一下“单例模式”。单例模式是一种设计模式,它确保一个类...
单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。一般用于全局接口(比如...同时跟多线程有关,并发中怎么处理多线程去操作这个单利进行实例问题
单例模式可以确保只有一个角色创建的实例存在,这样所有对角色创建的请求都会导向同一个实例,避免了重复创建和潜在的并发问题。 在Java中,实现单例模式有多种方法,比如懒汉式、饿汉式和双重检查锁定等。懒汉式是...