我们都知道在Java中i++操作是线程不安全的操作,虽然他看起来只有一行,像是只有一步操作,其实可以拆分开出来三步:
第一步:从内存中读出值i;
第二步:计算i的值;
第三步:将计算得到的i的值替换内存中原有的i;
但是以上的3个步骤不是一个原子操作,所以就会出现线程安全的问题,看下面的例子:
package com.ck.prefix;
import java.util.concurrent.CountDownLatch;
public class TestPlus{
private volatile int i;
public TestPlus(int i){
this.i = i;
}
public int getI() {
return i;
}
public void increment() {
i++;
}
public static void main(String[] args) throws InterruptedException {
//为了主线程等待子线程全部执行结束
final CountDownLatch latch = new CountDownLatch(10);
final TestPlus testPlus = new TestPlus(0);
for (int i = 0; i < 10 ; i++) {
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 2000; i++) {
testPlus.increment();
}
latch.countDown();
}
}).start();
}
//执行子线程执行结束
latch.await();
System.out.println(testPlus.getI());
}
}
我们期望的值是10*2000=20000,可是实际输出却是小于20000,;
我们可以对increment方法进行改造,加上synchronized关键字,如下所示:
public synchronized void increment() {
i++;
}
然后再多次运行,发现结果一直都是我们期望的结果20000,我们都知道synchronized关键字的作用,作用在方法上就是对这个方法执行线程同步的操作,从这点更可以证实i++操作不是一个线程安全操作;
那除了在方法上加synchronized就没有别的方法替代吗?当然是有的,这就是我们今天的主角AtomicInteger:
改造上面的程序,改用AtomicInteger实现自增:
package com.ck.prefix;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class TestPlus{
private AtomicInteger i;
public TestPlus(AtomicInteger i){
this.i = i;
}
public int getI() {
return i.get();
}
public void increment() {
i.addAndGet(1);
}
public static void main(String[] args) throws InterruptedException {
//为了主线程等待子线程全部执行结束
final CountDownLatch latch = new CountDownLatch(10);
final TestPlus testPlus = new TestPlus(new AtomicInteger(0));
for (int i = 0; i < 10 ; i++) {
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 2000; i++) {
testPlus.increment();
}
latch.countDown();
}
}).start();
}
//执行子线程执行结束
latch.await();
System.out.println(testPlus.getI());
}
}
输出结果永远是20000,说明addAndGet方法是线程安全的方法,AtomicInteger底层的实现架构就是CAS原理;
什么是CAS:
CAS全称:compare and swap (比较并交换)它是一个原子操作:
理论:简单来说就是先把内存中的共享变量的值取出来做操作,例如自增操作i++,做完自增后再检查内存中的值是否被别的线程修改过,如果没有修改过,就将修改过的值写回到内存,否则表示有别的修改过共享变量的值,执行失败;注意:判断内存的值是否被别的线程修改过并同时写回新值是一个线程安全的操作,乐观锁的底层实现也是CAS的思想,乐观的认为永远只有一个线程在操作共享变量,先对共享变量做修改,修改完成后再判断没有别的线程修改共享变量的值,如果没有就才进行后续操作;
AtomicInteger的ABA问题,因为我们CAS的时候是判断的内存中的值有没有被修改,可是内存中的值有可能被多个线程修改但是最后的值却没变的情况,例如内存中的值一开始是A,第一个线程读取到值是A,第二个线程将A改成了B,然后第三个线程又将B改成了A,对于第一个线程来说好像值没有改变一样,还是可以进行CAS操作,但是其实值已经变了2次,对于ABA问题,我们的思路是可以增加版本号来解决这个ABA问题,每次修改都将版本号的值自增,这样进行CAS的时候我们不但要判断内存的值还要判断版本的值是否改变,如果内存的值没变但是版本号变了说明出现了ABA问题,JDK为我们提供了理论实现AtomicStampedReference,可以解决ABA问题,示例如下:
package com.ck.prefix;
import java.util.concurrent.atomic.AtomicStampedReference;
public class Test {
public static void main(String[] args) {
final AtomicStampedReference<Integer> momey = new AtomicStampedReference(1,0);
Thread t1 = new Thread(new Runnable() {
public void run() {
Integer m = momey.getReference();
//stamp相当于版本号,每次修改都加1
int version = momey.getStamp();
try {
//这里睡1s让干扰线程先执行
Thread.sleep(1000);
//更新失败表示版本号起作用了
if(momey.compareAndSet(m, m+1, version,version +1)){
System.out.println("出现了ABA问题");
}else {
System.out.println("没出现ABA问题");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//干扰线程,模拟ABA问题
Thread t2 = new Thread(new Runnable() {
public void run() {
momey.compareAndSet(1,2,momey.getStamp(),momey.getStamp()+1);
momey.compareAndSet(2,1,momey.getStamp(),momey.getStamp()+1);
}
});
t1.start();
t2.start();
}
}
结束!