JMM内存模型 原子性 可见性

本文深入探讨Java内存模型(JMM),阐述其在多线程环境下如何保障原子性、可见性和有序性。通过示例代码解释了++i操作非原子性导致的问题,并展示了通过加锁实现原子性。同时,分析了volatile关键字确保可见性的作用,以及在防止指令重排序中的应用。最后,介绍了Happens-Before规则在保证线程间同步的重要作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JMM java内存模型,它是在多线程访问共享数据时,提供原子性、可见性有序性的规则和保障。

原子性

原子性:一个原子性操作可以包含一条或多条指令,一个原子性操作中的所有指令要么都不执行,要么全部执行且不会收到其他线程操作的影响。

package com.tech.jmm;

/**
 * @author lw
 * @since 2021/12/6
 */
public class A {
    static int i=0;
    static Object o=new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 50000; j++) {
                    ++i;
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 50000; j++) {
                    --i;
                }
            }
        });
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();

        System.out.println(i);
    }
}

 执行后,输出结果会出现多种情况,这是因为 ++i  --i并不是原子性操作导致。

++i对应的jvm字节码指令

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法
putstatic i // 将修改后的值存入静态变量i

--i对应的jvm字节码指令

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 减法
putstatic i // 将修改后的值存入静态变量i

++i 和 --i都是对应多条JVM指令,在多线程执行下可能会出现如下情况

出现负数:

// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1

出现正数:

// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1

由于以上情况的存在,会出现多种运行结果。

通过加锁,来实现原子性保障

package com.tech.jmm;

/**
 * @author lw
 * @since 2021/12/6
 */
public class A {
    static int i=0;
    static Object o=new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 50000; j++) {
                    synchronized (o){
                        ++i;
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 50000; j++) {
                    synchronized (o){
                        --i;
                    }
                }
            }
        });
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();

        System.out.println(i);
    }
}

多次运行后能得到正确执行结果

可见性

可见性:写线程对于共享变量的修改对于共享变量的修改,对其他线程是可见的,也就是读线程可以读取到共享变量的最新值。

package com.tech.jmm;

/**
 * @author lw
 * @since 2021/12/6
 */
public class B {
    static boolean run=true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            long start=System.currentTimeMillis();
            while (run){
                
            }
            System.out.println(System.currentTimeMillis()-start);
        }).start();
        
        Thread.sleep(1000);
        run=false;
    }
    
}

新建的线程需要频繁的从主内存读取共享变量run的值,JIT即时编译器对热点代码进行优化直接将主内存run的值保存到工作内存的高速缓存中,减少对主内存的访问提升性能。当主线程修改run值后,由于新建的线程使用的是工作内存中读取run的值,读到的run的值是true,所以程序一直循环执行。

对变量run加volatile修饰,确保可见性

package com.tech.jmm;

/**
 * @author lw
 * @since 2021/12/6
 */
public class B {
    volatile static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            long start = System.currentTimeMillis();
            while (run) {
            }
            System.out.println(System.currentTimeMillis() - start);
        }).start();

        Thread.sleep(1000);
        run = false;
    }

}

程序正常执行结束。

volatile 可以确保修改的可见性,当写线程修改共享变量值后,会立刻刷新到主内存,同时让其他读线程工作内存中变量的值失效,去主内存获取变量最新的值。

有序性

为了提升程序的执行性能,在不影响单线程执行结果的前提下,执行的指令会发生重排序。

package com.tech.jmm;

/**
 * DCL双重检测锁 单例模式
 * @author lw
 * @since 2021/12/7
 */
