JAVA多线程(四)--线程同步-Volatile

好久不见. 提交于 2020-03-01 07:36:10

1.cpu cache模型

  • cpu - 计算机的主内存(RAM), cpu速度变快,主内存没有跟上cpu的步伐,最终导致 cpu的处理速度和主内存的访问速度差距越来越大。
  • 而cpu cache的出现解决了以上问题,同时引入了缓存不一致的问题。
    比如:i++
    1)首先读取内存到cpu cache
    2)进行i+1
    3)将结果刷新到主内存中
    在单线程中这个例子不会出现任何问题,但是在多线程的情况下就会出现问题。原因就是:每个线程都有自己的工作内存(本地内存,对应cpu cache模型中的cache),i在多个线程的本地内存中都会存在一个副本。
    例:i 初始化为0;有两个操作A和B,都执行i++操作,按道理执行完之后i的值为2,但是就会有可能出现最后的结果i为1的情况,这就是缓存不一致的问题,要解决缓存不一致,我们有以下解决方式:
    1)总线加锁
    cpu和其他组建通信是通过总线来进行,采用总线加锁,会阻塞其他cpu对其他组件的访问,导致只有一个cpu抢到总线锁能够访问当前数据的内存。
    2)通过缓存一致性协议(MESI)保证能够保证每一个缓存中使用到的共享变量副本是一致的,大致思想就是说当cpu操作cache中
    的数据时,发现该数据是共享的,会进行如下操作:
    a. 读取操作,不用作任何处理
    b. 写入操作,发出信号通知其他cpu将该变量的cache line置为无效状态,其他cpu再次使用数据时需要从主内中再次获取。
    2.、java内存模型
    java线程 - 工作内存 - 主内存
    Java内存模型定义了线程和主内存之间的抽象关系:
  1. 共享变量存储于主内存之中,每个线程都可以访问
  2. 每个线程都有私有的工作内存(本地内存)
  3. 工作内存只存储该线程对共享变量的副本
  4. 线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存
    在这里插入图片描述
    3.volatile关键字
    先看一个例子:public static volatile int x = 10;
    我们发现在它的底层volatile修饰的变量存在于一个"lock;"的前缀,这个前缀相当于内存屏障。
    例:int x = 0;
    int y = 1;
    volatile int z = 100; //被volatile修饰,有内存屏障
    x++;
    y–;
    这个内存屏障的作用:
    1)保障指令重排序时不会将其后面的代码拍到内存屏障之前
    2)保障指令重排序时不会将其前面的代码拍到内存屏障之后
    3)保障在执行到内存屏障修饰的指令时前面的代码都能够全部得到执行
    4)强制将线程工作内存中值的修改刷新到主内存当中
    5)如果此时对volatile修饰的变量进行写操作,会导致其他线程工作内存中的缓存数据失效
    4、volatile对于并发编程三大特性的保证
    可以保证可见性,顺序性(禁止JVM和处理器对于volatile关键字修饰的指令重排序)
    例:10个线程对一个变量inc进行++1000次,通过结果来分析volatile能否保障原子性答案:不能
public class GY{
    /**
     * 有一个任务想要往下执行,必须等到其他任务执行完毕之后才可继续向下执行。
     */
    private static CountDownLatch countDownLatch = new CountDownLatch(10);

    public volatile static int inc = 0;

    public static void increase(){
        inc++;//本身不具备原子性,执行过程中有可能被其他线程打断
    }

    public static void main(String[] args) {
        for(int i=0; i<10; i++){
            new Thread(){
                @Override
                public void run() {
                    for(int i=0; i<1000; i++){
                        increase();
                    }
                    countDownLatch.countDown(); //计数器-1
                }
            }.start();
        }

        try {
            //等待其他线程执行完之后方可执行
            countDownLatch.await();// 10
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(inc);
    }
}

最后的结果可能不是1000,原因就是 inc++;本身不具备原子性,执行过程中有可能被其他线程打断。
再看一个例子: i =100
A: i+1 读取i=100,cpu切换到B去执行,i+1 = 101,刷新至主内存,cpu切换到B去执行
B: i=100,i+1,未刷新到主内存中,cpu切换到A去执行,刷新101至主内存
在这个例子中,我们也能够发现i最后只改变了一次,也就解释了上面的结果可能不是10000,说明volatile并不能保证原子性。
5.volatile和synchronized两者的区别
从使用上来看:

  1. volatile关键字只能用于修饰实例变量或者类变量,不能用于修饰方法以及方法参数、局部变量、常量等;
  2. synchronized关键字不能用于对变量的修饰,只能用于修饰方法或者语句块;
  3. volatile修饰的变量可以为null,synchronized关键字同步语句块的monitor对象不能为null;
    从对原子性的保证来看
  4. volatile无法保证原子性;
  5. 由于synchronized是一种排他的机制,因此被synchronized关键字修饰的同步代码是无法被中途打断的,因此其能够保证代码的原子性;
    从对可见性的保证来看
  6. 两者均可以保证共享资源在多线程间的可见性,但是实现机制完全不同
  7. Synchronized借助于JVM指令monitor enter和monitor exit对通过排他的方式使得同步代码串行化,在monitor exit时所有共享资源都将会被刷新到主内存中;
  8. 相比于synchronized关键字,volatile使用机器指令“lock;”的方式迫使其他线程工作内存中的数据失效,不得到主内存中进行再次加载;
    从对有序性的保证来看
  9. volatile关键字禁止JVM编译器以及处理器对其进行重排序,所有它能够保证有序性;
  10. 虽然synchronized关键字所修饰的同步方法也可以保证顺序性,但是这种顺序性是以程序的串行化执行换来的,在synchronized关键字所修饰的代码块中代码指令也会发生指令重排序的情况,但是由于synchronized关键字同步的作用,所以对程序来说没有任何的影响;
    其他方面:
  11. volatile关键字不会使得线程陷入阻塞;
  12. synchronized关键字会使得线程进入阻塞状态。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!