`

struts 引来的线程及单例问题

    博客分类:
  • J2SE
阅读更多
struts2线程问题 文章中
引出了线程的问题。。

就得回头再去看看线程部分。
在看公司框架的时候就看到了线程的内容。。Executor类的使用。。那时已经学习了下JDK新版本中的线程类。

在本文中将解决已下问题:
都说单例有性能问题,那性能问题到底体现在哪?
都讲单例有线程安全问题,那又体现在哪。。。上篇文章已经回答了这个问题。
单例模式中的几种写法的区别到底在哪?
双重检查锁为什么在java中无法实现?
如何去修复这个双重检查?

----------------------------------------------------------
http://www.jdon.com/jivejdon/thread/17133.html
这里面讲得比较多

public class Singleton(){
private Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
   if(instatnce==null){
       instance = new Singleton();}
  return instance;
}
}

这是最常见的写法了。一般人都这么写。单从这简单看一下,是没有问题的。
那在多线程下:
为了注意多线程的同步问题,如果有两个线程同时调用getInstance()方法,就有可能造成构造子被调用两次。在getInstatnce方法前加上同步关键词
public static synchronized Singleton getInstatce()

知道多线程的人都知道了,加了一个关键词,但在效率上是大打折扣了。
这应该就是人们常说的性能问题了。当有一个实例进入了此方法,别的来访问者就得等待。

另外一种编写的方法:
public class MySingleton {
    private static  MySingleton _instance =
        new MySingleton();
    private MySingleton() {
        // construct object . . .
    }
    public static MySingleton getInstance() {
        return _instance;
    }

在使用这种方式egar initialization的时候,性能问题就不存在。

第一种的lazy initialization会出现一些问题。所以得小心使用。
在effective java中也指出了。在不得已的情况下才去使用lazy initialization。


--------------------------------------------------
对于lazy initialization。。。重构下。
public class MySingleton {
    private static  MySingleton _instance ;
    private MySingleton() {
        // construct object . . .
    }
    public static MySingleton getInstance() {
         if (instance == null) {                               // (2)   
            synchronized(MySingleton.class) {               // (3)   
                if (instance == null) {                       // (4)   
                    instance = new MySingleton();           // (5)   
                }   
            }   
        }   
}
        return instance;
    }

这就是高人们常写的又重检查啦。
但通过专家的认证,这是有问题的。。高人们很是郁闷。。下面来看看原因。

这就是JMM的问题了。
JMM哪儿有问题呢。
首先得知道JMM是什么:
http://www.iteye.com/topic/108927
引用
内存模型描述的是程序中各变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的低层细节


在C或C++中, 可以利用不同操作平台下的内存模型来编写并发程序. 但是, 这带给开发人员的是, 更高的学习成本.
相比之下, java利用了自身虚拟机的优势, 使内存模型不束缚于具体的处理器架构, 真正实现了跨平台.
(针对hotspot jvm, jrockit等不同的jvm, 内存模型也会不相同)

Java被设计为跨平台的语言,在内存管理上,显然也要有一个统一的模型。而且Java语言最大的特点就是废除了指针,把程序员从痛苦中解脱出来,不用再考虑内存使用和管理方面的问题。
可惜世事总不尽如人意,虽然JMM设计上方便了程序员,但是它增加了虚拟机的复杂程度,而且还导致某些编程技巧在Java语言中失效。

JMM 主要是为了规定了线程和内存之间的一些关系。对Java程序员来说只需负责用synchronized同步关键字,其它诸如与线程/内存之间进行数据交换 /同步等繁琐工作均由虚拟机负责完成。如图1所示:根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。



引用
根据Java Language Specification中的说明, jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。

每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。


引用
内存模型的特征:
a, Visibility 可视性 (多核,多线程间数据的共享)
b, Ordering 有序性 (对内存进行的操作应该是有序的)


jmm怎么体现 可视性(Visibility) ?
在jmm中, 通过并发线程修改变量值, 必须将线程变量同步回主存后, 其他线程才能访问到.

jmm怎么体现 有序性(Ordering) ?
通过java提供的同步机制或volatile关键字, 来保证内存的访问顺序.

-----------------------------------------------------------------------
引用

缓存一致性(cache coherency)

什么是缓存一致性?
它是一种管理多处理器系统的高速缓存区结构,其可以保证数据在高速缓存区到内存的传输中不会丢失或重复。(来自wikipedia)

举例理解:
假如有一个处理器有一个更新了的变量值位于其缓存中,但还没有被写入主内存,这样别的处理器就可能会看不到这个更新的值.

解决缓存一致性的方法?
a, 顺序一致性模型:
要求某处理器对所改变的变量值立即进行传播, 并确保该值被所有处理器接受后, 才能继续执行其他指令.

b, 释放一致性模型: (类似jmm cache coherency)
允许处理器将改变的变量值延迟到释放锁时才进行传播.



从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保护的代码段,很有可能某条线程已经改动了变量的值,但是其他线程却无法看到这个改动,依然在旧的变量值上进行运算,最终导致不可预料的运算结果。

  JMM不保证执行顺序也是从效率上来考虑:
1, 编译器优化了程序指令, 以加快cpu处理速度.
2, 多核cpu动态调整指令顺序, 以加快并行运算能力.
引用
只要是在单个线程情况下执行结果是正确的,就可以认为编译器这样的“自作主张的调整代码次序”的行为是合法的。JLS在某些方面的规定比较自由,就是为了让JVM有更多余地进行代码优化以提高执行效率。而现在的CPU大多使用超流水线技术来加快代码执行速度,针对这样的CPU,编译器采取的代码优化的方法之一就是在调整某些代码的次序,尽可能保证在程序执行的时候不要让CPU的指令流水线断流,从而提高程序的执行速度。正是这样的代码调整会导致DCL的失效。



这样子就可能改变了语句执行的顺序。

对于双重检查:
public class LazySingleton {
    private int someField;
    
    private static LazySingleton instance;
    
    private LazySingleton() {
        this.someField = new Random().nextInt(200)+1;         // (1)
    }
    
    public static LazySingleton getInstance() {
        if (instance == null) {                               // (2)
            synchronized(LazySingleton.class) {               // (3)
                if (instance == null) {                       // (4)
                    instance = new LazySingleton();           // (5)
                }
            }
        }
        return instance;                                      // (6)
    }
    
    public int getSomeField() {
        return this.someField;                                // (7)
    }
}

引用
为了进一步证明这个问题,引用一下《DCL Broken Declaration》文章中的例子:
设一行Java代码:


Objects[i].reference = new Object();


经过Symantec JIT编译器编译过以后,最终会变成如下汇编码在机器中执行:


0206106A  mov     eax,0F97E78h
0206106F  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],400h
0206108D  mov     dword ptr [ecx+0Ch],0F84030h


可见,Object构造函数尚未调用,但是已经能够通过objects[i].reference获得Object对象实例的引用。
如果把代码放到多线程环境下运行,某线程在执行到该行代码的时候JVM或者操作系统进行了一次线程切换,其他线程显然会发现msg对象已经不为空,导致 Lazy load的判断语句if(objects[i].reference == null)不成立。线程认为对象已经建立成功,随之可能会使用对象的成员变量或者调用该对象实例的方法,最终导致不可预测的错误。


其实这个不可预测的错误就是对象没有初始化完成。

你可能很有疑问,怎么就初始化没有完成,就出现了错误,
引用
我需要预先陈述上面程序运行时几个事实:

   1. 语句(5)只会被执行一次,也就是LazySingleton只会存在一个实例,这是由于它和语句(4)被放在同步块中被执行的缘故,如果去掉语句(3)处的同步块,那么这个假设便不成立了。
   2. instance只有两种“曾经可能存在”的值,要么为null,也就是初始值,要么为执行语句(5)时构造的对象引用。这个结论由事实1很容易推出来。
   3. getInstance()总是返回非空值,并且每次调用返回相同的引用。如果getInstance()是初次调用,它会执行语句(5)构造一个LazySingleton实例并返回,如果getInstance()不是初次调用,如果不能在语句(2)处检测到非空值,那么必定将在语句(4)处就能检测到instance的非空值,因为语句(4)处于同步块中,对instance的写入--语句(5)也处于同一个同步块中。

既然根据第3条事实getInstance()总是返回相同的正确的引用,为什么还说DCL有问题呢?这里的关键是 尽管得到了LazySingleton的正确引用,但是却有可能访问到其成员变量 的 不正确值 ,具体来说LazySingleton.getInstance().getSomeField()有可能返回someField的默认值0。如果程序行为正确的话,这应当是不可能发生的事,因为在构造函数里设置的someField的值不可能为0


上面的解释是从JMM的角度,在同步块之内的方法不能保证其顺序!应该还是比较好理解的。。
最简单的方法就是理解,进入同步块后,JVM先执行了new操作,此时另一线程来访问,就直接返回了一个错误的对象。

当然也有人从用happen-before规则重新审视DCL,
http://www.iteye.com/topic/260515

引用
对于DCL是否有效,个人认为更多的是一种带有学究气的推断和讨论。而从纯理论的角度来看,存取任何可能共享的变量(对象引用)都需要同步保护,否则都有可能出错,但是处处用synchronized又会增加死锁的发生几率,苦命的程序员怎么来解决这个矛盾呢?事实上,在很多Java开源项目(比如 Ofbiz/Jive等)的代码中都能找到使用DCL的证据,我在具体的实践中也没有碰到过因DCL而发生的程序异常。个人的偏好是:不妨先大胆使用 DCL,等出现问题再用synchronized逐步排除之。也许有人偏于保守,认为稳定压倒一切,那就不妨先用synchronized同步起来,我想这是一个见仁见智的问题,而且得针对具体的项目具体分析后才能决定。还有一个办法就是写一个测试案例来测试一下系统是否存在DCL现象,附带的光盘中提供了这样一个例子,感兴趣的读者可以自行编译测试。不管结果怎样,这样的讨论有助于我们更好的认识JMM,养成用多线程的思路去分析问题的习惯,提高我们的程序设计能力。


引用
下面是我列出的三条非常重要的happen-before规则,利用它们可以确定两个操作之间是否存在happen-before关系。

   1. 同一个线程中,书写在前面的操作happen-before书写在后面的操作。这条规则是说,在单线程 中操作间happen-before关系完全是由源代码的顺序决定的,这里的前提“在同一个线程中”是很重要的,这条规则也称为单线程规则 。这个规则多少说得有些简单了,考虑到控制结构和循环结构,书写在后面的操作可能happen-before书写在前面的操作,不过我想读者应该明白我的意思。
   2. 对锁的unlock操作happen-before后续的对同一个锁的lock操作。这里的“后续”指的是时间上的先后关系,unlock操作发生在退出同步块之后,lock操作发生在进入同步块之前。这是条最关键性的规则,线程安全性主要依赖于这条规则。但是仅仅是这条规则仍然不起任何作用,它必须和下面这条规则联合起来使用才显得意义重大。这里关键条件是必须对“同一个锁”的lock和unlock。
   3. 如果操作A happen-before操作B,操作B happen-before操作C,那么操作A happen-before操作C。这条规则也称为传递规则。


分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics