Java内存模型

Java内存模型

文章目录

  • Java内存模型
  • 1.为什么要计算机同时处理几件事情
  • 2.主内存与工作内存
      • 注意:
    • 2.1主内存
    • 2.2工作内存
      • 2.2.1注意
  • 3.volatile关键字
    • 3.1多线程并发状态下三大特性
      • 3.1.1原子性
        • 3.1.1.1为什么保证原子性
      • 3.1.2可见性
        • 3.1.2.1为什么保证可见性
      • 3.1.3有序性
        • 3.1.3.1为什么保证有序性
      • 3.2volatile的性质
        • 3.2.1volatile可以保证可见性为什么无法保证原子性(简单概述)
        • 3.2.2volatile的作用场景
        • 3.2.3为什么volatile可以保证有序性和可见性
          • 3.2.3.1保证可见性
          • 3.2.3.2保证有序性
            • 3.2.3.2.1为什么禁止了指令重排

1.为什么要计算机同时处理几件事情

  1. 计算机的运算能力变的更强了
  2. 计算机的运算速度与它的存储和通信子系统的速度差距太大,大部分时间花费在I/O,网络通讯,数据库的访问上

我们不希望处理器在大部分时间都处于等待其他资源的空闲状态,为了节省,节约资源。我们选择让计算机同时处理多件任务

2.主内存与工作内存

《深入理解Java虚拟机》这本书中给我们讲述了一个概念:

Java内存模型的主要目的:是定义程序中各种变量的访问规则。即关注在虚拟机中把变量值存储到内存和从内存中取出变量值的底层细节

然后它讲述,这里面的变量Java中的变量还不一样,

前者包括了

  1. 实例字段
  2. 静态字段
  3. 构成数组对象的元素

但是不包括局部变量方法参数

但是后者

它的线程是私有的,不会被共享,自然不会发生竞争关系


这段话有点拗口

我理解的大致意思是:

java虚拟机中的变量是所有线程共有,比如你定义100张车票,然后弄2个线程,让他们去竞争这100张车票

会出现谁去领取第一张车票,谁去领取第2张车票这种问题。这个车票就是我们刚才java虚拟机中说到的变量

然后java中的变量就理解为多线程中你某个线程定义的private变量,其他线程没办法访问读取它


java虚拟机中的规定所有变量都存储到主内存中,每条线程都有属于自己的工作内存

工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取,赋值)都在工作内存中完成,不能直接读取主内存的数据

注意:

如果你使用volitale这个关键字的话,会少掉两个步骤

  1. 读取操作:当一个线程访问一个 volatile 变量时,会直接从主内存中读取最新的值,而不是从工作内存中读取。这确保了对该变量的读取操作能够获取最新的值。
  2. 写入操作:当一个线程对一个 volatile 变量进行写入操作时,会立即将写入的值刷新到主内存中,而不是仅仅更新自己的工作内存。这确保了对该变量的写入操作对其他线程是可见的。

就是有了这个关键字,保证了多线程的可见性有序性

2.1主内存

Java 程序中所有线程共享的内存区域,它存储了对象实例、静态变量、类信息等数据。主内存可以被多个线程同时访问。

2.2工作内存

工作内存是线程私有的内存区域,每个线程都有自己的工作内存。工作内存包含了线程运行时所需的数据,例如线程栈、本地变量等。

Java内部定义了8个方法

lock 加锁,把一个变量标识为一条线程独占的状态 主内存
unlock 把刚才加的那个锁给去掉 主内存
read 把一个变量的值从主内存传送到工作内存 主内存
load read操作主内存得到的变量值放入到工作内存的变量副本 工作内存
use 工作内存的变量值传递给执行引擎 工作内存
assign 把从执行引擎获得的值传递给工作内存 工作内存
store 工作内存的变量传送给主内存 工作内存
write 工作内存的变量值放入主内存的变量中 主内存

lock和unlock肯定不用想了,这个很好理解

read是把主内存的变量 传送到 工作内存

而load是把主内存的变量 读到 工作内存中的副本

read和load的不同的是,read只是把主内存的变量 传送到 工作内存,然后就没了,比如你要送某个人回家,你把她送到她家门口就没了,你也不能指望你把她送到家里面

而load在read把她送到家门口的基础上,然后又把她送到家里面

就是这样的道理

下面的storewrite也是这个样子的,不过read是把主内存的变量传给工作内存;而store是把工作内存的变量传给主内存

剩下就剩useassign了,其实use就可以理解为使用一个变量;assign相当于给变量赋值

这两个都在工作内存中执行

2.2.1注意

