Java 内存模型

1. 内存模型概念

(1)内存模型(Java Memory Model)和内存结构(堆栈那些)不是一个层面的概念,JMM 定义了一套在多线程读写共享数据(成员变量,静态变量等,而不是局部变量这种线程私有的)时,对数据的可见性有序性、和原子性的规则和保障。

(2)JMM规定了所有的变量都存储在主内存(虚拟机内存的一部分,但可以和操作系统的类比)中;

每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量;

不同的线程之间也无法直接访问对方的工作内存中的变量,线程间变量的值传递均需要通过主内存来完成。

(3)如果非要把主内存工作内存,和内存结构的堆栈方法区对应,那么主内存应该对应堆中的对象的实例数据部分,而工作内存对应栈。

2. 原子性

要执行就执行完,不能执行一半

违背原子性例子:两个线程对一个静态变量 i=0 执行 i++ 和 i--(每个对应四条JVM字节码指令),可能导致结果不是0

解决:用synchronized加锁,加锁位置应尽量减少代码获取释放锁的次数

synchronized( 对象 ) {
    要作为原子操作代码
}

3. 可见性

指一个对象能看到或访问另一个对象的能力

违背可见性的例子:t线程看不到主线程run变量的改变

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(()->{
        while(run){
            // ....
        }
    });
    t.start();
    Thread.sleep(1000);
    run = false; // 线程t不会如预想的停下来
}

解决:volatile(易变关键字),它可以用来修饰成员变量和静态成员变量,线程操作 volatile 变量都是直接操作主存

因此上面的run被修改后会存到主内存,t线程访问主内存内容会看到改变

4. 有序性

代码按顺序执行

违背有序性的例子:由于即时编译器在运行时会有指令重排的优化,多线程情况下可能出现非预期结果

解决:volatile 修饰的变量,可以禁用指令重排

因此volatile可以保证可见性和有序性,不能保证原子性,但属于轻量级并发控制;而synchronized可以保证三者,但更重量级

5. CAS

CAS 即 Compare and Swap ,它的实现用的是乐观锁的思想

// 需要不断尝试
while(true) {
    int 旧值 = 共享变量 ; // 比如拿到了当前值 0
    int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1

    if( compareAndSwap ( 旧值, 结果 )) {
    // 成功,退出循环
    }
}
  • 获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰
  • 结合 CAS 配合 volatile 可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下
  • 因为没有使用 synchronized(悲观锁),所以线程不会陷入阻塞,这是效率提升的因素之一
  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
  • CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令
  • 原子操作类例如:AtomicInteger、AtomicBoolean等,它们底层就是采用 CAS 技术 + volatile 来实现的
 


原文地址:https://www.cnblogs.com/Kinghao0319/p/14535057.html