JMM与Volatile

一、JMM

我们知道Java是一个跨平台的语言,那么Java是如何屏蔽掉各种操作系统、各种机器对内存访问的差异呢?

在JVM规范中,Java定义了一种内存模型,用来屏蔽掉各种硬件和操作系统的内存访问差异

1.JMM中的三大特性

  • 可见性
    • 当一个线程修改了某一个共享变量的值,其他线程能够立即知道该值变更。
    • 类似于git,在仓库中拷贝下去,自己独立的备份,仓库原来的不变,更新完成后可以提交到仓库(主内存)
    •  

       

  • 有序性
    • 对于一个线程的执行代码,Java规范规定JVM线程内部维持顺序,只要运行结果和顺序化执行的结果相同,那么指令执行的顺序可以和代码顺序不一样
  • 原子性

2.happens-before(先行发生原则)

在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间必须存在happens-before(先行发生)原则。

比如

int x = 5; //线程A执行
int y = x; //线程B执行

y是否一定等于5呢?

如果线程A的操作复核happens-before操作,则y为5,反之则不一定。

hanppens-before原则有8条

①. 次序规则
一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作(强调的是一个线程)
前一个操作的结果可以被后续的操作获取。将白点就是前面一个操作把变量X赋值为1,那后面一个操作肯定能知道X已经变成了1

②. 锁定规则
(一个unlock操作先行发生于后面((这里的"后面"是指时间上的先后))对同一个锁的lock操作(上一个线程unlock了,下一个线程才能获取到锁,进行lock))

③. volatile变量规则
(对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的"后面"同样是指时间是的先后)

④. 传递规则
(如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出A先行发生于操作C)

⑤. 线程启动规则(Thread Start Rule)
(Thread对象的start( )方法先行发生于线程的每一个动作)

⑥. 线程中断规则(Thread Interruption Rule)

对线程interrupt( )方法的调用先发生于被中断线程的代码检测到中断事件的发生
可以通过Thread.interrupted( )检测到是否发生中断
⑦. 线程终止规则(Thread Termination Rule)
(线程中的所有操作都先行发生于对此线程的终止检测)

⑧. 对象终结规则(Finalizer Rule)
(对象没有完成初始化之前,是不能调用finalized( )方法的 )

 

A Happens-before B,则意味着A发生过的事情对B来说是可见的

 

    private int value=0;
    public void setValue(){
        this.value=value;
    }
    public int getValue(){
        return value;
    }

如果get、set方法由两个进程进行,为了满足A先于B,要么加锁,要么使用volatile关键字

 

二、volatile

为了满足JMM内存模型的要求,我们使用了volatile关键字,关键字由两个特点:可见性、有序性。(并没有完全实现JMM的三个要求)

使用volatile修饰变量的时候,

如果写一个volatile变量,JMM会把该线程对应的本地内存中的共享变量立即刷新回到主内存中。

如果读一个volatile变量时,JMM会把该线程对应的工作内存设置为无效,直接从主内存中读取共享变量。

 

 

总结为一句话:一句话,volatile修饰的变量在某个工作内存修改后立刻会刷新会主内存,并把其他工作内存的该变量设置为无效。

那么volatile怎么保证呢?====》内存屏障

 

内存屏障

内存屏障粗分两种:

写屏障

在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中

读屏障

在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据。

细分四种

 

 

因此volatile实现了有序性和和可见性

 

但是,volatile并不适合运算,如i++,因为volatile并没有实现原子性

 那么volatile平常适用于哪些场合呢

状态标志,判断业务是否结束

复制代码
//这个前面讲过
public class UseVolatileDemo
{
    private volatile static boolean flag = true;

    public static void main(String[] args)
    {
        new Thread(() -> {
            while(flag) {
                //do something......循环
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            flag = false;
        },"t2").start();
    }
}
复制代码

开销较低的读,写锁策略

复制代码
public class UseVolatileDemo
{
    //
   // 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
   // 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
     
    public class Counter
    {
        private volatile int value;

        public int getValue()
        {
            return value;   //利用volatile保证读取操作的可见性
              }
        public synchronized int increment()
        {
            return value++; //利用synchronized保证复合操作的原子性
               }
    }
}
复制代码

DCL双锁案例

复制代码
public class SafeDoubleCheckSingleton
{
    private static SafeDoubleCheckSingleton singleton; //-----这里没加volatile
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
                    singleton = new SafeDoubleCheckSingleton();
                    //实例化分为三步
                    //1.分配对象的内存空间
                    //2.初始化对象
                    //3.设置对象指向分配的内存地址
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}
复制代码

 

posted @   冬日寻雾记  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示