如果要把一个变量从主内存复制到工作内存,那么你得先执行read然后执行load

如果你要把工作内存的值同步到主内存,那么你得先执行store再执行write

java内存模型要求上面的两个操作按顺序执行,但不要求是连续

比如说我现在先:

read a

然后不用直接load a,你可以再read b

可以出现read a read b load b load a

3.volatile关键字

我们在主内存和工作内存中讲过,如果不用volatile的话,直接写的话,它是先把变量放到自己的工作内存,然后再把工作内存的东西放到主内存

但是我们用了volatile的话就会少掉把工作内存的东西放入主内存的这个操作

说到这个之前我们先简单聊聊多线程并发状态下的3个特性

3.1多线程并发状态下三大特性

多线程并发状态下的三大特性分别是:

1.原子性

2.有序性

3.可见性

3.1.1原子性

一般说到原子性就说原子性和原子操作,在《Android进阶之光》中我对它的理解就是,一个过程,要么你一次性执行完,要么不执行

经典的原子操作就是赋值操作,还有提供的一些原子性操作的方法

IPC方式中,讲到过一个叫作:

 private AtomicBoolean mIsServiceDestroyed = new AtomicBoolean(false);

这里mIsServiceDestroyed就是一个具有原子性的一个boolean

前面举得都是有原子性,现在举一些没有原子性的,最常见的就是自增操作

为什么自增操作不是一个原子性操作呢?

你可以把自增操作看成4个步骤

  1. 读取变量的值:首先,线程会读取变量的当前值,以便进行后续操作。
  2. 增加变量的值:接下来,线程会对读取的值进行递增操作。
  3. 写入新值:线程将递增后的值写回到变量中,更新变量的值。
  4. 完成操作:线程完成自增操作,并继续执行后续的指令。

《Java虚拟机》中用字节码的方式给我们写了一下,但我看不懂。反正就和我上面是一个意思

可以明白自增操作不是一个原子操作,也不具有原子性

Java内存模型直接保证的原子性变量操作包括:read,load,assign,use,store,write

3.1.1.1为什么保证原子性

那么为什么保证原子性呢,比如这样

public class rwo {
    public static int i = 0 ;
    public static void add(){
        for(int w = 0;w<=100;w++) {
            i++;
        }
    }
}



public class one {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
               add();
            }
        });
        thread.start();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                    add();
            }
        });
        thread1.start();
        System.out.println(i);
    }
}

我们原本是想让结果输出202,但是运行之后输出来的结果各不一样,有时候是202,有时候又是100多,这就是为什么我们要保证原子性,如果不保证原子性,它可能自增操作还没执行完,就执行别的操作了

3.1.2可见性

可见性就更容易了,我们刚才扯了那么久的主内存,工作内存就是和这个有关的

一个线程的工作内存的变量,别的线程没办法获得变量

3.1.2.1为什么保证可见性

我们为什么要保证多线程的可见性呢?试着想一下,如果一个线程改变了一个值,但是另一个线程也在调用它,不保证可见性的话,可能原本的是4,进行+1操作是5,但是你另一个线程调用过来那个变量还是4,就会导致错误

3.1.3有序性

有序性可能会颠覆以前我们的思考逻辑

在java中存在一种叫做:指令重排序的现象

比如:

a = 1;
b = 2;
a++;
b++;

这段代码我们的理解就是给a赋值为1,b赋值为2,然后执行**a++操作后面执行b++**操作

但是实际上可能会出现,给a,b赋完值后,先执行b++然后执行a++

在单线程中指令重排序不会影响正确性,但是会影响多线程并发的正确性

3.1.3.1为什么保证有序性

比如说

public class ReorderingExample {
    private static int x = 0;
    private static int y = 0;
    private static int a = 0;
    private static int b = 0;
public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new Thread(() -> {
        a = 1;
        x = b;
    });

    Thread thread2 = new Thread(() -> {
        b = 1;
        y = a;
    });
    thread1.start();
    thread2.start();
    thread1.join();
    thread2.join();
    // 输出的结果可能为 (0, 0),表示发生了指令重排序
    System.out.println("(" + x + ", " + y + ")");
}
}

我们本想输出(1,1)但是最后输出(0,0)这就是因为出现了指令重排序

先执行了x = b这个操作,然后执行y = a这个操作,然后是a = 1,b = 1最后输出(0,0)

3.2volatile的性质

volatile可以说是Java虚拟机提供的最轻量级同步机制

volatile可以保证多线程的可见性有序性,那么原子性呢?

继续跑我们刚才的这段代码

