zl程序教程

您现在的位置是:首页 >  其他

当前栏目

volatile 详解

2023-04-18 14:55:32 时间

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

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

JMM(Java内存模型)

JMM本身是一种抽象的概念模型并不真实存在,塔描述的是一组规则或规范,通过这组规范定义了程序中各个变量(实例字段、静态字段、构成数组对象的元素)的访问方式

  • 线程解锁前,必须把共享变量的值刷新回主内存
  • 线程加锁前,必须读取主内存的最新值到自己的工作内存
  • 加锁解锁是同一把锁

可见性验证

class MyData {
    int number;
    public void setNumber60() {
        number = 60;
    }
}
/**
 * 验证volatile可见性
 * 
 * 假如int number = 0; 没有添加volatile关键字
 */
public class VisiblenessTest {
    public static void main(String[] args) {
        // 资源类
        MyData myData = new MyData();
        // 线程A
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "	 come in");
            // 模拟操作时间 3S
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.setNumber60();
            System.out.println(Thread.currentThread().getName() + "	 updated number value " + myData.number);
        }, "A").start();
        // 线程B
        new Thread(() -> {
            while (myData.number == 0){}
            System.out.println(Thread.currentThread().getName() + "	 is over");
        }, "B").start();
    }
}

输出:

A    come in
A    updated number value 60
但程序不结束

解析:

A线程、B线程使用同一份数据

A线程操作后,number已经被改变为60

B线程并不知道number已经被改变,所以一直在循环

使用volatile修饰number后

class MyData {
    volatile int number;
    public void setNumber60() {
        number = 60;
    }
}

输出:

A    come in
A    updated number value 60
B    is over
Process finished with exit code 0

B线程输出,程序正常结束

验证了volatile能够保证线程可见性,在A线程操作number后,B线程能够知道A线程修改后的值,结束循环,并正常输出

结论:

volatile可以保证可见性

原子性验证

原子性:不可分割,完整性,即某个线程正在做某个业务是,中间不可以呗分割,需要整体完成,要不同时成功,要不同时失败

class MyData2{
    volatile int number;
    public void add() {
        number++;
    }
}
/**
 * 验证volatile的原子性
 *
 * 原子性:不可分割,完整性,即某个线程正在做某个业务是,中间不可以呗分割,需要整体完成,要不同时成功,要不同时失败
 */
public class AtomicityTest {
    public static void main(String[] args) {
        MyData2 myData = new MyData2();
        // 20个线程,每个线程调用1000次,正常清苦,最终number值应该为20000
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myData.add();
                }
            }).start();
        }
        // 等待线程执行结束,获取myData.number,查看结果
        // 后台线程:Main GC,抛开他们两个
        while(Thread.activeCount() > 2) {}
        System.out.println(Thread.currentThread().getName() + "	 get number:" + myData.number);
    }
}

输出:

main     get number:18576
Process finished with exit code 0

20个线程,每个线程执行1000次,理论情况输出数值应该为20000,实际与理论不符,多次执行,发现输出小于20000

解析:

number++是非线程安全的

number++会被分为三步

1、获取number当前值,记作n1——也就是上面所讲的JMM中将主内存加载到工作内存

2、将n1值加一,记作n2——在工作内存中操作

3、将n2赋值给number——将工作内存中内容重新写入主内存

Compiled from "AtomicityTest.java"
class volatileTest.MyData2 {
  volatile int number;
  volatileTest.MyData2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field number:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field number:I
      10: return
}

注意看add方法中

getfield #2 获取值

iconst_1 定义整形1

iadd 相加

putfield #2 放回去

在多线程情况下,T1获取number值为0,T2获取number值为0,在T1将number加为1时,T2获取到CPU,T1被挂起,这是主内存中number还是0,T2也将number加为1,并写会主内存,T1继续执行,再写一遍,则正常情况number现在的值应该为2,但是还是1,出现了线程操作丢失的问题,所以输出永远小于20000

那么如何解决原子性问题呢?

1、加锁Synchronized Lock

2、原子类操作 将number设为AtomicInteger

  • 有序性undefined计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分为以下三种:

image.png

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一直

处理器在进行重排序时必须要考虑指令之间的数据依赖性

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

例:

{
        int x = 2;  // 语句1
        int y = 3;  // 语句2
        x = x + 5;  // 语句3
        y = x * x;  // 语句4
}

因为Java会先对源码进行编译,那么编译后的代码顺序有可能会与源码不一致

例如语句1和语句2没有先后顺序要求,那么最终的顺序有可能是:

1234、2134、1324

但是4一定不会在前面,因为他要依赖x的值,且过程中x的值有所改变

但是多线程下指令重排可能会导致数据执行不一致的情况,无法保证数据的最终一致性

image.png

public class ReSortSeqTest {
    int a = 0;
    boolean flag = false;
    public void method1() {
        a = 1;            // 语句1
        flag = true;      // 语句2
    }
    public void method2() {
        if (flag) {
            a = a + 5;
            System.out.println("****** return:" + a);
        }
    }
}

如果顺序执行,那么方法2输出应该为6

但是如果在多线程且指令重排的情况下,有可能语句2先于语句1执行,但是执行完语句2,被其他线程抢占了CPU,执行方法2,那么此时a的值仍未默认值0,那么输出5,此时语句1才执行,a又被赋值为1,此时的情况我们无法准确地考虑最终的结果,但是指令重排不好演示,所以只能枯燥的讲一下,后续如果有更好的演示方式,我再进行更新