Java线程安全之volatile详解

 更新时间:2023年08月26日 09:19:42   作者:not coder  
这篇文章主要介绍了Java线程安全之volatile详解,volatile 的存在,解决了不同内存间拷贝的同步问题,在每一次使用或者修改时候,都去原持有内存中去拿最新的状态,需要的朋友可以参考下

volatile

volatile 的存在,解决了不同内存间拷贝的同步问题,在每一次使用或者修改时候,都去原持有内存中去拿最新的状态

或者说可以这样理解,volatile 的修饰让线程放弃了使用各自内存的做法转而使用共享内存,从而保证了可靠性,但是降低了效率

所以说我们只在需要不同线程访问同一个值的时候才去打开这个限制

第一种情况

首先我们来写一个测试代码

public class Synchronized1Test {
    private boolean running = true;
    public void runTest() {
        new Thread() {
            @Override
            public void run() {
                while (running) {
                    System.out.println("线程开始执行,,,,");
                }
            }
        }.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        stop();
    }
    private void stop() {
        running = false;
    }
}

代码大致要表达的是,主线程在一秒之后要调用 stop 方法将 running = false,这样,子线程的死循环就会被打破,这样子线程停止执行,主线程也执行完毕,整个程序结束。

但是实际的执行结果是,子线程的死循环并不会结束,会一直不停的执行下去

这是因为线程直接做变量的值的修改并不是直接改的,而是先拷贝一份到自己线程的内存区域,更改完之后再适时同步给原持有此值的线程,这样做的目的是提高程序的运行效率

而在这里,主线程持有此变量,子线程在执行时候,将值拷贝了一份到自己的内存中,然后子线程并没有做值的修改,并不存在将值同步给主线程的过程,而此值的持有线程是主线程,主线程在更改完此值之后只会同步给自己,并不会同步给子线程,所以说子线程的值永远为最初拷贝走的值,永远为true,就永远不会停下来了。

有一个关键字 volatile 能解决这个问题

public class Synchronized1Test {
    private volatile boolean running = true;
	.....
}

volatile 的存在,解决了不同内存间拷贝的同步问题,在每一次使用或者修改时候,都去原持有内存中去拿最新的状态,或者说可以这样理解,volatile 的修饰让线程放弃了使用各自内存的做法转而使用共享内存,从而保证了可靠性,但是降低了效率,所以说我们只在需要不同线程访问同一个值的时候才去打开这个限制

再次运行,果然一秒结束

第二种情况

public class Synchronized2Test {
    private int x = 0;
    private void count() {
        x++;
    }
    public void runTest() {
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 1_000_000; i++) {
                    count();
                }
                System.out.println("x 在线程1的最终值" + x);
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 1_000_000; i++) {
                    count();
                }
                System.out.println("x 在线程2的最终值" + x);
            }
        }.start();
    }
}

从代码来看,两个线程分别对同一个数进行一百万次从自增操作,无论谁最后执行完成,理论上来讲,总有一个线程打印出来的结果是两百万,但实际的运行效果,无论执行多少次都没有两百万的结果出来。

都是因为上文说的,值的同步性问题,在各自内存区域修改完后适时同步回去,导致的这个问题

然后我们加上 volatile 关键字

public class Synchronized2Test {
    private volatile int x = 0;
	.....
}

运行,还是不行,为啥啊,都加上了怎么还是不行呢?

这里主要的原因是因为 x++; 这个操作

在 Java 中, x++; 是两步操作,不是原子操作,是可拆的操作

他的运算过程大致相当于是

int temp = x + 1;
	x = temp;

这样,在代码分为两行执行的时候,抢线程的操作就发生了,就出现了值异常的情况

到目前为止,线程安全除了加 volatile 关键字外,还需要保证会互相影响的操作合成一个原子操作

这里解释一下原子操作,简单来说,把多个步骤看成一个整体,要么你别执行我,要么就完全执行,不要在执行一半时候发生线程切换

这里引入我们保证一块代码块执行原子操作的关键字:synchronized

这时候我们改一下我们的代码,给 count 方法增加 synchronized 关键字,并且可以将 volatile 关键字去除了

public class Synchronized2Test {
    private int x = 0;
    private synchronized void count() {
        x++;
    }
    .....
}

