场景:i++是线程安全的吗?
首先看段代码:
public class Test {
static Integer num = 0;
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 100; j++) {
num++;
}
});
thread.start();
}
System.out.println(num);
}
}
100个线程同时操作全局变量num,每个线程都对num进行100次循环的++操作,理论上最后的结果是10000,实际上却不是。
输出结果:
7385
原因:
Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内存中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。如下图所示:
- 线程之间的共享变量存储在主内存(Main Memory)中
- 每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。
- 从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
- Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。
简单来说就是每个线程之间都有一个私有的内存。在这个例子中,0线程读取了之后,++操作可能在私有的内存中进行,并不会写入主存,此时1线程开启,读取的还是主内存中的值。
参考来源及推荐阅读:Java内存模型(JMM)总结
解决方案
- 对 i++ 操作的方法加同步锁,同时只能有一个线程执行 i++ 操作(效率慢);
- 将num变成局部变量(可能不满足部分需求);
- 使用Atomic原子类(效率比1要高);
有些同学可能了解过volatile关键字,那么在这个场景中==volatile关键字并不能起到作用==。
先了解一下volatile的作用:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
加上这个关键字之后,它会强制将对缓存的修改操作立即写入主存,如果是写操作,会导致其他CPU中对应的缓存无效。
看一下效果
public class Test {
static volatile Integer num = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 100; j++) {
num++;
}
});
thread.start();
}
System.out.println(num);
}
}
结果:
9022
原因:
假设当前值是100
- 0线程读取100,进行++操作(未写入)
- 1线程读取100,进行++操作,此时0线程写入主存(此时主存的值是101)
- 1线程写入主存(此时主存被1线程写入后还是101)
也就是说,多个线程同时读取这个共享变量的值,就算保证其他线程修改的可见性,也不能保证线程之间读取到同样的值然后相互覆盖对方的值的情况。
这时候AtomicInteger就登场了,先看代码。
public class Test {
static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 100; j++) {
num.incrementAndGet();
}
});
thread.start();
}
//主线程运行到这一步的时候,子线程可能还没结束,所以先让主线程休眠一段时间再输出
System.out.println(num);
Thread.sleep(1000);
System.out.println(num);
}
}
结果:
9900
10000
AtomicInteger
概念
AtomicInteger 是一个 Java concurrent 包提供的一个原子类,通过这个类可以对 Integer 进行一些原子操作。这个类的源码比较简单,主要是依赖于 sun.misc.Unsafe 提供的一些 native 方法保证操作的原子性。使用CAS操作具体实现。
CAS
首先我们来简单理解一下CAS的概念。
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
AtomicInteger源码
先看下AtomicInteger的成员变量和静态代码块
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
AtomicInteger 主要有两个静态变量和一个成员变量,在初始化的时候会通过静态代码块给 valueOffset 赋值。
-
Unsafe 的 objectFieldOffset 方法可以获取成员属性在内存中的地址相对于对象内存地址的偏移量。说得简单点就是找到这个成员变量在内存中的地址,便于后续通过内存地址直接进行操作。
-
valueOffset 其实就是用来定位 value,后续 Unsafe 类可以通过内存地址直接对 value 进行操作。
-
value就是初始化时用于存放实际数值。
再看下例子中使用的方法incrementAndGet()
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
再看下getAndAddInt()方法
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
调用了getAndAddInt,继续往下看
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
这时候会发现它里面又调用了 getIntVolatile 和 compareAndSwapInt 方法,而这两个方法都是 native 方法,具体说明可以参照 Unsafe 的 API 文档。
getIntVolatile 的主要作用是通过对象 var1 和成员变量相对于对象的内存偏移量 var2 来直接从内存地址中获取成员变量的值,所以 var5 就是当前 AtomicInteger 的值。
compareAndSwapInt的主要逻辑如下:
- 通过对象 var1 和成员变量的内存偏移量 var2 来定位内存地址
- 判断当前地址的值是否等于 var5
不等于:返回 false
等于:把当前地址的值替换成 var5 + var4 并返回 true
所以,综合来说,getAndAddInt 方法的主要逻辑如下:
- 根据对象 var1 和内存偏移量 var2 来定位内存地址,获取当前地址值
- 循环通过 CAS 操作更新当前地址值直到更新成功
- 返回旧值
网友评论