volatile和synchronized的原子性以及重排序造成的问题

720 阅读8分钟

volatile

volatile是轻量级同步机制,访问时不会执行加锁操作 volatile这个关键字的作用:

1. 可见性:当操作一个volatile修饰的变量时,会从主内存刷新最新值
2. 防止重排序,加入内存屏障可以防止重排序操作

volatile没有原子性的问题

哪些操作是复合操作,而不是原子操作:

1. new一个对象其实分为三步:1. 开辟内存空间 2. 初始化对象 3. 对象指向内存空间
2. i++或者i+=1; 1. 获取i2. i+1 3. 新值即(i+1)赋值给i
3. 在32位JVM中long或者Double类型的赋值 因为long是8个字节,当运行在32位JVM虚拟机的时候会分为两步操作  1. 先读取前32位数据 2. 再读取后32位数据 在64位操作系统的时候long类型的赋值是原子性的

注意:volatile不能修饰 写入操作不能修饰依赖当前值的变量 因为有些操作不是原子性的 例子1: 但是不能保证原子性,而且系统内部不会对其进行优化. 注意:不能修饰写入操作依赖当前值的变量,这样会出现数据不一致问题.(因为不是原子性的) 就比如

volatile i=0;
i++;

如果a线程操作i++进行100次,b线程操作i++执行100次.请问最后i的值为多少? 答案:小于10000. 原因: 13. 当a线程抢占cpu,获得i=1, 14. 这个时候b线程抢占cpu,虽然volatile修饰的变量可以获得最新值,但是a并没有进行更新操作,所以不会更新主内存,这时b获得i=1的的确确是最新值 15. 然后b进行自增操作i=2 16. a抢占资源后继续执行自增操作,结果i=2 17. 最后的问题是a和b都进行了自增操作,最后i只自增了一次.

解决方案1:加synchronized锁(悲观锁)保证原子性. 解决方案2:使用Atomic包. Atomic包: 提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap) 参考: www.jianshu.com/p/84c75074f…


synchronized

注意:每个对象都有一个对象锁

保证同一个时间内只有一个线程(拿到锁)可以访问这个代码块 修饰位置:

1. 普通方法,锁住整个方法,效率低,作用的是该对象实例.这样子作用是一个对象有多个synchronized内容,但是同一时间该对象实例只有一块synchronized内容可以被访问.
2. 静态方法,锁住整个方法,效率低,锁住的是该对象class类,作用的是该类的所有对象
3. 代码块,降低了锁住方法的颗粒度.
4. 修饰一个类,作用的是这个类的所有对象

synchronized作用:

1. 可见性:当线程获得锁的时候会清空工作内存,从主内存更新最新数据,当释放锁的时候会将更新同步到主内存中
2. 原子性:操作不可中断,多并发情况下同一时间只有一个线程操作锁内容,相当于单线程
3. 有序性:注意,它不能解决重排序问题,那为什么说它是有序的? 因为synchronized锁住后线程相当于单线程,符合as-if-serial原则.不管怎么重排序优化 它都是不影响最后的结果.但这是针对它本身来说的,如果多线程情况下就会出现问题.这个等下讨论.

这里的有序性是在内部观察时有序,因为在synchronized修饰代码中是单线程的,但是线程之间就不是有序性了,会发生重排序.

这里提一嘴:什么操作可能会有重排序问题呢? 8. new一个对象其实分为三步:1. 开辟内存空间 2. 初始化对象 3. 对象指向内存空间 9. i++或者i+=1; 1. 获取i值 2. i+1 3. 新值即(i+1)赋值给i 10. 在32位JVM中long或者Double类型的赋值 因为long是8个字节,当运行在32位JVM虚拟机的时候会分为两步操作1. 先读取前32位数据 2. 再读取后32位数据 在64位操作系统的时候long类型的赋值是原子性的.

那为什么会有问题呢?? 主要是因为有些操作不是原子操作,发生了重排序问题.单线程情况不会出现问题,但是一旦多线程就会出现问题.

举例2: 单例双重加锁机制