运行,果然顺利的打印出来了两百万,多次运行也是这个结果

受保护的类型

到此,我们讲一下官方提供的原子型的类型,这里拿之前出现过的 AtomicInteger 举例

AtomicInteger 对标的就是 int,他是对 int 的包装,可以保证 int 具有同步性和原子性

这里的同步性可以理解为上文提到的被 volatile 修饰,原子性可以理解为被 synchronized 修饰

例:

// 可以理解为默认值为 0 的 int
AtomicInteger count  = new AtomicInteger(0);
// 相当于是 ++count
count.incrementAndGet();
// 相当于是 count++
count.getAndIncrement();

代码中的 count.incrementAndGet() 相当于是 ++count

count.getAndIncrement() 相当于是 count++

除此之外还有

常规类型保护类型
intAtomicInteger
booleanAtomicBoolean
int[]AtomicIntegerArray
TAtomicReference< T>

不一一列举

其中要说的就是 AtomicReference,他把传入的范型,自动帮我们做成了保护类型,使得在赋值和取值时候不出错

到此这篇关于Java线程安全之volatile详解的文章就介绍到这了,更多相关Java的volatile内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Mybatis增强版MyBatis-Flex的具体使用

    Mybatis增强版MyBatis-Flex的具体使用

    Mybatis-Flex一个用于增强MyBatis的框架,本文主要介绍了Mybatis增强版MyBatis-Flex的具体使用,具有一定的参考价值,感兴趣的可以了解一下
    2024-06-06
  • 详解Java数字签名提供XML安全

    详解Java数字签名提供XML安全

    在本篇文章中我们给大家整理了关于Java数字签名提供XML安全的知识点内容,有需要的朋友们可以学习下。
    2018-08-08
  • Java 集合框架掌握 Map 和 Set 的使用(内含哈希表源码解读及面试常考题)

    Java 集合框架掌握 Map 和 Set 的使用(内含哈希表源码解读及面试常考题)

    这篇文章主要介绍了Java 集合框架掌握 Map 和 Set 的使用并含有内含哈希表源码解读及面试常考题, Map 和 Set 是一种适合动态查找的集合容器或者数据结构下面文章详细介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2021-12-12
  • Spring Boot中slf4j日志依赖关系示例详解

    Spring Boot中slf4j日志依赖关系示例详解

    在项目开发中,记录日志是必做的一件事情。而当我们使用Springboot框架时,记录日志就变得极其简单了。下面这篇文章主要给大家介绍了关于Spring Boot中slf4j日志依赖关系的相关资料,需要的朋友可以参考下
    2018-11-11
  • Spring Boot 中 MyBatis 与 Spring Data JPA 的优缺点对比

    Spring Boot 中 MyBatis 与 Spring 

    这篇文章主要介绍了Spring Boot 中 MyBatis 与 Spring Data JPA 的优缺点对比,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2025-05-05
  • SpringBoot登录、退出、获取用户信息的session处理方案

    SpringBoot登录、退出、获取用户信息的session处理方案

    这篇文章主要介绍了SpringBoot登录、退出、获取用户信息的session处理,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-08-08
  • 深入理解Java注解类型(@Annotation)

    深入理解Java注解类型(@Annotation)

    这篇文章主要介绍了深入理解Java注解类型(@Annotation),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05
  • Java SE使用数组实现高速数字转换功能

    Java SE使用数组实现高速数字转换功能

    随着大数据时代的到来,数字转换功能变得越来越重要,在Java开发中,数字转换功能也是经常用到的,下面我们就来学习一下如何使用Java SE数组实现高速的数字转换功能吧
    2023-11-11
  • 一文详解如何使用Java打造更优雅的AI代理

    一文详解如何使用Java打造更优雅的AI代理

    在人工智能(AI)代理开发的浪潮中,Python似乎一直是默认的编程语言选择,本文将深入探索Java如何以更少的代码和更高的可扩展性创造出更高效,更优雅的AI代理,希望对大家有所帮助
    2025-10-10
  • Java类的初始化顺序的实现

    Java类的初始化顺序的实现

    类的初始化过程是在类加载阶段完成的,包括加载、验证、准备、解析和初始化等步骤,本文主要介绍了Java类的初始化顺序的实现,感兴趣的可以了解一下
    2023-12-12

最新评论