public class rwo {
    public volatile static int i = 0 ;
    public static void add(){
        for(int w = 0;w<=100;w++) {
            i++;
        }
    }
}

public class one {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
               add();
            }
        });
        thread.start();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                    add();
            }
        });
        thread1.start();
        System.out.println(i);
    }
}

我们发现打印出来的和刚才一样,要么202,要么100多,要么不到100

但是我们在add方法前面加上synchronized

public synchronized static void add(){
        for(int w = 0;w<=100;w++) {
            i++;
        }
    }

会发现,无论你跑了多少次,最后结果都是202

这一点就说明了volatile无法保证原子性,也说明了自增操作不是一个原子操作

3.2.1volatile可以保证可见性为什么无法保证原子性(简单概述)

我们说volatile可以保证操作的可见性,它可以少掉将变量放入工作内存再放到主内存

而是直接放到主内存

但是为什么无法保证原子性呢?
我们还是举刚才的例子

假设现在的i的值为0

第一个线程执行i++,它先读取i的原始值,然后第一个线程被阻塞了。然后执行第二个线程,这时候第二个线程将所有操作执行完,i的值已经变成了1。第一个线程再去执行后面的操作的时候因为之前已经读取过i的值了,就不会继续去读取,进行**+1操作**,结果第1个线程最后执行完后的结果还是1。

本来我们想的是2,结果因为没保证原子性,出来的结果还是1

3.2.2volatile的作用场景

1.已经可以保证操作的原子性

比如这段代码

public class Example {
private volatile boolean flag = false;
public void setFlag() {
    // 更新状态标志位
    flag = true;
}

public void doSomething() {
    // 检查状态标志位
    if (flag) {
        // 执行相应操作
    }
}
}

flag的赋值操作是原子操作,只要满足了原子操作,我们就可以用volatile关键字了

2.DCL双重锁检验

public class Singleton {
private static volatile Singleton instance;
private Singleton() {
    // 私有构造函数
}

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}
}

一般它们会问volatile的作用与两个if的作用

volatile的作用就是保证了可见性有序性,可是下面不是已经有synchronized 了嘛?为什么还要加volatile,很简单,因为instance在这个锁的外面,要是在里面的话,就不需要加volatile

现在说一下两个if的作用

  1. 第一个if其实可以加也可以不加,不加是因为其实第二个if判断就可以解决所有的事,加上第一个if的目的是为了增加效率,如果一旦发现有一个instance不为null就直接退出,不用进入synchronized,使用synchronized会增加一些开销,如线程之间的竞争、锁的获取和释放等操作会引入一定的性能消耗。
  2. 第二个if就是真正真正的进行判断了,因为加上了锁保证了在同步块内部再次检查共享变量的状态,防止其他线程在等待锁期间已经初始化了共享变量,避免重复初始化,以确保只有一个线程执行初始化操作。

3.2.3为什么volatile可以保证有序性和可见性

3.2.3.1保证可见性

因为它带有一个lock前缀,它的作用就是将本处理器的缓存写入内存,该写入操作会引起别的处理器或者别的内核无效化其缓存,相当于store和write操作,让volatile变量的修改对其他处理器立即可见

3.2.3.2保证有序性

《深入理解java虚拟机》中说到volatile修饰的变量,赋值后会多执行一个lock addl $0x0,(%esp)操作,这个操作相当于一个内存屏障,指令排序时不能把后面的指令排序到内存屏障之前的位置,只有一个处理器访问的时候,并不需要内存屏障;但如果有2个或者以上的多处理器访问同一内存,其中有一个在观察另一个,这个时候就需要内存屏障

3.2.3.2.1为什么禁止了指令重排

虽然可能发生指令的重排序,但是处理器必须正确处理指令依赖情况保障程序能够得出正确执行结果

比如:指令1把地址A的值+10,指令2把地址A的值×2

指令3把地址B的值减去3

1,2这两个指令有依赖关系。但是1,3;2,3没有这种关系

1与2不能乱排,但是2,3;1,3可以乱排

只要保证了处理器执行后面依赖到A,B的值的操作时能获得正确的A与B即可

因此用volatile修饰后把修改同步到内存,意味着前面的操作都已经执行过了,因此出现指令重排序无法越过内存屏障的效果

内存屏障之前的指令都会在内存屏障之前执行,而在内存屏障之后的指令都会在内存屏障之后执行,保证了操作的有序性。内存屏障之前的指令都会在内存屏障之前执行,而在内存屏障之后的指令都会在内存屏障之后执行,保证了操作的有序性。

你可能感兴趣的:(JVM,java,jvm,开发语言)