侧边栏壁纸
博主头像
MD_Tech_博客

行动起来,活在当下

  • 累计撰写 8 篇文章
  • 累计创建 8 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Volatile详解

Siuux_7
2025-03-10 / 0 评论 / 0 点赞 / 8 阅读 / 0 字

并发编程中的三个概念

原子性:

原子性:一个操作或者多个操作,要么全部执行,且执行的过程中不会被任何因素打断,要么就都不执行。
原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。

可见性

 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

public class JMMDemo {  
    private static int num=0;  
  
    public static void main(String[] args) {  
        new Thread(() -> {  
            while (num==0){  
  
            }  
        }).start();  
  
        try {  
            TimeUnit.SECONDS.sleep(5);  
        } catch (InterruptedException e) {  
            throw new RuntimeException(e);  
        }  
        num=1;  
    }  
}

以上代码执行时子线程会永远执行下去,即使主线程中已经将num的值改为1。这就是可见性问题,主线程对变量num修改了之后,子线程没有立即看到主线程修改的值。

有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性

单线程中,编译器和处理器会考虑指令之间的数据依赖性,进行重排,不会导致最终结果出现错误,
但是在多线程操作中会引起错误。

//线程1:
context = loadContext();  //语句1
inited = true;     //语句2
//线程2
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

上面代码中,因为语句1和语句2没有数据依赖,所以可能会进行指令重排,导致语句2先执行,那么线程2会认为初始化已经完成从而导致错误去使用context。

Java内存模型

在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

JMM.png

那么Java语言对原子性,可见性,有序性如何保证呢

  • 原子性
    • 基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操作。
    • 所有引用reference的赋值操作
    • java.concurrent.Atomic.* 包中所有类的一切操作
  • 可见性
    • Java提供了volatile关键字来保证可见性。
    • 通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
  • 有序性
    • 通过volatile关键字来保证一定的“有序性”
    • 通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

所以volatile保证了有序性、可见性,并不保证原子性

附:

另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

深入剖析volatile关键字

首先volatile只能修饰类的成员变量,类的静态成员变量。当共享变量被volatile修饰后,那么就具备了两层含义

  1. 保证了不同线程对这个变量进行操作的可见性,即一个线程修改了共享变量的值,这个新值对其他线程是可见的
    例子:修改上面的第一段代码,解决一直循环的问题
public class JMMDemo {  
    private volatile static  int num=0;  //使用volatile修饰
    public static void main(String[] args) {  
        new Thread(() -> {  
            while (num==0){  
  
            }  
        }).start();  
  
        try {  
            TimeUnit.SECONDS.sleep(5);  
        } catch (InterruptedException e) {  
            throw new RuntimeException(e);  
        }  
        num=1;  
    }  
}
  1. 禁止进行指令的重排序
    例子:DCL懒汉式
public class LazyMan {  
  
    private static LazyMan lazyMan;  
  
    private LazyMan(){  
        System.out.println(Thread.currentThread().getName()+"init");  
    }  
    private static LazyMan getInstance(){  
        if(lazyMan ==null){  
            synchronized (LazyMan.class){  
                if(lazyMan !=null){  
                    lazyMan = new LazyMan(); //不是一个原子性操作 指令可能重排,  
                                            //1.分配内存空间  
                                            //2.初始化对象  
                                            //3.把对象指向内存空间  
                                            //线程A 执行1,3后 线程B会得到空对象出现错误  
                                            //使用volatile修饰符  
                }  
            }  
        }  
        return lazyMan;  
    }  
}

volatile不保证原子性操作,下面看一个例子

public class Test {  
    public volatile int inc = 0;  
  
    public void increase() {  
        inc++;  
    }  
  
    public static void main(String[] args) {  
        final Test test = new Test();  
        for(int i=0;i<10;i++){  
            new Thread(){  
                public void run() {  
                    for(int j=0;j<10000;j++)  
                        test.increase();  
                };  
            }.start();  
        }  
  
        while(Thread.activeCount()>2)  //保证前面的线程都执行完  
            Thread.yield();  
        System.out.println(test.inc);  
    }  
}

程序的执行结果并不会如期望的一样为100000,事实上每次的结果都不一样,都是小于100000的数字。因为自增操作并不是原子性操作,volatile也不能保证原子性操作。
假设线程1先读取了变量的值为10,读取到工作内存后被阻塞,线程2也读取到变量的值为10再进行自增操作后返回到主内存。线程1唤醒后,因为已经将变量读取到了工作内存,会直接进行自增操作。结果两个线程执行自增操作最后结果只加了1

可以使用synchronized、Lock或者AtomicInteger进行修改

//synchronized
public void increase() {  
    synchronized (this) {  
        inc++;  
    }  
}
//Lock
public void increase() {  
    try {  
        lock.lock();  
        inc++;  
    }catch (Exception e) {  
        e.printStackTrace();  
    }finally {  
        lock.unlock();  
    }  
}
//AtomicInteger
public class Test {  
    public volatile AtomicInteger inc = new AtomicInteger();  
  
    public void increase() {  
        inc.getAndIncrement();  
    }  
  
    public static void main(String[] args) {  
        final Test test = new Test();  
        for(int i=0;i<10;i++){  
          new Thread(()->{  
              for(int j=0;j<10000;j++)  
                  test.increase();  
          }).start();  
        }  
  
        while(Thread.activeCount()>2)  //保证前面的线程都执行完  
            Thread.yield();  
        System.out.println(test.inc);  
    }  
}
0

评论区