0 0

单例模式并发的问题!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个答案 按时间排序 按投票排序

0 0

采纳的答案

这道题答案不是很重要,思路比较重要,上面有的人说的答案也对,但是不精确(有时候不精确不会产生大的问题,有时候就会,下面会解释

这个观点),所以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
0 0

  有人认为valatile能够保持原子操作,其实是很片面的(JVM只是保证对valatile的变量不做任何优化,也就是保证不保留线程对其的副本,也许将来JVM会将其实现为真正的原子操作,但是这得不到保证,而且,如果你依赖于某版本的JVM,你的代码将不具有可有移植性)。
所以安全的做法是(摘自THINGKING IN JAVA):
(1)将类中所有方法用synchronized同步(虽然你的目的是为某个方法同步),如果忽略了其中一个,你很难保证没有负面效应。
(2)去除同步控制时,你得非常小心。这主要基于性能上的考虑(但是JVM已经优化了同步的代码),所以只有当你用性能工具测试其真正为性能瓶颈时,才能这么做。

2010年1月29日 11:58
0 0

别客气,我也是晚上刚好有时间,呵呵。
1.java不是寄存器读写是乱序的,是指java字节码(刚才javap -c生成的那个)的执行是乱序的(具体咋实现的,我也很好奇),不过我明白你意思了。
2和3说的的基本正确。

2010年1月28日 20:59
0 0

在一本并发书上看见的,说这个单例会造成有一些问题,具体原因是因为java对读写共享对象的域时并不保证可线性化性,甚至不保证顺序一致性。原因在于,如果严格遵循顺序一致性,那么将会导致已被广泛采用的编译优化技术变得无效.

是这样:
虽然instance = new Singleton();只有一条语句,但可能分成CPU指令就会有好几个.JVM为了性能,这些指令可能并行执行.

看这个语句,构建对像,然后 置instance为非空,即指向这个对像所在地址.但是如果先置instance为非空,但还没有完成对像构建,而发了线程切换.线程2就会因为if(instance == null)这个条件不满足,而返回instance引用,但对像是不完整的,导至异常发生.

2010年1月28日 14:38
0 0

是的,因为它说 共享对象 的访问不是线性的,所以我做了这个假设..
在本机试了下,始终没出现第二个 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
0 0

我的理解:

   在访问这个单例对象的时候,这个单例方法是public static级别的,即共享对象,而外部(多线程)在首次访问这个方法体的时候,用synchronized 锁住这个对象,让多个线程排队(线性化)等待读写,理论上,我们要求第一个到达的线程会初始这个对象, 然而public static级别 的方法体(读写共享对象)并没有如他所说的(线性化),而是(java对读写共享对象的域时并不保证可线性化性,甚至不保证顺序一致性),所以有可能在还没在第一个线程初始化的时候,第二个线程就已经开始要访问了。。。所以有可能出错。。

   (如果严格遵循顺序一致性,那么将会导致已被广泛采用的编译优化技术变得无效。)这句话,应该是共享对象在锁的级别上是拥有共享锁,让速度更优policy,而没有考虑synchronized 这种独享锁的概念,

   以上我只是个人理解,仅供参考。。。

2010年1月28日 12:46
0 0

单列模式最明显的标志就是 构造方法 私有化。
你贴的代码说实话我也没怎么看懂。

2010年1月28日 11:59

