- AtomicInteger的使用场景
- AtomicInteger源码分析
- CAS
我们知道在进行i=i+1或者i++、++i操作时是线程不安全的。
下面展示代码 count++
public class MainTest {
private Integer count=0;
public void add()
{
count++;
}
}
经过javap的编译如下:
"Java\jdk1.8.0_211\bin\javap.exe" -c MainTest.class
Compiled from "MainTest.java"
public class com.txs.common.MainTest {
public com.txs.common.MainTest();
Code:
0: aload_0 //将引用变量this压入栈顶
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void add();
Code:
0: iconst_0 //将 数值 0 推送至栈顶。
1: istore_1 //将0存入第二个局部变量 ,此时 count=0
2: iload_1 //将count=0推送至栈顶
3: iconst_1//将 数值1 推送至栈顶
4: iadd //将栈顶数值1和count=0相加并将结果压入栈顶,此时栈顶为1
5: istore_1 //将栈顶数值1存入第二个局部变量,此时count=1
6: return //从当前方法返回 void
}
Process finished with exit code 0
从以上字节码执行过程发现,jvm在执行count++过程大致分为2步骤,第一步先将count设值为0,然后将count加1再赋值为count。
所以如果线程并发执行的话,多个线程都同时从局部变量取count为0,导致实际结果可能小于最终值。
下面展示代码并发执行最终count值小于1000
package com.txs.juc;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* juc 自增加并发测试
*/
public class AtomicIntegerTest {
private static Integer count=0;
private static Integer THREAD_COUNT=1000;
private static CountDownLatch countDownLatch=new CountDownLatch(THREAD_COUNT);
public static void main(String[] args) throws Exception {
for (int i = 0; i < THREAD_COUNT; i++) {
//启动1000个线程
new Thread(new Runnable() {
@Override
public void run() {
add();
countDownLatch.countDown();
}
}).start();
}
//等待所有线程执行结束
countDownLatch.await();
System.out.println(count);
// System.out.println(atomicInteger);
}
public static void add()
{
//普通的++
try {
Thread.sleep(1);//睡眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
执行结果如下:
"D:\Program Files\Java\jdk1.8.0_211\bin\java.exe"
917
Process finished with exit code 0
"D:\Program Files\Java\jdk1.8.0_211\bin\java.exe"
877
Process finished with exit code 0
"D:\Program Files\Java\jdk1.8.0_211\bin\java.exe"
897
Process finished with exit code 0
所以这个时候,怎样保证cout++是线程安全,每次执行都能得到正确的结果呢?
这其中有2种方法,一种是在count++方法上加上同步锁synchronized; 但是这种方法是重量级锁,会导致代码执行比较慢,一旦并发量大,所有浪费的时间都在加锁和解锁上,代码真正执行的时间很短。
第二种方法是使用JUC原子操作Integer类型AtomicInteger方法incrementAndGet来自增操作。
以下是AtomicInteger
方法实现安全自增代码
package com.txs.juc;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* juc 自增加并发测试
*/
public class AtomicIntegerTest {
private static AtomicInteger atomicInteger=new AtomicInteger(0);
private static Integer THREAD_COUNT=1000;
private static CountDownLatch countDownLatch=new CountDownLatch(THREAD_COUNT);
public static void main(String[] args) throws Exception {
for (int i = 0; i < THREAD_COUNT; i++) {
//启动1000个线程
new Thread(new Runnable() {
@Override
public void run() {
addAtomic();
countDownLatch.countDown();
}
}).start();
}
//等待所有线程执行结束
countDownLatch.await();
// System.out.println(count);
System.out.println(atomicInteger);
}
public static void addAtomic()
{
try {
Thread.sleep(1);//睡眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicInteger.incrementAndGet();
}
}
使用AtomicInteger方法执行count++无论怎么执行结果一直是1000
"D:\Program Files\Java\jdk1.8.0_211\bin\java.exe"
1000
Process finished with exit code 0
那么AtomicInteger是怎么既保证线程安全又增加执行效率的呢?
我们查看一下AtomicInteger源码分析一下吧。源码为JDK8下的。
首先我们查看一下AtomicInteger类的变量和静态块代码
*
* @since 1.5
* @author Doug Lea
*/
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
//Unsafe是使用c语言直接操作内存资源的类
//该类主要提供compareAndSwapInt和getIntVolatile方法操作内存数据
private static final Unsafe unsafe = Unsafe.getUnsafe();
//字段value在java内存中的偏移量
private static final long valueOffset;
static {
try {
//objectFieldOffset获取对象AtomicInteger实例字段value的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//value就是要进行操作AtomicInteger方法的值
private volatile int value;
Unsafe是一个不安全的类,因为它可以直接操作java内存资源,Unsafe里面有一些方法直接可以分配内存,但是java内存分配后需要进行释放,如果内存分配使用完不能及时释放会出现内存泄漏问题,所以使用Unsafe方法要注意不要过度使用。
至于什么是字段的偏移量,通过偏移量怎么操作java内存中的值?
可以参考: https://www.jianshu.com/p/cb5e09facfee.
总结一句话是,偏移量就是java对象的实例数据(也就是变量value),在java内存(也就是java堆)中的位置定位。通过偏移量可以获取value在内存中的值。
接下来,我们看看AtomicInteger的自增方法incrementAndGet的源码
/**
* Atomically increments by one the current value.
*
* @return the updated value
* //该方法调用了Unsafe 的getAndAddInt方法
* getAndAddInt方法是循环获取AtomicInteger对象中偏移量为 valueOffset的值并加1,将value值更新并返回value旧值
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
看看unsafe.getAndAddInt方法
//参数1 var1 是要获取内存的对象
//参数2 var2 是内存对象var1的在内存中的偏移量
//参数3 var4 是要加的数值 为1
public final int getAndAddInt(Object var1, long var2, int var4) {
//这是一个do..while循环
int var5;
do {
//getIntVolatile方法获取对象var1中偏移量var2的值,
//强制从内存获取var2偏移量的值,这个方法要求被使用的属性被volatile修饰,而且获取的是Integer类型的值。
var5 = this.getIntVolatile(var1, var2);
//compareAndSwapInt 这个数CAS操作
//操作如下:
//1、var1和var2是用来获取内存中value的值
//2、从内存中获取的值和var5进行比较是否相同
//3、相同,就把内存中value值更新成var5 + var4
//4、不相同,就返回false,继续执行getIntVolatile方法获取内存值赋值给var5,直到值相同
//5、执行成功,并更新内存值为var5 + var4,返回true,跳出循环
//6、最后返回内存旧址
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
什么是CAS? 即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,返回false,处理器不做任何操作。我们都知道,CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。