JVM学习笔记(六)JMM和Volatile

1.JMM

JMM(Java Memory Model的缩写)是一种java内存模型。它类似于缓存一致性协议,用于定义数据读写的规则。
JMM定义了线程工作内存和主内存的一种抽象关系,线程中的共享对象存在于主内存中,每个线程都有一块私有的本地内存。
解决共享对象可见性问题,即线程中修改数据立马同步到主线程中,其它线程复制时可以得到最新的数据:volilate和synchronize关键词。(Synchronized保证内存可见性和操作的原子性;Volatile只能保证内存可见性)
JMM数据同步模型如下图。
在这里插入图片描述

2.Volatile

2.1.什么是volatile?

volatile是JAVA虚拟机提供的轻量级的同步机制。

2.2volatile的特性:

(1)可见性

  • volatile的可见性是多线程同步之间的一种通讯机制,在JMM中规定共享变量(实例域、静态域、数组元素)放在主内存中,非共享变量(局部变量、方法定义参数和异常处理器参数)放在每个线程自己的工作内存,每个线程在使用共享变量时会将其拷贝到自己的工作内存进行操作,但操作完成后不知道何时将其写回主内存,此
    时主内存的值未发生改变,其他线程嗅探到主内存的值未发生改变,导致其他线程操作了脏数据”。
class MyThread extends Thread{
    
    
    int number =0;
    @Override
    public void run() {
    
    
        System.out.println("我的线程开始执行");
        while (true) {
    
    
            /**
            *当嗅探到number的值被改变时跳出循环
            *未使用volatile修饰number,
            *此时main线程修改了number的值,
            *但是对于我的线程并不可见,所以会死循环
            */
            if (number != 0) {
    
    
                break;
            }
        }
        System.out.println("我的线程执行结束");
    }
}
/**
 *  volatile实例类
 *   number未使用volatile修饰,多线程之间不具有可见性
 */
public class VolatileDemo {
    
    
    public static void main(String[] args) {
    
    
        MyThread myThread = new MyThread();//创建我的线程
        myThread.start();
        /**
         *  为防止主线程跑在 我的线程 之前,
         *  即防止我的线程还未开始时主线程已经执行完毕,将number的值该为100后我的线程才跑
         */
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        //在程序执行1秒后这段话会被执行,给 我的线程 足够的启动时间
        myThread.number=100;
        System.out.println(Thread.currentThread().getName()+" number="+myThread.number);
    }
}


在这里插入图片描述
但对于加了volatile关键字的变量进行操作,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在的缓存行的数据写回主内存,其他线程会嗅探在总线上的数据来检查自己的数据是否过期当发现自己的缓存行对应的内存地址被改变就会将当前线程中的变量置为无效,当线程需要对该变量进行操作时,会重新从主内存中读取变量最新的值,在进行操作,这样就实现了多线程之间的通信。

class MyThread extends Thread{
    
    
    volatile int number =0;
    @Override
    public void run() {
    
    
        System.out.println("我的线程开始执行");
        while (true) {
    
    
            /**
             * 当嗅探到number的值被改变时跳出循环
             * number被volatile修饰,
             * 所以当main线程修改了number的值时
             * 会及时通知 我的线程,我的线程从主内存
             * 中获取最新的值后跳出循环
             */
            if (number != 0) {
    
    
                break;
            }
        }
        System.out.println("我的线程执行结束");
    }
}
/**
 *  volatile实例类
 *   number使用volatile修饰,多线程之间具有可见性,实现多线程间的通信
 */
public class VolatileDemo {
    
    
    public static void main(String[] args) {
    
    
        MyThread myThread = new MyThread();//创建我的线程
        myThread.start();
        /**
         *  为防止主线程跑在 我的线程 之前,
         *  即防止我的线程还未开始时主线程已经执行完毕,将number的值该为100后我的线程才跑
         */
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        //在程序执行1秒后这段话会被执行,给 我的线程 足够的启动时间
        myThread.number=100;
        System.out.println(Thread.currentThread().getName()+" number="+myThread.number);
    }
}

