Java基础、中级、高级、架构面试资料

Java 线程安全的3大核心:原子性、可见性、有序性

JAVA herman 3235浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog2,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领
腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云

有人将原子性、可见性、有序性归结为 java 多线程的3大核心。我认为欠佳,应该把它归为线程安全的知识点。本文我就给大家详细的说下,线程安全的3个核心知识点:原子性、可见性、有序性。

在开始之前我们先来看看什么是线程安全?

线程安全

线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题。

在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。

比如下面的代码,在多线程环境中,可能就会出现线程不安全的问题:

xttblog ++;

想象下线程A和B同时执行同一个这段代码,我们无法知道操作系统何时会在两个线程之间切换。JVM并不是将这段代码视为单条指令来执行的。

++ 操作不是一个原子性的操作,同样的 — 操作也不是原子性的操作。

原子性

原子性是指操作是不可分的。其表现在于对于共享变量的某些操作,应该是不可分的,必须连续完成。

比如上面的 ++ 操作,实际上 JMM 会分 3 步来完成。

  1. 读取变量 xttblog 的值
  2. xttblog 的值+1
  3. 将值赋予变量 xttblog

这三个操作中任何一个操作过程中,xttblog 的值被人篡改,那么都会出现我们不希望出现的结果。所以我们必须保证这是原子性的。

所以想要实现 ++ 这样的原子操作就需要用到 synchronized 或者是 lock 进行加锁处理。

如果是基础类的自增操作可以使用 AtomicInteger 这样的原子类来实现(其本质是利用了 CPU 级别的 的 CAS 指令来完成的)。

可见性

可见性是指一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。

为什么会出现这种问题呢?

我们知道,java 线程通信是通过共享内存的方式进行通信的,而我们又知道,为了加快执行的速度,线程一般是不会直接操作内存的,而是操作缓存。

java线程内存模型:

java 线程内存模型实际上,线程操作的是自己的工作内存,而不会直接操作主内存。如果线程对变量的操作没有刷新写入到主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的。而如果另一个变量没有读取主内存中最新的值,而是使用旧的值的话,同样的也可以列为不可见。(可以理解数据库中的幻读,脏读)

对于jvm来说,主内存是所有线程共享的java堆,而工作内存中的共享变量的副本是从主内存拷贝过去的,是线程私有的局部变量,位于java栈中。

那么我们怎么知道什么时候工作内存的变量会刷写到主内存当中呢?

volatile 关键字就是用于保证内存可见性,当线程A更新了 volatile 修饰的变量时,它会立即刷新到主线程,并且将其余缓存中该变量的值清空,导致其余线程只能去主内存读取最新值。

使用 volatile 关键词修饰的变量每次读取都会得到最新的数据,不管哪个线程对这个变量的修改都会立即刷新到主内存。

synchronized和加锁也能能保证可见性,实现原理就是在释放锁之前其余线程是访问不到这个共享变量的。但是和 volatile 相比开销较大。

另外 Java 中还存在 happens-before。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这个以后细说!

有序性

有序性是指程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。

为什么会出现不一致的情况呢?

这是由于重排序的缘故。

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排在单线程中不会出现问题,但在多线程中可能会出现数据不一致的问题。

例如下面这段代码:

int a = 100 ; //1
int b = 200 ; //2
int c = a + b ; //3

正常情况下的执行顺序应该是 1>>2>>3。但是有时 JVM 为了提高整体的效率会进行指令重排导致执行的顺序可能是 2>>1>>3。但是 JVM 也不能是什么都进行重排,是在保证最终结果和代码顺序执行结果一致的情况下才可能进行重排。

重排在单线程中不会出现问题,但在多线程中会出现数据不一致的问题。

Java 中可以使用 volatile 来保证顺序性,synchronized 和 lock 也可以来保证有序性,和保证原子性的方式一样,通过同一段时间只能一个线程访问来实现的。

除了通过 volatile 关键字显式的保证顺序之外, JVM 还通过 happen-before 原则来隐式的保证顺序性。

其中有一条就是适用于 volatile 关键字的,针对于 volatile 关键字的写操作肯定是在读操作之前,也就是说读取的值肯定是最新的。

深入理解 JVM 的一些设计,对你的编程会有莫大的帮助!

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » Java 线程安全的3大核心:原子性、可见性、有序性