Java关键字-volatile详解

点击 Mr.绵羊的知识星球 解锁更多优质文章。

目录

一、介绍

1. 简介

2. 特性

二、实际应用

1. 案例一


一、介绍

1. 简介

    volatile是java关键字,同时也是JVM 提供的轻量级的同步机制。

2. 特性

    你需要先了解一下Java内存模型Java Memory Model (JMM详解,写完上传),而volatile关键字拥有以下特性(不保证原子性),也就是说他无法保证线程安全。

    (1) 保证可见性:git地址

    可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * VolatileVisibility
 * 保证可见性验证
 *
 * @author wxy
 * @date 2023-02-23
 */
public class VolatileVisibility extends AbstractVolatile {
    private static final Logger LOGGER = LoggerFactory.getLogger(VolatileVisibility.class);

    public static void main(String[] args) {
        // 验证可见性
        visibility();
    }

    public static void visibility() {
        Count count = new Count();
        // 将num初始值设置为0
        count.num = 0;

        new Thread(() -> {
            // 线程1休眠3秒
            sleep(3000);
            // 休眠3秒后, 将num + 1
            count.add(1);
        }, "thread 1").start();

        new Thread(() -> {
            // 循环
            while (true) {
                if (count.getNum() == 1) {
                    // 如果线程2获取到线程1修改完成的num, 则打印后跳出while循环
                    LOGGER.info("current num value: {}", count.getNum());
                    break;
                }
            }
        }, "thread 2").start();

        // main线程等待5秒后结束, 你也可以使用其他的方式等待上面线程执行完毕。例如CountdownLatch...之后文章都会介绍
        sleep(5000);
        LOGGER.info("thread main end");
    }
}

    a. 未加volatile关键字

    上述代码中线程2先获取num=0的值,由于不满足if (count.getNum() == 1)的条件,所以无法跳出循环。三秒钟之后线程1将num的值修改为1,但是线程2无法感知将一直处于循环状态,导致程序无法停止。

6a2ccd21d4d14c599bf08ef66bc47f9c.png

    b. 加了volatile关键字

    上述代码中线程2先获取num=0的值,由于不满足iif (count.getNum() == 1)的条件,所以处于循环状态。三秒钟之后线程1将num的值修改为1,因为num被volatile修饰,具有可见性,num=1的修改对线程2可见,满足条件,所以线程2跳出循环,程序停止。

Java关键字-volatile详解_第1张图片

    (2) 不保证原子性:git地址

    无法保证变量同时只能被一个线程修改,也就是无法保证线程安全。(代码可以不看,就是多个线程操作同一个共享变量)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * VolatileNonAtomicity
 * 不保证原子性验证
 *
 * @author wxy
 * @date 2023-02-23
 */
public class VolatileNonAtomicity extends AbstractVolatile {
    private static final Logger LOGGER = LoggerFactory.getLogger(VolatileNonAtomicity.class);

    public static void main(String[] args) {
        // 验证不保证原子性
        nonAtomicity();
    }

    public static void nonAtomicity() {
        Count count = new Count();

        for (int num = 0; num < 10; num++) {
            // 创建10个线程
            new Thread(() -> {
                for (int index = 0; index < 20; index++) {
                    // 循环十次, 每次循环+1
                    count.add(1);
                    // 休眠1毫秒(我电脑性能太好, 不休眠不好看结果)
                    sleep(1);
                }
            }).start();
        }
        // main线程休眠3秒等待上面执行完毕, 你也可以使用其他的方式等待上面线程执行完毕。例如CountdownLatch...之后文章都会介绍
        sleep(3000);
        LOGGER.info("current num value: {}", count.getNum());
    }
}

    循环了200次,实际结果应该是200,但是最后结果是175。

2064d6f2c3874cdba4c5baa114d94ca7.png

    (3) 禁止指令重排序:git地址

    计算机在执行程序时,为了提高性能,编译器和处理器通常都会对指令做重排序。这会产生什么样的问题呢?

    多线程环境下,由于编译器优化重排,导致不同线程中使用的变量不能保证一致性,无法确定执行的结果。volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * VolatileInstructReset
 * 禁止指令重排序验证
 *
 * @author wxy
 * @date 2023-02-23
 */
public class VolatileInstructReset extends AbstractVolatile {
    private static final Logger LOGGER = LoggerFactory.getLogger(VolatileInstructReset.class);

    /* 未使用volatile关键字前: n次循环后a,b同时为0 */
    private static int a;
    private static int b;
    private static int x;
    private static int y;

    /* 增加volatile关键字后: a,b都符合不同时为0的预期 */
    /*private static volatile int a;
    private static volatile int b;
    private static volatile int x;
    private static volatile int y;*/

    public static void main(String[] args) throws InterruptedException {
        // 指令重排
        instructReset();
    }

    public static void instructReset() throws InterruptedException {
        long startTime = System.currentTimeMillis();

        for (; ; ) {
            /* 定义四个数组, 每个里面仅有一个元素并且初始值为0 */
            a = 0;
            b = 0;
            x = 0;
            y = 0;

            Thread thread1 = new Thread(() -> {
                // 将x赋值为1, 执行后x=1
                x = 1;
                // 将a赋值为y, 如果线程1先执行那么a=0, 如果线程2先执行那么a=1
                a = y;
            }, "线程1");

            Thread thread2 = new Thread(() -> {
                // 将y赋值为1, 执行后y=1
                y = 1;
                // 将b赋值为x, 如果线程1先执行那么b=1, 如果线程2先执行那么b=0
                b = x;
            }, "线程2");
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            if (a == 0 && b == 0) {
                // 理论上不管是先执行线程1还是先执行线程2, a[0] & b[0]不可能同时为0
                // 实际上确有同时为0的可能
                LOGGER.info("a: {}, b: {}", a, b);
                break;
            }
        }

        LOGGER.info("execute end, cost time: {}", System.currentTimeMillis() - startTime);
    }
}

    a. 未加volatile关键字

    按照代码中的逻辑,a和b不可能同时为0。但是在多线程下,a和b竟然同时为0,说明在多线程环境下,指令重排使得变量执行或赋值顺序被修改。

Java关键字-volatile详解_第2张图片

    b. 加了volatile关键字

    测试了两次,每次大概四十分钟(手动停了),a和b都不同时为0,说明多线程环境下,使用volatile关键字修饰后,执行或赋值顺序没有被修改。

d3a68ffe8d7748489b155c0d28652b3a.png

二、实际应用

1. 案例一

   Java单例设计模式(懒汉式)

 

 

你可能感兴趣的:(Java,java)