public class Singleton {
    private static Singleton INSTANCE=null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(INSTANCE==null){
            synchronized (Singleton.class){
                if(INSTANCE==null){
                    INSTANCE=new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

 基于双层检测锁机制实现的单例模式

package com.tech.jmm;

/**
 * DCL双重检测锁 单例模式
 * @author lw
 * @since 2021/12/7
 */
public class Singleton {
    private static Singleton INSTANCE=null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(INSTANCE==null){
            synchronized (Singleton.class){
                if(INSTANCE==null){
                    INSTANCE=new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

这个单例模式,看着没问题,但是由于在执行 INSTANCE=new Singleton(); 时内部存在重排序,会产生一定的问题,INSTANCE=new Singleton();对应的字节码指令如下:

0: new #2 // class cn/itcast/jvm/t4/Singleton
3: dup
4: invokespecial #3 // Method "<init>":()V
7: putstatic #4 // Field
INSTANCE:Lcn/itcast/jvm/t4/Singleton

0:为对象分配内存空间,并生成引用地址

3:将引用地址复制一份

4:执行对象的初始化方法,进行初始化

7:将对象的引用赋值给变量

总体流程是这样三个步骤:先为对象分配内存空间并生成引用,对象初始化,将对象的引用赋值给变量。

如果第二步和第三步发生重排序,当执行了将对象的引用赋值给变量,但是还没有执行对象初始化,如果此时另外一个线程调用获取对象实例的方法,检测到对象不为空,直接返回单例对象,但是此时对象还没有初始化完成,在使用时会出现多种问题。

解决方案:

在变量前添加volatile修饰,禁止创建对象时的指令重排

package com.tech.jmm;

/**
 * DCL双重检测锁 单例模式
 * @author lw
 * @since 2021/12/7
 */
public class Singleton {
    private volatile static Singleton INSTANCE=null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(INSTANCE==null){
            synchronized (Singleton.class){
                if(INSTANCE==null){
                    INSTANCE=new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

Happens-Before规则

happens-before规则定义了哪些操作的写对于其他线程的读是可见的,它是保障可见性和有序性的一套规则总结。

解锁前的写操作happens-before加锁后的读操作
package com.tech.jmm.before;

/**
 * 解锁前的写 happens-before 加锁后的读
 * @author lw
 * @since 2021/12/7
 */
public class LockBefore {
    static int x;
    static Object m=new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (m){
                x=10;
            }
        },"t1").start();
        new Thread(()->{
            synchronized (m){
                System.out.println(x);
            }
        },"t2").start();
    }
}

线程t2读取的值为10

volatile变量的写操作 happens-before 其他线程对于该变量的读操作
package com.tech.jmm.before;

/**
 * volatile变量的写操作 happens-before 其他线程对于该变量的读操作
 * @author lw
 * @since 2021/12/7
 */
public class VolatileBefore {
    volatile static int x;

    public static void main(String[] args) {
        new Thread(()->{
            x=10;
        },"t1").start();
        new Thread(()->{
            System.out.println(x);
        },"t2").start();
    }
}

线程t2读取的值为10

线程start前变量的写,happens-before,该线程中的读
package com.tech.jmm.before;

/**
 * 线程start前变量的写 happens-before变量的读
 * @author lw
 * @since 2021/12/7
 */
public class StartBefore {
    static int x=0;
    public static void main(String[] args) {
        x=10;
        new Thread(()->{
            System.out.println(x);
        },"t2").start();
    }
}

线程t2读取的值为10

对一个线程中断前进行共享变量的写,happens-before,所有知道线程被中断的线程中的读取
package com.tech.jmm.before;

/**
 * 对一个线程中断前进行共享变量的写,happens-before,所有知道线程被中断的线程中的读取
 * @author lw
 * @since 2021/12/7
 */
public class InterruptBefore {
    static int x=0;

    public static void main(String[] args) {
        Thread t2 = new Thread(() -> {
            while (true){
                if(Thread.currentThread().isInterrupted()){
                    System.out.println(x);
                    break;
                }
            }
        }, "t2");
        t2.start();

        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            x=10;
            t2.interrupt();
        }, "t1");
        t1.start();
        
        while (!t2.isInterrupted()){
            Thread.yield();
        }
        System.out.println(x);
    }
}

线程t2 main读取的值都为10

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值