但是双重加载会有一些问题:虽然synchronized可以保证同一时间只有一个线程操作代码块,但是当创建单例对象的时候会出现重排序问题. 具体原因可以参见这两篇: www.cnblogs.com/a154627/p/1… blog.csdn.net/qq_22771739…

先脱离题目,了解下双重加锁的方法 第一种:synchronized锁住方法:效率低

class Singleton{
    private static Singleton singleton;
    private Singleton(){}
    public synchronized static Singleton getSingleton(){
        if(singleton==null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

第二种:双重加锁以及共享单例加volatile修饰,防止重排序

class Singleton{
    private volatile static Singleton singleton;
    private Singleton(){}
    public static Singleton getSingleton(){
        if(singleton==null){
            synchronized (Singleton.class){
                if(singleton ==null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

为什么要两次判断singleton空? synchronized锁住的是代码块,假设两个线程同时访问getSingleton()方法,a先进入第一个判空,但是b又抢占了资源也进入了第一个判空并且初始化;这个时候a抢占资源了 已经路过第一个判空了,如果没有第二个判空,它也会创建一个singleton.所以需要两次判空. 第三种:内部静态类:

class Singleton{
    private Singleton(){}
    private static class li{
        private static Singleton singleton = new Singleton();
    }
    public static Singleton getSIngleton(){
        return li.singleton;
    }
}

静态内部类不会随着外部类的初始化而初始化,他是要单独去加载和初始化的. 而类加载在多线程的情况下会被正确加锁以及同步,这样就保证了单例.

回到原来的题目:双重加锁的问题: 虽然synchronized代码块,但是发生了重排序问题:

singleton = new Singleton();

因为这个是复合操作:实际分为三步:

1. 开辟内存空间
2. 初始化对象
 3. 对象指向内存空间

但是发生了重排序问题: 操作2和操作3对换了,这个时候如果另一个抢占资源,第一个判空的时候获取到了一个加载到一半的对象.这个是有问题的,因为这个对象不完整

有人会问了: synchronized不是保证可见性吗? 它的可见性是指两个线程获取同一个锁的临界区的时候,在获取锁以及释放锁的时候保持可见性,获取到的是最新值,而不是针对执行synchronized修饰代码块的过程.

我也看过一个答案: jdk1.2以后内存模型的内存结构发生了变化:每次synchronized中创建的对象在工作内存中,只有当执行完毕后才会将对象刷新到主内存中.所以外部线程访问该对象的时候要么是null,要么是完整的对象.

我还特意看了内存模型的机制,是这样工作内存会拷贝主内存共享变量.想想感觉上面说的也没毛病啊,只有当代码执行完后,才会将工作内存的内容刷新到主内存中.

后来我是这么理解通的: 结合单例 java是值传递,工作内存中拷贝了主内存中的单例对象.但注意:并不是将内存中的对象复制一份到工作内存中,而是将该对象的地址复制到内存中,所以当synchronized在工作内存中new 单例的时候其实相当于在主内存操作,外部线程是可以访问到这个三个过程的,所以会出现不完整对象的问题. 所以需要volatile修饰共享单例防止重排序的发生

不知道我理解的对不对,欢迎指错

例子3:

class SafeCalc{
	static long value =0L;
	synchronized long get(){
		return value;
	}
	synchronized static void addOne(){
		value+=1;
	}
}

请问这段代码会有并发问题吗? 答案: 因为这两个锁的对象不是同一个,第一个对象锁是SafeCalc.class,第二个是this指对象实例 但是两个锁的临界区没有互斥关系,所以这两个方法对value没有可见性. 记住保证可见性的前提是 多个线程获取同一个锁对象会清空工作内存,加载主内存最新内存.以及释放锁对象更新主内存.

并发这个一块还有很多问题.. 我也只是学习了一部分,加油! 对了synchronized+volatile的双重加锁机制还是很有问题的,还停留在理论的,具体我也不清楚, 因为测试的时候如果只有synchronized也不会出现并发问题.

参考: www.cnblogs.com/ouyxy/p/724…