java内存模型学习

产生线程安全的最终原因是因为:在编译器中生成的指令顺序,可以与在源代码中的顺序不同。  该现象称为重排序

编译器可以把变量存放在寄存器中而不是内存中,处理器可以采用乱序或者并行的顺序来处理程序,缓存可能会改变将写入变量提交到主内存的次序(等待补充)

java语言规规范要求jvm维护线程中类似串行的语义:只要程序运行的最终结果与严格执串行环境中执行的结果相同,那么上诉操作都是允许的。

java内存模型(JMM)主要目的是定义了程序中各个变量的访问规则,其中不包括局部变量和方法参数,这些是线程私有的

JMM规定了所有变量储存在主内存里。每个线程都有自己的工作线程,工作线程中保存了要使用到的变量的拷贝(来自住内存的拷贝),线程对变量的操作必须在工作内存中执行,线程变量的传递由住内存来执行(http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html)




为了避免重排序的Happens-Before规则

1:程序顺序规则。如果程序中操作A在操作B之前,那么在线程中操作A也在操作B之前

2:监视器规则:锁的解锁操作必须在同一个锁的加锁操作之前

3:volatile变量规则:对volatile修饰的变量,其写入操作必须在读入操作之前

4:线程启动规则:thread.start的调用必须在线程的任何操作之前

5:线程关闭规则:线程的操作必须在其他线程检测到该线程已经结束前执行完毕

6:终端规则,当一个线程在另一个线程上调用interrupt时,interrupt必须在被中断线程被检测之前执行

7:终极器规则(不知道什么鬼)

8:传递性,如果操作A在操作B之前,操作B在操作C之前,那么操作A也必须在操作C之前


造成线程不安全的情况有三,第一数据竞争,第二,竞态条件,第三可见性

例如: ++count,此中包含的操作是读取,修改,赋值。两个线程同时操作这个步骤时,对count的同时读取会发生数据竞争,而一整个操作会发生竞态条件,线程之间会争取读取这个数据或者执行这个程序,此时就是所有的重排序的出现

可见性例子如下


主线程中启动了一条子线程,然后对number和ready赋值, 但是在赋值和子线程之间到底谁先执行,这并没有明确规定(重排序),所以number可能是0也可能是42,ready可能是false也可能是true; 此外,由于可见性的原因,当不使用同步操作时,线程读取到的数据可能是过期数据(注解1),此外,同样由于可见性的原因,还会产生局部创建对象的问题,以及看到一个只被部分构造的对象的问题。例如

当一个线程创建houlder,另一个线程引用对象houlder时,会由于重排序而发生引用没有指向对象,引用已经指向对象但赋值未完成等问题,还有最明显的是当两个线程同时读取一个对象时,很显然会发生两个线程得到的对象的状态不一致(类似数据竞争)

线程不安全的原因就是多个线程同时操作同一个共享,可变数据时的重排序引起的。那么避免线程不安全就有三种方法,1:避免共享,2:避免可变,3:避免重排序

1:方法有:

ad-hoc线程限制(这是完全靠实现者实现的线程封闭。),

栈限制:将变量和引用储存在方法区内,方法区内的变量为本地变量,他们本身就被限制在线程中,不被共享。但是栈限制一定要注意的是变量不能逸出,一旦逸出(注释2)就变成了共享变量

threadLocal:该对象提供了get,set访问器,为每个使用它的线程提供了一份单独的拷贝,所以get总是返回当前线程set的最新值,这是目前最规范的方式

2:

创建后不可修改的对象叫做不可变对象,不可变对象永远是线程安全的,满足如下条件的对象是不可变对象:1,它的状态不能在创建后被修改,2,所有的域都是final修饰3,被正确构造(无this逸出)

2.1,对象不可变

final域:final域是不能修改的,尽管他指向的对象是允许被修改的。同时final域使得初始化安全性(注释3)成为可能

在该程序中,final保证了引用不变,arrays.copyof保证了lastfactors引用对应的数组对象不变,故onevaluecachhe对象是不可变的。

volatile,数据的写入操作必须在读取操作之前,


onevaluecache作为不可变容器,收纳所有可变对象,在该程序中被volatile修饰,保证了其可见性。 该例子说明了在不使用锁操作也实现了同步。

2.2,对象可变,则该对象必须被安全的发布。即对象的引用和状态必须对其他线程可见,有四种手段,1:通过静态初始化器初始化对象的引用,2:使用volatile修饰引用或者将引用存入atomicReference,3将引用存储到被正确创建的final域中(不是很懂)4:将他的引用储存到由锁保护的域中。其核心是发布对象的操作必须在使用该对线的线程开始使用该对象之前执行。

此中特别说明静态初始化器,最典型的例子,延迟加载,如下

static修饰的语句的执行时间是在类的初始化阶段,即类被加载之后线程调用之前。其中synchronized修饰保证了方法的可见性(避免了竞抬条件和不安全发布),故在静态初始化阶段,该操作自动对所有线程可见.为了避免每次getinstance花费开销,将实例化提前静态初始

但我们要做的是延迟加载,故必须在方法内获得实例,我们可以结合前两种方法得到如下

优异的延迟加载必须解决两个问题,一个是竞态条件和不安全发布,一个是同步开销,而双重检查加锁是一个简单易懂的延迟加载方案(虽然这个方案是错的)


首先,他显然是一个延迟加载,其次在常见代码路径上他没有使用synchronized,避免了同步开销,当resource为空的时候,他使用synchronized避免了竞态条件。然而resource是在静态域中的一个公共对象(对于所有线程的getinstance是共享的),这时候会由于不安全发布而引起对象仅被构造了一部分,就有可能发生如下情况。一个线程在同步块里面执行 resource=new resource(),此时resource可能引用为空,但有状态,有引用但无状态,完整构造对象,另一个线程刚好执行到了resource是否等于空的判断。DCL无法处理当有引用但对象的状态为空的情况。 这说明DCL在大多数情况下是能完美运行的,但存在的潜在威胁


3,所有的重排序问题都是通过锁来解决,关于锁唯一可提的是可重入性,某一个线程拥有了某个锁,他可以进入任何一个由该锁保护的代码块而不被阻塞。该实现方法为:为每一个锁关联一个计数器和线程所有者,当线程进入一个由锁保护的代码块时,计数器就会加1,当计数器为0时,意味着没有线程持有该锁

注解1:当线程没有同步时可能会读取一个过期值,但这个过期值起码是由之前某个线程设置的,,这种安全性保证也称为最低安全性,然而对非volatile修饰的long和double却不是这样子的,JMM要求对变量的读取都应该是原子操作,但对非volatile修饰的long和double,JVM允许将64位的读操作或写操作分成两个32位的操作,这导致当该变量的操作在两个线程中执行时,由于重排序,就和可能读到某个值的高32位和另一个值的低32位

注释2:发布一个对象指使当前对象能够在当前范围之外被使用。逸出是指当一个对象还没有被构造完成时被发布。

如上例,一个私有的对象可以通过getStates来访问,这个对象就已经被发布了。当发布一个对象的时,这意味着破坏了程序的封装性(同时意味着使程序的正确性分析变得复杂,因为程序的关系变得更复杂了)

注释3:初始化安全性的语义意味着可以防止对象的初始化引用被重排序到构造过程之前

初始化安全性意味着被正确构造的不可变对象在没有同步的情况下能够安全的被多个线程享用(例如不可变的string对象在程序中是安全的)

展开阅读全文

没有更多推荐了,返回首页