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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Springmvc完成ajax功能实例详解

    Springmvc完成ajax功能实例详解

    在本篇文章里小编给大家整理了关于Springmvc完成ajax功能实例内容,有需要的朋友们可以参考学习下。
    2019-09-09
  • Java打包之后读取Resources下的文件失效原因及解决方法

    Java打包之后读取Resources下的文件失效原因及解决方法

    这篇文章主要给大家介绍了Java打包之后读取Resources下的文件失效的问题分析和解决方法,文中通过代码示例和图文结合给大家讲解非常详细,需要的朋友可以参考下
    2023-12-12
  • SpringBoot加载配置文件的实现方式总结

    SpringBoot加载配置文件的实现方式总结

    在实际的项目开发过程中,我们经常需要将某些变量从代码里面抽离出来,放在配置文件里面,以便更加统一、灵活的管理服务配置信息。所以本文将为大家总结一下SpringBoot加载配置文件的常用方式,需要的可以参考一下
    2022-03-03
  • SpringMVC+Mybatis实现的Mysql分页数据查询的示例

    SpringMVC+Mybatis实现的Mysql分页数据查询的示例

    本篇文章主要介绍了SpringMVC+Mybatis实现的Mysql分页数据查询的示例,具有一定的参考价值,有兴趣的可以了解一下
    2017-08-08
  • 简单讲解Java的Socket网络编程的多播与广播实现

    简单讲解Java的Socket网络编程的多播与广播实现

    这篇文章主要介绍了Java的Socket网络编程的多播与广播实现,包括网络编程发送和接受数据的一些基础知识整理,需要的朋友可以参考下
    2016-01-01
  • javaweb文件打包批量下载代码

    javaweb文件打包批量下载代码

    这篇文章主要为大家详细介绍了javaweb文件打包批量下载代码,批量下载未批改作业,感兴趣的小伙伴们可以参考一下
    2016-06-06
  • java:找不到符号报错的排错方案举例

    java:找不到符号报错的排错方案举例

    当你使用一个未定义或未导入的类时,编译器会报错,下面这篇文章主要给大家介绍了关于java:找不到符号报错的排错方案,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-01-01
  • Springboot实现给前端返回一个tree结构方法

    Springboot实现给前端返回一个tree结构方法

    这篇文章主要介绍了SpringBoot返回给前端一个tree结构的实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • 区块链java代码实现

    区块链java代码实现

    这篇文章主要为大家详细介绍了区块链java代码实现,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01
  • SpringBoot中Controller的传参方式详细讲解

    SpringBoot中Controller的传参方式详细讲解

    这篇文章主要介绍了SpringBoot在Controller层接收参数的常用方法,Controller接收参数的常用方式总体可以分为三类,第一类是Get请求通过拼接url进行传递,第二类是Post请求通过请求体进行传递,第三类是通过请求头部进行参数传递,下面我们来详细看看
    2023-01-01

最新评论