相关推荐

    多线程单例模式并发访问

    深入浅出:讲解单例模式,多线程安全和并发访问问题.让你轻松应对面试

    多线程并发下的单例模式-源码

    该资源是多线程并发下的单例模式-源码,几乎包含了所有方式实现的单例模式,并且能够确保在多线程并发下的线程安全性。 读者可结合本人博客 http://blog.csdn.net/cselmu9?viewmode=list 中的《线程并发之单例模式...

    设计模式之单例模式和工厂模式

    细心整合和单例模式和工厂模式的几种模型,懒汉式,饿汉式,如何并发操作模式,等都有详细讲解

    高并发下多种单例模式实现源码

    单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。一般用于全局接口(比如...同时跟多线程有关,并发中怎么处理多线程去操作这个单利进行实例问题

    单例模式详解.txt

    单例模式的实现机制,并发情况下的单例模式的存在问题及解决方法,无锁的线程安全单例模式

    单例模式简单实现

    实现单例模式:1、私有化的构造函数。2、提供静态方法或者公共属性提供全局访问点。3、考虑多线程高并发的问题。 注意点:lock之前判断一下实例是否为空。lock之前判断一下什么情况可以lock什么情况不可以,一般多...

    最全的单例模式j(java实现),下到就是赚到!

    最近在学习多线程相关知识,同时加深了对单例的理解,从并发的角度学习到了不同的单列模式,提供出来供大家一起学习

    击穿单例模式的Demo示范代码

    对于设计模式,最熟知和最常用的不外乎单例模式和工厂模式,对于单例模式如果编写不严谨的话也存在安全漏洞问题,这个击穿单例模式的代码很形象的说明了这个问题,其中包含如何使用现成并发技术,欢迎大家下载学习。

    JAVA多线程并发下的单例模式应用

    单例模式应该是设计模式中比较简单的一个,也是非常常见的,但是在多线程并发的环境下使用却是不那么简单了,今天给大家分享一个我在开发过程中遇到的单例模式的应用。 首先我们先来看一下单例模式的定义: 一个类有...

    23种设计模式之单例模式

    单例模式 饿汉式: /** * 饿汉式单例(提前把对象创建) * 可能会浪费空间,提前把对象创建好了,但是不一定会用。 */ public class Hungry { private Hungry(){ } private final static Hungry HUNGRY=new ...

    你想要Android数据库操作精华(安全、并发、单例等)

    自己写的android数据库操作Demo,使用的是Android Studio开发,包括数据库框架、数据库的加密、并发,单例模式操作数据库,绝对是你想要的Demo。

    【资源免费下载】Java代码积累丨大话设计模式(Java实现版本)、线程协作

    单例模式 结构型模式 队列模式 桥接模式 组合模式 装饰模式 外观模式 享元模式 代理模式 行为模式(类行为模式) 解释器模式 模板方法模式 行为模式(对象行为模式) 策略模式 观察者模式 状态模式 导入模式 迭代器...

    [并发并行]_[线程同步]_[pthread_once 实现单例模式分析]

    http://blog.csdn.net/infoworld/article/details/49798215 mingw 编译,Win32部分可以直接用vs编译.

    SatanDaddy#myblog#单例设计模式1

    * 单例设计模式-饿汉式// 构造器* 返回实例对象* 饿汉式避免了并发安全问题,但是却无法实现lazyLoad饿汉式面临的问题:对象无法实现lazy-load

    覆盖一系列高级主题,包括复杂的语法和特性、Python的高级编程技巧、常见的设计模式、并发编程、性能优化等

    设计模式: 探讨常见的设计模式,如工厂模式、单例模式、观察者模式等,以及如何在Python中应用这些模式。 测试和调试: 介绍高级的测试技术和调试工具,以确保代码的质量和可维护性。 性能优化: 提供关于Python...

    毕业设计订餐系统源码-design_pattern:设计模式

    单例模式是并发协作软件模块中需要最先完成的,因而其不利于测试; 单例模式在某种情况下会导致“资源瓶颈”。 单例模式的应用举例: 生成全局惟一的序列号; 访问全局复用的惟一资源,如磁盘、总线

    高性能高并发服务器架构

    1.不要频繁的new对象,对于在整个应用中只需要存在一个实例的类使用单例模式.对于String的连接操作,使用StringBuffer或者StringBuilder.对于utility类型的类通过静态方法来访问。 2. 避免使用错误的方式,如Exception...

    java经典面试题目-面经-java-Java语言的进阶概念-常用的库和框架-并发编程-网络编程-Web开发-面经

    如何实现线程安全的单例模式? 什么是Java中的生命周期回调方法?列举一些常见的生命周期回调方法。 什么是Java中的注解处理器?如何自定义和使用注解处理器? 什么是Java中的并发编程?列举一些常见的并发类和...

    基于Linux的web服务器

    项目是在Linux下以C++开发语言搭建的Web服务器,服务器可以支持相对数量的客户端并发和及时响应,该项目支持用户注册、登录,访问图片和视频。...使用单例模式和RAII机制实现数据库连接池,减少数据库连接的开销

Global site tag (gtag.js) - Google Analytics