在这里插入图片描述
(2)禁止指令重排序

指令重排序是指JAVA语言规定JVM线程内部维持顺序化语义(满足As-If-Serial语义和Happens-Before原则),即只要程序执行的最终结果与其顺序化执行的结果相同,JVM允许指令执行的顺序与代码逻辑书写的顺序可以不一致,这样的过程叫做指令重排序。指令重排序的意义是指令更加符合CPU执行的特性,提高执行效率。
在这里插入图片描述

但是在多线程中某些时候指令重排序会让程序出现预期之外的结果(如DCL中会造成线程不安全),所以我们有时候需要禁止JVM做这种指令重排序的优化。
volatile通过加入内存屏障防止这种指令重排,用volatile修饰的变量在写操作时在volatile写的前后分别加入StoreStore屏障和StoreLoad屏障防止写操作时发生指令重排,在volatile读操作之后加入LoadLoad屏障和LoadStore屏障防止读操作时发生指令重排序。

(3) 不保证原子性

原子性是指该操作是一个完整的整体,是不可被分割的,同一时刻只能有一个线程对它进行操作。

被volatile修饰的对象操作不能保证原子性,即该操作会因为线程调度器而被中断操作。

首先做一个测试:i++。输出结果为10,因为在底层实现的时候会引入一个临时变量具体为:

public static void main(String[] args) {
    
    
        //i++测试:
        int i=10;
        i=i++;
        System.out.println(i);
        /*
        *底层原理:
        _temp = i ;
        i = i + 1 ;
        i = _temp ;
        * */
    }

所以i++就是一个非原子性操作,采用多线程再次测试:测试结果中会因为i++的非原子性操作带来读写不一致问题。比如:1,3,2,2,4,5,6,7,8,9序列的出现。两个2的出现就说明了,两个线程在同时从主存中读取,和进行加一操作时出现的不一致问题,那么可以用Volatile来解决这个不一致问题吗?不行。因为,这是一个非原子性操作。

class MyData{
    
    
     volatile int number=0;
     //该方法会使number+1
    public void numberPlusPlus(){
    
     
        number++;
    }
}
public class VolatileByAtomicDemo {
    
    
    public static void main(String[] args) {
    
    
        MyData myData = new MyData();
        /**
         * 循环创建10个线程,都调用1000次numberPlusPlus使number+1
         */
        for (int i = 1; i <= 10; i++) {
    
    
            new Thread(()->{
    
    
                //双重for为了增加线程被抢占的概率
                for (int j = 1; j <= 1000; j++) {
    
    
                    myData.numberPlusPlus();
                }
            },String.valueOf(i)).start();

        }
        /**
         * java运行时会自动调起main线程和GC线程
         * Thread.activeCount()>2说明自己创建的线程还有未执行结束的,
         * 等待所有的自创建线程执行结束
         */
        while(Thread.activeCount() > 2){
    
    
            Thread.yield();
        }
        /**
         *  预期在循环结束后numberPlusPlus被调用10000次,该语句打印10000
         *  实际结果小于10000(有可能等于10000),原因时number++在执行时被拆分为3步
         *  1.读取number的值    2.执行number+1    3.将操作结果写回
         *  因为无法保证原子性,a线程在执行number++的第二步后,b线程抢占了资源
         *  此时a线程已经进行了+1操作但是因为b此时加塞抢占,a线程挂起,并未将值写回
         *  b线程读取到的值与a线程相同,b线程执行完number++操作后释放资源,
         *  a线程从挂起状态变为执行,但因为挂起前a线程只剩下写回操作,执行后直接写回,
         *  导致b线程的操作被覆盖,丢失操作数据
         */
        System.out.println(myData.number);
    }

}

如果想要保证原子性则需要在number++时加上synchronized,或者int类型的number换为能够保证原子性的AtomicInteger类型变量。

猜你喜欢

转载自blog.csdn.net/jingli456/article/details/114584827