Volatile不保证原子性及解决方案
原子性的意义
原子性特别是在并发编程领域,是一个极其重要的概念,原子性指的是一个操作或一组操作要么全部执行成功,要么全部不执行,不会出现部分执行的情况。这意味着原子性操作是不可分割的,它们在执行过程中不会被其他操作中断或干扰。
原子性的意义在于它保证了数据的一致性和程序的正确性。在多线程或多进程的环境中,当多个操作同时访问和修改共享数据时,如果没有原子性保证,可能会导致数据不一致或不确定的结果。例如,如果一个线程在读取某个数据时,另一个线程同时修改了这个数据,那么第一个线程读取到的数据可能是不正确的。通过确保操作的原子性,可以避免这种情况,从而维护数据的完整性和程序的正确执行。
了解了上面的原子性的重要概念后,接下来一起聊一聊 volatile 关键字。
volatile 关键字在 Java 中用于确保变量的更新对所有线程都是可见的,但它并不保证复合操作的原子性。这意味着当多个线程同时访问一个 volatile 变量时,可能会遇到读取不一致的问题,尽管它们不会看到部分更新的值。
Volatile 的限制
- 不保证原子性:volatile 变量的单个读写操作是原子的,但复合操作(如自增或同步块)不是原子的。
- 不保证顺序性:volatile 变量的读写操作不会与其他操作(如非 volatile 变量的读写)发生重排序。
一个例子
用一个示例来解释会更清楚点,假如我们有一段代码是这样的:
class Counter {
private volatile int count = 0;
void increment() {
count++;
}
int getCount() {
return count;
}
}
尽管 count 是 volatile 变量,但 increment 方法中的复合操作 count++(读取-增加-写入)不是原子的。因此,在多线程环境中,多个线程可能会同时读取相同的初始值,然后增加它,导致最终值低于预期。
volatile 不保证原子性的代码验证
以下是一个简单的 Java 程序,演示了 volatile 变量在多线程环境中不保证复合操作原子性的问题:
public class VolatileTest {
private static volatile int counter = 0;
public static void main(String[] args) throws InterruptedException {
int numberOfThreads = 10000;
Thread[] threads = new Thread[numberOfThreads];
for (int i = 0; i < numberOfThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
counter++;
}
});
threads[i].start();
}
for (int i = 0; i < numberOfThreads; i++) {
threads[i].join();
}
System.out.println("Expected count: " + (numberOfThreads * 100));
System.out.println("Actual count: " + counter);
}
}
在这个例子中:
- counter 是一个 volatile 变量。
- 每个线程都会对 counter 执行 100 次自增操作。
- 理论上,如果 counter++ 是原子的,最终的 counter 值应该是 10000 * 100。
然而,由于 counter++ 包含三个操作:读取 counter 的值、增加 1、写回 counter 的值,这些操作不是原子的。因此,在多线程环境中,最终的 counter 值通常会小于预期值,这证明了 volatile 变量不保证复合操作的原子性。
解决方案
1. 使用 synchronized 方法或块:
- 将访问 volatile 变量的方法或代码块声明为 synchronized,确保原子性和可见性。
class Counter {
private volatile int count = 0;
synchronized void increment() {
count++;
}
synchronized int getCount() {
return count;
}
}
2. 使用 AtomicInteger 类:
java.util.concurrent.atomic 包中的 AtomicInteger 提供了原子操作,可以替代 volatile 变量。
import java.util.concurrent.atomic.AtomicInteger;
class Counter {
private AtomicInteger count = new AtomicInteger(0);
void increment() {
count.incrementAndGet();
}
int getCount() {
return count.get();
}
}
3. 使用锁(如 ReentrantLock):
使用显式锁(如 ReentrantLock)来同步访问 volatile 变量的代码块。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private volatile int count = 0;
private final Lock lock = new ReentrantLock();
void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
使用volatile变量的正确使用场景
如果操作是简单的读写,并且你只需要保证可见性,可以使用 volatile。但对于复合操作,可以使用上述其他方法来实现,通过这些方法,可以确保在多线程环境中对共享资源的正确同步和可见性。
Volatile不保证原子性及解决方案的更多相关文章
- 为什么volatile不能保证原子性而Atomic可以?
在上篇<非阻塞同步算法与CAS(Compare and Swap)无锁算法>中讲到在Java中long赋值不是原子操作,因为先写32位,再写后32位,分两步操作,而AtomicLong赋值 ...
- volatile之一--volatile不能保证原子性
Java语言是支持多线程的,为了解决线程并发的问题,在语言内部引入了 同步块 和 volatile 关键字机制在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这 ...
- 【转】为什么volatile不能保证原子性而Atomic可以?
直接上好文链接!!! 为什么volatile不能保证原子性而Atomic可以?
- 为什么volatile不能保证原子性?
为什么volatile能替代简单的锁,却不能保证原子性?这里面涉及volatile,是java中的一个我觉得这个词在Java规范中从未被解释清楚的神奇关键词,在Sun的JDK官方文档是这样形容vola ...
- Java并发编程之验证volatile不能保证原子性
Java并发编程之验证volatile不能保证原子性 通过系列文章的学习,凯哥已经介绍了volatile的三大特性.1:保证可见性 2:不保证原子性 3:保证顺序.那么怎么来验证可见性呢?本文凯哥(凯 ...
- Volatile不保证原子性(二)
Volatile不保证原子性 前言 通过前面对JMM的介绍,我们知道,各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后在写回到主内存中的. 这就可能存在一个线程AAA修改 ...
- Java线程-volatile不能保证原子性
下面是一共通过volatile实现原子性的例子: 通过建立100个线程,计算number这个变量最后的结果. package com.Sychronized; public class Volatil ...
- volatile不能保证原子性
1.看图自己体会 2.体会不了就给你个小程序 package cs.util; public class VolatileDemo { private volatile int count =0; p ...
- JUC 并发编程--05, Volatile关键字特性: 可见性, 不保证原子性,禁止指令重排, 代码证明过程. CAS了解么 , ABA怎么解决, 手写自旋锁和死锁
问: 了解volatile关键字么? 答: 他是java 的关键字, 保证可见性, 不保证原子性, 禁止指令重排 问: 你说的这三个特性, 能写代码证明么? 答: .... 问: 听说过 CAS么 他 ...
- 为什么volatile能保证有序性不能保证原子性
对于内存模型的三大特性:有序性.原子性.可见性. 大家都知道volatile能保证可见性和有序性但是不能保证原子性,但是为什么呢? 一.原子性.有序性.可见性 1.原子性: (1)原子的意思代表着-- ...
随机推荐
- [第一届长城杯]-ez-python
----这道题似曾相识,和某刃的题目不能说不同,简直就一摸一样 ----考点:pickle序列化问题,命令执行 ----正常访问网站,源代码F12给出?pic=的提示,这种大致一看就是可以任意文件访问 ...
- JDK动态代理的深入理解
引入代理模式 代理模式是框架中经常使用的一种模式,动态代理是AOP(面向切面编程)思想的一种重要的实现方式,在我们常用的框架中也经常遇见代理模式的身影,例如在Spring中事务管理就运用了动态代理,它 ...
- 手写Word2vec算法实现
1. 语料下载:https://dumps.wikimedia.org/zhwiki/latest/zhwiki-latest-pages-articles.xml.bz2 [中文维基百科语料] 2. ...
- Vue3简单项目流程分享——工作室主页
Vue3简单项目流程分享--工作室主页 零.写在最前 以下是项目相关的一些链接: 源代码GitHub仓库(需要魔法上网):仓库 网页示例(需要魔法上网):网页示例 UI图(来源@设计师杨贺):Mast ...
- ts的keyof
keyof 是 TypeScript 中的一种类型操作符,用于获取一个类型的所有键(属性名)作为联合类型.它的语法如下: type KeysOfType = keyof ObjectType; 其中: ...
- WPS WORD EXCEL 不合并显示
WPS WORD EXCEL 不合并显示 版本:WPS 12 , 下载时间约是2023 年. 1.在开始菜单里找到 WPS OFFICE - 配置工具 2.点击"高级(A)". 3 ...
- C#.NET 逐行读取TXT文本
C#.NET 逐行读取TXT文本 using System; using System.IO; class Program { static void Main() { string filePath ...
- 590. N 叉树的后序遍历 | Javascript 递归实现
题目 题目链接:590. N 叉树的后序遍历 解题思路 递归后续遍历,正常的思路 然后有一个要注意的地方就是如果js定义了全局变量来存储结果,每次调用函数之前一定要记得清空,否则答案会带上之前的结果. ...
- Go 语言中的异常处理简单实践 panic、recover【GO 基础】
〇.Go 中的异常处理简介 Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误. panic.recover 参数类型为 interface{},因此可抛出任何类型对象 ...
- python解析字符串中的省市区字符串
#python解析字符串中的省市区字符串 #str4 = "XX省XX市辛桥乡赵庄村XX号" #str4 = "XX省XX市XX区八卦二路XX号XX栋XX楼" ...