关于java的volatile关键字与线程栈的内容以及单例的DCL

  用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新的值。volatile很容易被误用,用来进行原子性操做。java

package com.guangshan.test;

public class TestVolatile {
    
    public static int count = 0;
    
    public static void inc () {
        try {
            Thread.sleep(1);
        } catch (Exception e) {

        }
        count++;
    }
    
    public static void main(String[] args) throws InterruptedException {
        
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                
                public void run() {
                    TestVolatile.inc();
                }
            }).start();
        }
        System.out.println(count);
        
        Thread.sleep(1000);
        
        System.out.println(count);
        
    }
}

  这段代码,最后的count值颇有可能不为1000(main函数所在的线程为主线程,主线程的最后一句代码执行后,会进入Thread.exit()方法,该方法会强制终止全部该线程建立的线程),在sleep(1000)后,其余加的线程已经结束了,按理讲,这里的count应该为1000的,可是为何不是1000呢?程序员

  不少人觉得,这个是多线程并发问题,只须要在变量count以前加上volatile就能够避免这个问题,那咱们在修改代码看看,看看结果是否是符合咱们的指望。数组

  加入volatile以后,仍然有可能不是1000,下面咱们分析一下缘由缓存

  在 java 垃圾回收整理一文中,描述了jvm运行时刻内存的分配。其中有一个内存区域是jvm虚拟机栈,每个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先经过对象的引用找到对应在堆内存的变量的值,而后把堆内存变量的具体值load到线程本地内存中,创建一个变量副本,以后线程就再也不和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完以后的某一个时刻(线程退出以前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。下面一幅图描述这写交互安全

  

  

  read and load 从主存复制变量到当前工做内存
  use and assign  执行代码,改变共享变量值 
  store and write 用工做内存数据刷新主存相关内容多线程

  其中use and assign 能够屡次出现并发

  可是这一些操做并非原子性,也就是 在read load以后,若是主内存count变量发生修改以后,线程工做内存中的值因为已经加载,不会产生对应的变化,因此计算出来的结果会和预期不同jvm

  对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工做内存的值是最新的函数

  例如假如线程1,线程2 在进行read,load 操做中,发现主内存中count的值都是5,那么都会加载这个最新的值优化

  在线程1堆count进行修改以后,会write到主内存中,主内存中的count变量就会变为6

  线程2因为已经进行read,load操做,在进行运算以后,也会更新主内存count的变量值为6

  致使两个线程即便用volatile关键字修改以后,仍是会存在并发的状况。

 

  synchronize关键字修饰的代码块,会自动与主内存同步资源,即在退出sync代码块时,主内存资源自动同步为最新的资源(猜想)

 

  单例的DCL(双重检查加锁) 

public class SingletonKerriganD {   
    
    /**  
     * 单例对象实例  
     */  
    private static SingletonKerriganD instance = null;   
    
    public static SingletonKerriganD getInstance() {   
        if (instance == null) {   
            synchronized (SingletonKerriganD.class) {   
                if (instance == null) {   
                    instance = new SingletonKerriganD();   
                }   
            }   
        }   
        return instance;   
    }   
}  

看起来这样已经达到了咱们的要求,除了第一次建立对象以外,其余的访问在第一个if中就返回了,所以不会走到同步块中。已经完美了吗? 

咱们来看看这个场景:假设线程一执行到instance = new SingletonKerriganD()这句,这里看起来是一句话,但实际上它并非一个原子操做(原子操做的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操做有不少,咱们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大体作了3件事情: 

1.给Kerrigan的实例分配内存。 

2.初始化Kerrigan的构造器 

3.将instance对象指向分配的内存空间(注意到这步instance就非null了)。 

可是,因为Java编译器容许处理器乱序执行(out-of-order),以及JDK1.5以前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是没法保证的,也就是说,执行顺序多是1-2-3也多是1-3-2,若是是后者,而且在3执行完毕、2未执行以前,被切换到线程二上,这时候instance由于已经在线程一内执行过了第三点,instance已是非空了,因此线程二直接拿走instance,而后使用,而后瓜熟蒂落地报错,并且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来,真是一茶几的杯具啊。 

DCL的写法来实现单例是不少技术书、教科书(包括基于JDK1.4之前版本的书籍)上推荐的写法,其实是不彻底正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决因而否能保证二、3步的顺序。在JDK1.5以后,官方已经注意到这种问题,所以调整了JMM、具体化了volatile关键字,所以若是JDK是1.5或以后的版本,只须要将instance的定义改为“private volatile static SingletonKerriganD instance = null;”就能够保证每次取instance都从主内存读取,就可使用DCL的写法来完成单例模式。

2、如下来自http://rainyear.iteye.com/blog/1734311

java线程内存模型

线程、工做内存、主内存三者之间的交互关系图:

 

key edeas

全部线程共享主内存
每一个线程有本身的工做内存
refreshing local memory to/from main memory must  comply to JMM rules

 

产生线程安全的缘由

线程的working memory是cpu的寄存器和高速缓存的抽象描述:如今的计算机,cpu在计算的时候,并不老是从内存读取数据,它的数据读取顺序优先级 是:寄存器-高速缓存-内存。线程耗费的是CPU,线程计算的时候,原始的数据来自内存,在计算过程当中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。当多个线程同时读写某个内存数据时,就会产生多线程并发问题,涉及到三个特 性:原子性,有序性,可见性。 支持多线程的平台都会面临 这种问题,运行在多线程平台上支持多线程的语言应该提供解决该问题的方案。

JVM是一个虚拟的计算机,它也会面临多线程并发问题,java程序运行在java虚拟机平台上,java程序员不可能直接去控制底层线程对寄存器高速缓存内存之间的同步,那么java从语法层面,应该给开发人员提供一种解决方案,这个方案就是诸如 synchronized, volatile,锁机制(如同步块,就绪队 列,阻塞队列)等等。这些方案只是语法层面的,但咱们要从本质上去理解它;

 

每一个线程都有本身的执行空间(即工做内存),线程执行的时候用到某变量,首先要将变量从主内存拷贝的本身的工做内存空间,而后对变量进行操做:读取,修改,赋值等,这些均在工做内存完成,操做完成后再将变量写回主内存;

各个线程都从主内存中获取数据,线程之间数据是不可见的;打个比方:主内存变量A原始值为1,线程1从主内存取出变量A,修改A的值为2,在线程1未将变量A写回主内存的时候,线程2拿到变量A的值仍然为1;

这便引出“可见性”的概念:当一个共享变量在多个线程的工做内存中都有副本时,若是一个线程修改了这个共享变量的副本值,那么其余线程应该可以看到这个被修改后的值,这就是多线程的可见性问题。

普通变量状况:如线程A修改了一个普通变量的值,而后向主内存进行写回,另一条线程B在线程A回写完成了以后再从主内存进行读取操做,新变量的值才会对线程B可见;

 

如何保证线程安全 
编写线程安全的代码,本质上就是管理对状态(state)的访问,并且一般都是共享的、可变的状态。这里的状态就是对象的变量(静态变量和实例变量) 
线程安全的前提是该变量是否被多个线程访问, 保证对象的线程安全性须要使用同步来协调对其可变状态的访问;如果作不到这一点,就会致使脏数据和其余不可预期的后果。不管什么时候,只要有多于一个的线程访问给定的状态变量,并且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。Java中首要的同步机制是synchronized关键字,它提供了独占锁。除此以外,术语“同步”还包括volatile变量,显示锁和原子变量的使用。 
在没有正确同步的状况下,若是多个线程访问了同一个变量,你的程序就存在隐患。有3种方法修复它: 
l 不要跨线程共享变量; 
l 使状态变量为不可变的;或者 
l 在任何访问状态变量的时候使用同步。

 

volatile要求程序对变量的每次修改,都写回主内存,这样便对其它线程课件,解决了可见性的问题,可是不能保证数据的一致性;特别注意:原子操做:根据Java规范,对于基本类型的赋值或者返回值操做,是原子操做。但这里的基本数据类型不包括long和double, 由于JVM看到的基本存储单位是32位,而long 和double都要用64位来表示。因此没法在一个时钟周期内完成 

 

通俗的讲一个对象的状态就是它的数据,存储在状态变量中,好比实例域或者静态域;不管什么时候,只要多于一个的线程访问给定的状态变量。并且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问;

同步锁:每一个JAVA对象都有且只有一个同步锁,在任什么时候刻,最多只容许一个线程拥有这把锁。

当一个线程试图访问带有synchronized(this)标记的代码块时,必须得到 this关键字引用的对象的锁,在如下的两种状况下,本线程有着不一样的命运。
一、 假如这个锁已经被其它的线程占用,JVM就会把这个线程放到本对象的锁池中。本线程进入阻塞状态。锁池中可能有不少的线程,等到其余的线程释放了锁,JVM就会从锁池中随机取出一个线程,使这个线程拥有锁,而且转到就绪状态。
二、 假如这个锁没有被其余线程占用,本线程会得到这把锁,开始执行同步代码块。
(通常状况下在执行同步代码块时不会释放同步锁,但也有特殊状况会释放对象锁
如在执行同步代码块时,遇到异常而致使线程终止,锁会被释放;在执行代码块时,执行了锁所属对象的wait()方法,这个线程会释放对象锁,进入对象的等待池中)

 

Synchronized关键字保证了数据读写一致和可见性等问题,可是他是一种阻塞的线程控制方法,在关键字使用期间,全部其余线程不能使用此变量,这就引出了一种叫作非阻塞同步的控制线程安全的需求;

ThreadLocal 解析

顾名思义它是local variable(线程局部变量)。它的功用很是简单,就是为每个使用该变量的线程都提供一个变量值的副本,是每个线程均可以独立地改变本身的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每个线程都彻底拥有该变量。

每一个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的而且 ThreadLocal 实例是可访问的;在线程消失以后,其线程局部实例的全部副本都会被垃圾回收(除非存在对这些副本的其余引用)。

 

3、java内存模型

http://blog.csdn.net/jinyongqing/article/details/21343629

java中,线程之间的通讯是经过共享内存的方式,存储在堆中的实例域,静态域以及数组元素均可以在线程间通讯。java内存模型控制一个线程对共享变量的改变什么时候对另外一个线程可见。
线程间的共享变量存在主内存中,而对于每个线程,都有一个私有的工做内存。工做内存是个虚拟的概念,涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化,总之就是指线程的本地内存。存在线程本地内存中的变量值对其余线程是不可见的。
若是线程A与线程B之间如要通讯的话,必需要经历下面2个步骤,如图所示:
1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2. 而后,线程B到主内存中去读取线程A以前已更新过的共享变量。 

 http://ifeve.com/wp-content/uploads/2013/01/221.png

 
关于volatile变量
因为java的内存模型中有工做内存和主内存之分,因此可能会有两种问题:
(1)线程可能在工做内存中更改变量的值,而没有及时写回到主内存,其余线程从主内存读取的数据仍然是老数据
(2)线程在工做内存中更改了变量的值,写回主内存了,可是其余线程以前也读取了这个变量的值,这样其余线程的工做内存中,此变量的值没有被及时更新。
为了解决这个问题,可使用同步机制,也能够把变量声明为volatile,volatile修饰的成员变量有如下特色:
(1)每次对变量的修改,都会引发处理器缓存(工做内存)写回到主内存。
(2)一个工做内存回写到主内存会致使其余线程的处理器缓存(工做内存)无效。
基于以上两点,若是一个字段被声明成volatile,java线程内存模型确保全部线程看到这个变量的值是一致的。
此外,java虚拟机规范(jvm spec)中,规定了声明为volatile的long和double变量的get和set操做是原子的。这也说明了为何将long和double类型的变量用volatile修饰,就能够保证对他们的赋值操做的原子性了
ps:最上面那个加的例子貌似说明了第二种状况是不能使用volatile解决的,并且换成long或者double都是同样的结果,没效果。

 

关于volatile变量的使用建议:多线程环境下须要共享的变量采用volatile声明;若是使用了同步块或者是常量,则没有必要使用volatile。
 
java内存模型与synchronized关键字
synchronized关键字强制实施一个互斥锁,使得被保护的代码块在同一时间只能有一个线程进入并执行。固然synchronized还有另一个 方面的做用:在线程进入synchronized块以前,会把工做存内存中的全部内容映射到主内存上,而后把工做内存清空再从主存储器上拷贝最新的值。而 在线程退出synchronized块时,一样会把工做内存中的值映射到主内存,但此时并不会清空工做内存。这样一来就能够强制其按照上面的顺序运行,以 保证线程在执行完代码块后,工做内存中的值和主内存中的值是一致的,保证了数据的一致性!  
因此由synchronized修饰的set与get方法都是至关于直接对主内存进行操做,不会出现数据一致性方面的问题。
 
 
关于CAS
CAS 操做包含三个操做数 —— 内存位置(V)、预期原值(A)和新值(B)。 若是内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。不然,处理器不作任何操做。不管哪一种状况,它都会在 CAS 指令以前返回该 位置的值。(在 CAS 的一些特殊状况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;若是包含该值,则将 B 放到这个位置;不然,不要更改该位置,只告诉我这个位置如今的值便可。”
 
为何CAS能够用于同步?
例如,有一个变量i=0,Thread-1和Thread-2都对这个变量执行自增操做。 可能会出现Thread-1与Thread-2同时读取i=0到各自的工做内存中,而后各自执行+1,最后将结果赋予i。这样,虽然两个线程都对i执行了自增操做,可是最后i的值为1,而不是2。
解决这个问题使用互斥锁天然能够。可是也可使用CAS来实现,思路以下:
自增操做能够分为两步:(1)从内存中读取这个变量的当前值(2)执行(变量=上一步取到的当前值+1)的赋值操做。
 
多线程状况下,自增操做出现问题的缘由就是执行(2)的时候,变量在主内存中的值已经不等于上一步取到的当前值了,因此赋值时,用CompareAndSet操做代替Set操做:首先比较一下内存中这个变量的值是否等于上一步取到的当前值,若是等于,则说明能够执行+1运算,并赋值;若是不等于,则说明有其余线程在此期间更改了主内存中此变量的值,上一步取出的当前值已经失效,此时,再也不执行+1运算及后续的赋值操做,而是返回主内存中此变量的最新值。“比较并交换(CAS)”操做是原子操做,它使用平台提供的用于并发操做的硬件原语。
 
 
总结:volatile关键字只保证每次对该变量的修改,都反映到主内存中,且
相关文章
相关标签/搜索