zoukankan      html  css  js  c++  java
  • 关于java的volatile关键字与线程栈的内容以及单例的DCL

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

    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变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样

      对于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是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static SingletonKerriganD instance = null;”就可以保证每次取instance都从主内存读取,就可以使用DCL的写法来完成单例模式。

    二、以下来自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关键字引用的对象的锁,在以下的两种情况下,本线程有着不同的命运。
    1、 假如这个锁已经被其它的线程占用,JVM就会把这个线程放到本对象的锁池中。本线程进入阻塞状态。锁池中可能有很多的线程,等到其他的线程释放了锁,JVM就会从锁池中随机取出一个线程,使这个线程拥有锁,并且转到就绪状态。
    2、 假如这个锁没有被其他线程占用,本线程会获得这把锁,开始执行同步代码块。
    (一般情况下在执行同步代码块时不会释放同步锁,但也有特殊情况会释放对象锁
    如在执行同步代码块时,遇到异常而导致线程终止,锁会被释放;在执行代码块时,执行了锁所属对象的wait()方法,这个线程会释放对象锁,进入对象的等待池中)

     

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

    ThreadLocal 解析

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

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

     

    三、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关键字只保证每次对该变量的修改,都反映到主内存中,且
  • 相关阅读:
    关于深拷贝和浅拷贝的理解
    Oracle数据库入门
    单例模式入门
    oracle存储过程 (还没动手实践、剩余内容找时间在处理、游标还没接触)
    Oracle用户名及默认密码
    oracle数据库怎么创建数据库实例
    Java 强、弱、软、虚,你属于哪一种?
    内存溢出和内存泄漏的区别
    aop中execution 表达式
    JPA 中@Enumerated
  • 原文地址:https://www.cnblogs.com/guangshan/p/4972758.html
Copyright © 2011-2022 走看看