- 浏览: 105867 次
- 性别:
- 来自: 北京
文章分类
- 全部博客 (75)
- JVM (22)
- 数据结构 (11)
- java 基础 (16)
- gc (6)
- jmock (1)
- Google (2)
- MapReduce (1)
- Memory (2)
- 算法 (2)
- cglib (1)
- jdk (3)
- 虚拟机 (3)
- 安全 (2)
- 多线程 (1)
- 工作 (1)
- 生活 (1)
- MongoDB (2)
- Hadoop (4)
- HDFS (2)
- cms (2)
- Spring (1)
- 网络协议 (1)
- GitHub (1)
- MYSQL 调优和使用必读(转) (1)
- 分布式 (2)
- Big Data (0)
- 技术Blog (1)
- Hbase (2)
- Zookeeper (1)
- paper (0)
最新评论
-
lzc_java:
Java线程安全兼谈DCL -
select*from爱:
it's nice
IT业薪水大揭秘
转载自 ---- http://www.iteye.com/topic/875420
如果你搜索网上分析dcl为什么在java中失效的原因,都会谈到编译器会做优化云云,我相信大家看到这个一定会觉得很沮丧、很无助,对自己写的
程序很没信心。我很理解这种感受,因为我也经历过,这或许是为什么网上一直有人喜欢谈dcl的原因。如果放在java5之前,从编译器的角度去解释dcl
也无可厚非,在java5的JMM(内存模型)已经得到很大的修正,如果到现在还只能从编译器的角度去解释dcl,那简直就在污辱java,要知道
java的最大优势就是只需要考虑一个平台。你可以完全无视网上绝大多数关于dcl的讨论,很多时候他们自己都说不清楚,除Doug
Lea等几个大牛,我不相信谁比谁更权威。
很多人不理解dcl,不是dcl有多么复杂,恰恰相反,而是对基础掌握得不够。所以,我会先从基础讲起,然后再分析DCL。
我们都知道,当两个线程同时读写(或同时写)一个共享变量时会发生数据竞争。那我怎么才能知道发生了数据竞争呢?我需要去读取那个变量,发生数据
竞争通常有两个表现:一是读取到陈旧数据,即读取到虽是曾经写入的数据,但不是最新的。二是读取到之前根本没有写入的值,也就是说读到垃圾。
数据陈旧性
为了读取到另一个线程写入的最新数据,JMM定义了一系列的规则,最基本的规则就是要利用同步。在Java中,同步的手段有synchronized和volatile两种,这里我只会涉及到syncrhonized。请大家先记住以下规则,接下来我会细讲。
规则一:必须对变量的所有写和所有读同步,才能读取到该最新的数据。
先看下面的代码:
- public class A {
- private int some;
- public int another;
- public int getSome() { return some; }
- public synchronized int getSomeWithSync() { return some; }
- public void setSome( int v) { some = v; }
- public synchronized void setSomeWithSync( int v) { some = v; }
- }
让我们来分析一个线程写,另一个线程读的情形,一共四种情形。初始情况都是a = new A(),暂不考虑其它线程。
情形一:读写都不同步。
Thread1 | Thread2 |
(1) a.setSome(13) | |
(2) a.getSome() |
这种情况下,即使thread1先写入some为13,thread2再读取some,它能读到13吗?在没有同步协调下,结果是不确定的。从图
上看出,两个线程独立运行,JMM并不保证一个线程能够看到另一个线程写入的值。在这个例子中,就是thread2可能读到0(即some的初始值)而不
是13。注意,在理论上,即使thread2在thread1写入some之后再等上一万年也还是可能读到some的初始值0,尽管这在实际几乎不可能发
生。
情形二:写同步,读不同步
Thread1 | Thread2 |
(1) a.setSomeWithSync(13) | |
(2) a.getSome() |
情形三:读同步,写不同步
Thread1 | Thread2 |
(1) a.setSome(13) | |
(2) a.getSomeWithSync() |
在这两种情况下,thread1和thread2只对读或只对写some加了锁,这不起任何作用,和[情形一]一样,thread2仍有可能读到
some的初始值0。从图上也可看出,thread1和thread2互相之间并没有任何影响,一方加锁并不影响另一方的继续运行。图中也显示,同步操作
相当于在同步开始执行lock操作,在同步结束时执行unlock操作。
情形四:读写都同步
Thread1 | Thread2 |
(1) a.setSomeWithSync(13) | |
(2) a.getSomeWithSync() |
在情形四中,thread1写入some时,thread2等待thread1写入完成,并且它能看到thread1对some做的修改,这时
thread2保证能读到13。实际上,thread2不仅能看到thread1对some的修改,而且还能看到thread1在修改some之前所做的
任何修改。说得更精确一些,就是一个线程的lock操作能看见另一线程对同一个对象unlock操作之前的所有修改,请注意图中的红色箭头。
沿着图中箭头指示方向,箭头结尾处总能看到箭头开始处操作做的修改。这样,a.some[thread2]能看见
lock[thread2],lock[thread2]能看见unlock[thread1],unlock[thread1]又能看见
a.some=13[thread1],即能看到some的值为13。
再来看一个稍微复杂一点的例子:
例子五
Thread1 | Thread2 |
(1) a.another = 5 | |
(2) a.setSomeWithSync(13) | |
(3) a.getSomeWithSync() | |
(4) a.another = 7 | |
(5) a.another |
thread2最后会读到another的什么值呢?会不会读到another的初始值0呢,毕竟所有对another的访问都没有同步?不会。
从图中很清晰地可以看出,thread2的another至少到看到thread1在lock之前写入的5,却并不能保证它能看到thread1在
unlock写入的7。因此,thread2可以什么读到another的值可能5或7,但不会是0。你或许已经发现,如果去掉图中thread2读取
a.some的操作,这时相当于一个空的同步块,对结论并没有任何影响。这说明空的同步块是起作用的,编译器不能擅自将空的同步块优化掉,但你在使用空的
同步块应该特别小心,通常它都不是你想要的结果。另外需要注意,unlock操作和lock操作必须针对同一个对象,才能保证unlock操作能看到
lock操作之前所做的修改。
例子六:不同的锁
- class B {
- private Object lock1 = new Object();
- private Object lock2 = new Object();
- private int some;
- public int getSome() {
- synchronized (lock1) { return some; }
- }
- public void setSome( int v) {
- synchronized (lock2) { some = v; }
- }
- }
Thread1 | Thread2 |
(1) b.setSome(13) | |
(2) b.getSome() |
在这种情况下,虽然getSome和setSome都加了锁,但由于它们是不同的锁,一个线程运行时并不能阻塞另一个线程运行。因此这里的情形和情形一、二、三一样,thread2不保证读到thread1写入的some最新值。
现在来看DCL:
例子七: DCL
- public class LazySingleton {
- private int someField;
- private static LazySingleton instance;
- private LazySingleton() {
- this .someField = 201 ; // (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)
- }
- }
假设thread1先调用getInstance(),由于此时还没有任何线程创建LazySingleton实例,它会创建一个实例s并返回。
这是thread2再调用getInstance(),当它运行到(2)处,由于这时读instance没有同步,它有可能读到s或者null(参考情形
二)。先考虑它读到s的情形,画出流程图就是下面这样的:
由于thread2已经读到s,所以getInstance()会立即返回s,这是没有任何问题,但当它读取s.someFiled时问题就发生
了。
从图中可以看thread2没有任何同步,所以它可能看不到thread1写入someField的值20,对thread2来说,它可能读到
s.someField为0,这就是DCL的根本问题。从上面的分析也可以看出,为什么试图修正DCL但又希望完全避免同步的方法几乎总是行不通的。
接下来考虑thread2在(2)处读到instance为null的情形,画出流程图:
接下来thread2会在有锁的情况下读取instance的值,这时它保证能读到s,理由参考情形四或者通过图中箭头指示方向来判定。
关于DCL就说这么多,留下两个问题:
- 接着考虑thread2在(2)读到instance为null的情形,它接着调用s.someFiled会得到什么?会得到0吗?
- DCL为什么要double check,能不能去掉(4)处的check?若不能,为什么?
原子性
回到情形一,为什么我们说thread2读到some的值只可能为为0或13,而不可能为其它?这是由java对int、引用读写都是原子性所决
定的。所谓“原子性”,就是不可分割的最小单元,有数据库事务概念的同学们应该对此容易理解。当调用some=13时,要么就写入成功要么就写入失败,不
可能写入一半。但是,java对double, long的读写却不是原子操作,这意味着可能发生某些极端意外的情况。看例子:
- public class C {
- private /* volatile */ long x; // (1)
- public void setX( long v) { x = v; }
- public long getX() { return x; }
- }
Thread1 | Thread2 |
(1) c.setX(0x1122334400112233L) | |
(2) c.getX() |
thread2读取x的值可能为0,1122334400112233外,还可能为别的完全意想不到的值。一种情况假设jvm对long的写入是
先写低4字节,再写高4字节,那么读取到x的值还可能为112233。但是我们不对jvm做如此假设,为了保证对long或double的读写是原子操
作,有两种方式,一是使用volatile,二是使用synchronized。对上面的例子,如果取消(1)处的volatile注释,将能保证
thread2读取到x的值要么为0,要么为1122334400112233。如果使用同步,则必须像下面这样对getX,setX都同步:
- public class C {
- private /* volatile */ long x; // (1)
- public synchronized void setX( long v) { x = v; }
- public synchronized long getX() { return x; }
- }
因此对原子性也有规则(volatile其实也是一种同步)。
规则二:对double, long变量,只有对所有读写都同步,才能保证它的原子性
有时候我们需要保证一个复合操作的原子性,这时就只能使用synchronized。
- public class Canvas {
- private int curX, curY;
- public /* synchronized */ getPos() {
- return new int [] { curX, curY };
- }
- public /* synchronized */ void moveTo( int x, int y) {
- curX = x;
- curY = y;
- }
- }
Thread1 | Thread2 |
(1) c.moveTo(1, 1) | |
(2) c.moveTo(2, 2) | |
(3) c.getPos() |
当没有同步的情况下,thread2的getPos可能会得到[1, 2],
尽管该点可能从来没有出现过。之所以会出现这样的结果,是因为thread2在调用getPos()时,curX有0,1或2三种可能,同样curY也有
0,1或2三种可能,所以getPos()可能返回[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0],
[2,1], [2,2]共九种可能。要避免这种情况,只有将getPos()和moveTo都设为同步方法。
总结
以上分析了数据竞争的两种症状,陈旧数据和非原子操作,都是由于没有恰当同步引起的。这些其实都是相当基础的知识,同步可有两种效果:一是保证读
取最新数据,二是保证操作原子性,但是大多数书籍都对后者过份强调,对前者认识不足,以致对多线程的认识上存在很多误区。如果想要掌握java线程高级知
识,我只推荐《Java并发编程设计原则与模式》。其实我已经好久没有写Java了,这些东西都是我两年前的知识,如果存在问题,欢迎大家指出,千万不要
客气。
发表评论
-
Java基础 之软引用、弱引用、虚引用 ·[转载]
2012-06-07 18:13 11061、概述 在JDK1.2以前的版本中,当一个对象不 ... -
Java中常用的加密方法(JDK)
2012-03-30 16:35 10881转载自 ---- http://www.iteye.co ... -
java的内存管理
2012-03-29 16:59 1560转载自 ---- http://yangzhiyong77 ... -
Java栈与堆
2011-10-10 16:39 828转载自 ---- http://mylir.i ... -
Java内存泄露的理解与解决
2011-10-10 16:38 933转载自 ---- http://henryyang.itey ... -
JVM问题诊断常用命令:jinfo,jmap,jstack
2011-08-18 11:19 1519转载自 ---- http://singleant.iteye ... -
Java HotSpot 性能引擎架构
2011-08-17 17:04 993转载自 ---- http://lifethink ... -
用happen-before规则重新审视DCL
2011-08-17 17:00 788转载自 ---- http://lifethink ... -
CMS gc实践总结(转载)
2011-08-10 15:09 1036首先感谢阿宝 同学的帮助,我才对这个gc算法的调整有 ... -
GC机制小结
2011-08-10 14:07 685转载自 ---- http://zhangjian ... -
排序算法java版(转载)
2011-08-10 14:06 853转载自 ---- http://yiyickf.iteye.c ... -
Java内存模型(JMM) 资料整理(转载)
2011-08-10 13:35 946转载自 ---- http://blog.csdn.net/o ... -
ClassLoader解析(转载)
2011-08-05 14:35 925转载自 ---- http://shangjava ... -
深入理解java的finalize
2011-08-03 17:01 693转载自 ---- http://zhang-xzhi-x ... -
深入理解java的clone
2011-08-03 17:01 730转载自 ---- http://zhang-xzh ...
相关推荐
单例模式如果使用不当,就容易引起线程安全问题。 * 饿汉式不存在线程安全问题,但是它一般不被使用,因为它会浪费内存的空间 * 懒汉式会合理使用内存空间,只有第一次被加载的时候,才会真正去创建对象。但是这种...
描述一下Java线程的生命周期,线程状态; 线程之间的通信方式; 描述一下notify和notifyAll区别; synchronized关键字加在静态方法和实例方法的区别; 用锁的注意点; cas机制可能导致的问题ABA,什么是ABA; 程序开...
熟练掌握java基础,包括面向对象、多线程、IO流、UDP/TCP、socket、文件上传下载、XML解析、JSON解析、servlet、filter、listener、HTTP协议等等; 熟练掌握 使用jdbc标准连接数据库,掌握DDL、DML、DCL、DQL和...
深入分析java单例模式什么是单例模式单例模式的常见写法一、饿汉式单例优点缺点示例二、懒汉式单例示例1(普通写法)示例2(synchronized写法)示例3(DCL写法)示例4(内部类写法)三、注册式单例示例1(容器式)示例2(枚举式...
DML,DCL,DDL 事物 索引 非关系型数据库 阶段二:JavaWEB XML格式 WEB服务器Tomcat 小服务程序 Cookie和会议 筛选器和监听器 国际化 文件上传下载 阶段三:JavaEE主流框架 春天 行动计划 国际奥委会 事物 ...
懒汉式优化-加锁同步3.DCL双检锁/双重校验锁重排序问题多线程执行时序表volatile 作用优化-基于volatile 的双重检查锁4.IODH按需初始化持有者反射问题私有构造函数异常处理5.枚举实现单例使用推荐 什么是单例? 单例...
整体结构和一些主要职责(如数据库操作 事务跟踪 安全等),剩余的就是变化的东西,针对这个领域中具体应用产生的具体不同 的变化需求,而这些变化东西就是 J2EE 程序员所要做的。 由此可见,设计模式和 J2EE 在思想...
SQL语言的分类情况大致如下: 1) 数据定义语言(DDL):Create,Drop,Grant,Revoke,… 2) 数据操纵语言(DML):Update,Insert,Delete,… 3) 数据控制语言(DCL):Commit,Rollback,Savapoint,… 4) 其他:Alter ...
2.标准的SQL语言语句类型可以分为:数据定义语句(DDL)、数据操纵语句(DML)和数据控制语句(DCL)。 3.在需要滤除查询结果中重复的行时,必须使用关键字Distinct; 在需要返回查询结果中的所有行时,可以使用关键字ALL...