1.保证i++在多线程下的安全
并发对一个数进行++
操作会导致结果不准确,因为++
操作本身并不是原子性的,所以在多线程下会出现问题。
/**
* @author 二十
* @since 2021/8/26 8:53 上午
*/
public class Demo {
private static volatile int count = 0;
private static int threadSize = 100;
private static CountDownLatch countDownLatch = new CountDownLatch(threadSize);
private static void request() {
try {
TimeUnit.MICROSECONDS.sleep(5);
/**
* 出现问题的原因:count++分为三步操作,
* count->栈 A
* B=A+1
* count=B
*/
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 10; j++) {
request();
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("count:" + count);
}
}
通过加锁的方式可以保证数据的准确性,但是如果直接在request()
加锁,效率会很低,因为这样锁的粒度比较大。
分析一下count++
操作:
- count的值赋值给操作数栈内的a
- 将操作数栈内的a+1并赋值给b
- 将操作数栈内b的值赋值给count
并发条件下出现错误的原因就是因为假如a线程在操作一半的过程中(1-3之间),线程b来获取count的值进行++操作,就会获取到不准确的count值。可以控制在第三步加锁,在线程将自己操作数栈内的结果赋值给count之前,先比较当前的最新count和当前线程操作数栈内拷贝的count副本值是否一致,如果一致,则当前线程可以将自己操作数栈的结果赋值给count,否则重新获取count值进行操作,直到成功。
/**
* @author 二十
* @since 2021/8/26 8:53 上午
*/
public class Demo {
private static volatile int count = 0;
private static int threadSize = 100;
private static CountDownLatch countDownLatch = new CountDownLatch(threadSize);
private static void request() {
try {
TimeUnit.MICROSECONDS.sleep(5);
/**
* 出现问题的原因:count++分为三步操作,
* count->栈 A
* B=A+1
* count=B
*/
// count++;
int e;
while (!compareAndSwap(e = getCount(), e + 1)) {
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static int getCount() {
return count;
}
private static synchronized boolean compareAndSwap(int e, int n) {
if (getCount() == e) {
count = n;
return true;
}
return false;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 10; j++) {
request();
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("count:" + count);
}
}
2.Java对cas的支持
2.1 cas概念
CAS 全称“CompareAndSwap”,中文翻译过来为“比较并替换”
CAS操作包含三个操作数————内存位置(V)、期望值(A)和新值(B)。
- 如果内存位置的值与期望值匹配,那么处理器会自动将该位置值更新为新值。
- 否则,处理器不作任何操作。
- 无论哪种情况,它都会在CAS指令之前返回该位置的值。(CAS在一些特殊情况下仅返回CAS是否成功,而不提取当前值)
- CAS有效的说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改。
2.2 jdk中对cas提供的支持
java中提供了对CAS操作的支持,具体在sun.misc.unsafe类中,声明如下:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
参数var1 | 表示要操作的对象 |
---|---|
参数var2 | 表示要操作对象中属性地址的偏移量 |
参数var4 | 表示需要修改数据的期望的值 |
参数var5 | 表示需要修改为的新值 |
2.3 cas实现原理
CAS通过调用JNI的代码实现,JNI:java Native Interface
,允许java调用其它语言。而compareAndSwapxxx
系列的方法就是借助“C语言”来调用cpu底层指令实现的。
以常用的Intel x86平台来说,最终映射到的cpu的指令为“cmpxchg
”,这是一个原子指令,cpu执行此命令时,实现比较并替换的操作!
2.4 cmpxchg怎么保证多核心下的线程安全?
系统底层进行CAS操作的时候,会判断当前系统是否为多核心系统,如果是就给“总线”加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行CAS操作,也就是说CAS的原子性是平台级别的!
2.5 cas存在的问题
1)ABA问题
线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题。
举例:一个小偷,把别人家的钱偷了之后又还了回来,还是原来的钱吗,你老婆出轨之后又回来,还是原来的老婆吗?ABA问题也一样,如果不好好解决就会带来大量的问题。最常见的就是资金问题,也就是别人如果挪用了你的钱,在你发现之前又还了回来。但是别人却已经触犯了法律。
2)循环时间长开销大
3)只能保证一个共享变量的原子操作
2.6 ABA问题演示
/**
* @author 二十
* @since 2021/8/26 2:15 下午
*/
public class Aba {
static CountDownLatch countDownLatch = new CountDownLatch(2);
static AtomicInteger i = new AtomicInteger(1);
public static void main(String[] args) {
new Thread(() -> {
int e = Aba.i.get();
int n = e + 1;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
boolean flag = i.compareAndSet(e, n);
System.out.println("flag = " + flag);
countDownLatch.countDown();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
i.incrementAndGet();
i.decrementAndGet();
countDownLatch.countDown();
}).start();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.7 jdk如何解决ABA问题
解决ABA问题其实很简单,只需要加一个版本号,每次操作,版本号都会加1,每次比较交换的时候,把版本号也比较一下,这样就能确保,这个数据是正确的。
AtomicStampReference主要包含一个对象引用及一个可以自动更新的整数“stamp
”的pair
对象来解决ABA问题。
/**
* @author 二十
* @since 2021/8/26 2:29 下午
*/
public class AtomicStampReferenceTest {
static CountDownLatch countDownLatch = new CountDownLatch(2);
static AtomicStampedReference<Integer> i = new AtomicStampedReference(1, 1);
public static void main(String[] args) {
new Thread(() -> {
int e = Aba.i.get();
int n = e + 1;
int ev = i.getStamp();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
boolean flag = i.compareAndSet(e, n, ev, ev + 1);
System.out.println("flag = " + flag);
countDownLatch.countDown();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
i.set(i.getReference() + 1, i.getStamp() + 1);
i.set(i.getReference() - 1, i.getStamp() + 1);
countDownLatch.countDown();
}).start();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}