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

  1. 云栖社区>
  2. 博客>
  3. 正文

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

codingcoge 2019-03-30 11:52:14 浏览463
展开阅读全文

volatile

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

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

volatile没有原子性的问题

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

1. new一个对象其实分为三步:1. 开辟内存空间 2. 初始化对象 3. 对象指向内存空间
2. i++或者i+=1; 1. 获取i值 2. 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.
原因:

  1. 当a线程抢占cpu,获得i=1,
  2. 这个时候b线程抢占cpu,虽然volatile修饰的变量可以获得最新值,但是a并没有进行更新操作,所以不会更新主内存,这时b获得i=1的的确确是最新值
  3. 然后b进行自增操作i=2
  4. a抢占资源后继续执行自增操作,结果i=2
  5. 最后的问题是a和b都进行了自增操作,最后i只自增了一次.

解决方案:加synchronized锁保证原子性.


synchronized

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

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

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

synchronized作用:

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

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

这里提一嘴:什么操作可能会有重排序问题呢?

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

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

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

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

先脱离题目,了解下双重加锁的方法
第一种: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也不会出现并发问题.

网友评论

登录后评论
0/500
评论
codingcoge
+ 关注