并发编程的可见性问题
多线程访问共享变量,造成线程不安全,最后的数值不对
public class VDemo {
private static int num = 0;
public static void add() {
num++;
}
public static void main(String[] args) {
//理论上应该为20000
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("执行结果:"+num);
}
}
执行结果:13696
为什么出现这种问题?
是由于多个线程访问同一个变量,第一个值为0,加了1,第二个又迅速的访问,发现读取到的值依旧是0,没有获取到最新值,导致读取到的永远是旧值,这就造成了 这种问题,正常的数字应该是20000,然而少了很多。
解决方法
synchronized
public class VDemo {
private static int num = 0;
public static void add() {
num++;
}
public static void main(String[] args) {
//理论上应该为20000
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
//加锁来保证原子性,锁的是当前线程.class
synchronized (Thread.class) {
for (int j = 1; j <= 1000; j++) {
add();
}
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("执行结果:"+num);
}
}
执行结果:20000
Lock
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class VDemo {
private static int num = 0;
public static void add() {
num++;
}
// Lock 锁,默认为非公平锁
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
//理论上应该为20000
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
try {
//上锁,注意,再此必须try/catch 防止程序出现异常,从而造成死锁问题
lock.lock();
for (int j = 1; j <= 1000; j++) {
add();
}
} catch (Exception e) {
} finally {
// 解锁
lock.unlock();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("执行结果:"+num);
}
}
执行结果:20000
volatile
import java.util.concurrent.atomic.AtomicInteger;
public class VDemo {
//原子类的 AtumicInteger
private volatile static AtomicInteger num = new AtomicInteger();
public static void add() {
//+1 方法 底层使用CAS
num.getAndIncrement();
}
public static void main(String[] args) {
//理论上应该为20000
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("执行结果:"+num);
}
}
执行结果:20000
getAndIncrement()方法:
底层使用了CAS
底层和操作系统挂钩,在内存中修改值 Unsafe 是很特殊的一个类
指令重排
什么是指令重排:你写的程序计算机并不是按照你写的那样去执行的
源代码 --> 编译器优化重排 —> 指令并行也可能会重排 --> 内存系统也可能会重排 —> 执行
处理器在执行指令重排的时候,考虑:数据之间的依赖性
int a = 1; // 1
int b = 2; // 2
a = a + 2; // 3
b = a * a; // 4
//执行顺序:1234,通过指令重排后,可能是 2134, 1324
在多线程的情况下指令重排可能会导致结果不一致
volatile可以避免指令重排
内存屏障,CPU指令 ,作用
- 保证特定的操作的执行顺序
- 可以保证某些变量的内存可见性(利用这些特性volatile实现了可见性)
volatile 读 / 写插入内存屏障规则:
- 在每个 volatile 读操作的后面插入 LoadLoad 屏障和 LoadStore 屏障。
- 在每个 volatile 写操作的前后分别插入一个 StoreStore 屏障和一个 StoreLoad 屏障
编辑器不会对带有修饰volatile的属性产生指令重排
Volatile 是可以保证可见性 不能保证原子性,由于内存屏障,可以避免指令重排的现象产生!