a++/++a 为什么不是线程安全的,我们通过一个例子来看看,
private int incrementNum=0;
public void methodC(){
incrementNum++;
}
public void methodD(){
++incrementNum;
}
//最大线程数
final int MAX_TERAD=100;
//循坏次数
final int MAX_TURN=21474836;
@Test
public void incrementtest() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(MAX_TERAD);
Runnable runnable =()->{
for (int i=0;i<MAX_TURN;i++){
methodC();
}
latch.countDown();
};
for (int i=0;i<MAX_TERAD;i++){
new Thread(runnable).start();
}
latch.await();
System.out.println("理论结果:"+MAX_TURN*MAX_TERAD);
System.out.println("实际结果:"+getincrementNum());
System.out.println("差距:"+(MAX_TURN*MAX_TERAD-getincrementNum()));
}
执行结果:
从结果上可知,实际结果与理论结果相差还是很大的。
为什么会相差这么大呢,可以从原子性和可见性两个方面解释。
原子性分析
首先,我们先从代码的字节码来查看一下,代码进过编译后使用jclasslib工具查看字节码结果如下:
从字节码可知,有4个关键性的汇编指令:
① 获取当前incrementNum变量的值,并且放入栈顶
② 将常量1放入栈顶;
③将当前栈顶中的两个int类型的值相加, 并把结果放入栈顶;
④吧栈顶的结果再赋值给incrementNum变量。
通过以上4个关键性的汇编指令可以看出,在汇编代码的层面,++操作实质上是4个操作。这4个操作之间是可以发生线程切换的,或者说是可以被其他线程中断的。所以,++操作不是原子操作,在并行场景会发生原子性问题。
在运行测试代码的时候,通过jps -l 查询到线程运行的pid后,通过jstack pid 查看JVM线程运行情况可知
为了方便查看运行情况,我设置了10个线程同时运行, 从截图可看出,10个线程同时都是running的状态,那么多个线程同时执行i++代码时出现并发的几率很大,从运行的实际结果和理论结果可看出,已经出现了并发的操作了。
可见性分析
可见性是指一个线程对共享变量的修改,另一个线程能够立刻可见。多个线程对共享变量incrementNum进行自增操作,就会发生可见性问题,问题描述如下:
(1)主存中有变量incrementNum,初始值为0;
(2)Thread_0 将incrementNum 加1,先将incrementNum=0复制到自己的私有内存中,然后更新incrementNum的值,Thread_0 操作完成之后其私有内存中incrementNum的值为1,但是Thread_0 将更新后的incrementNum值会刷到主存的时间是不固定的。
(3)在Thread_0没有回刷incrementNum到主存前,刚好Thread_1同样从主存中读取incrementNum,此时值为0,和Thread_0进行同样的操作,最后期望值是incrementNum=2目标没有达成,最终incrementNum=1。
Thread_0和Thread_1 并发操作incrementNum发生内存可见性问题的过程如下:
这也是导致多线程下incrementNum自增的实际结果与预期结果不一致的原因之一。
保证a++线程安全的三种方式
synchronized 同步保证a++ 线程安全
可以使用synchronized关键字保证a++ 的线程安全,这种方式是多个线程运行时,只有一个线程是获取running 状态,其他线程是BLOCK状态,实现代码如下:
public Integer availableLock = new Integer(1);
/**
* 同步方法
*/
public void methodB(){
synchronized (this.availableLock){
this.incrementNum++;
}
}
运行结果如下
从运行结果可以看出,实际值与理论值是一致的。接下来我们通过jstack 工具和字节码来看下是什么原因保证一致的。
首先jstack pid 查看Jvm线程运行情况:
从运行结果可看出,只有一个线程是Running状态。
接着我们来看看加了synchronized关键字的字节码
使用synchronized关键字后,从截图的字节码中可以看到使用了monitorenter 和monitorexit指令进行了处理。每个对象都与一个monitor相关联,一个monitor 的lock的锁只能被一个线程在同一时间获得。 这就是为什么使用多个线程时,只有一个线程状态是Running的原因。
使用显式锁lock保证a++线程安全
使用显示锁保证a++线程安全的代码如下:
private final Lock lock = new ReentrantLock();
public void methodC1() {
lock.lock();
try {
incrementNum++;
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
运行结果:
从运行结果可以看出, 实际值与理论值是一致的。同样我们jstack工具来查看一下是如何保证实际值与结果值一致的,
首先使用jstack pid 查看一下JVM 线程运行的情况:
从结果可以看出,一共10个线程执行,但是和synchronized关键字一样,也只有一个线程状态是runing的,其余线程的状态是WAITING (parking)状态的,
AtomicInteger 原子类保证a++线程安全
使用AtomicInteger 原子类保证a++线程安全的代码如下:
AtomicInteger atomicIntegerIncrementNum = new AtomicInteger(0);
public void atomicIntegerIncrementNum(){
atomicIntegerIncrementNum.getAndIncrement();
}
public AtomicInteger getAtomicIntegerIncrementNum() {
return atomicIntegerIncrementNum;
}
运行结果如下:
从运行结果可以看出, 实际值与理论值是一致的。接下来我们用jclasslib工具查看一下一致的原因,如下图
从图中可知,AtomicInteger 的字节码只有一个JVM执行指令。
三种方式性能比较
使用jmh进行性能测试分析,
//度量批次为10次
@Measurement(iterations = 10)
//预热批次为10次
@Warmup(iterations = 10)
//采用平均响应时间作为度量方式
@BenchmarkMode(Mode.AverageTime)
//时间单位为毫秒
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class SynchronizedVsLockVsAtomicInteger {
@State(Scope.Group)
public static class IntMonitor {
private int a;
private final Lock lock = new ReentrantLock();
//使用显式锁Lock进行共享资源同步
public void lockInc() {
lock.lock();
try {
a++;
} finally {
lock.unlock();
}
}
//使用synchronized 关键字进行共享资源同步
public void synInc() {
synchronized (this) {
a++;
}
}
public int getA() {
return a;
}
}
@State(Scope.Group)
public static class AtomicIntegerMonitor {
private AtomicInteger a = new AtomicInteger(0);
public void inc() {
a.getAndIncrement();
}
public AtomicInteger getA() {
return a;
}
}
//基准测试方法
@GroupThreads(10) // 10个线程执行
@Group("sync")
@Benchmark
public void syncInc(IntMonitor monitor) throws InterruptedException {
//循坏次数
final int MAX_TURN = 1000000;
for (int i = 0; i < MAX_TURN; i++) {
monitor.synInc();
}
}
//基准测试方法
@GroupThreads(10) //10个线程执行
@Group("lock")
@Benchmark
public void lockInc(IntMonitor monitor) throws InterruptedException {
//循坏次数
final int MAX_TURN = 1000000;
for (int i = 0; i < MAX_TURN; i++) {
monitor.lockInc();
}
}
//基准测试方法
@GroupThreads(10) //10个线程执行
@Group("atomic")
@Benchmark
public void licInc(AtomicIntegerMonitor monitor) throws InterruptedException {
//循坏次数
final int MAX_TURN = 1000000;
for (int i = 0; i < MAX_TURN; i++) {
monitor.inc();
}
}
public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder().include(SynchronizedVsLockVsAtomicInteger.class.getSimpleName())
.forks(1)
.timeout(TimeValue.seconds(10))
.addProfiler(StackProfiler.class) //堆栈信息
.addProfiler(GCProfiler.class) //GC信息
.build();
new Runner(options).run();
}
}
基准测试结果如下:
从基准测试的结果不难看出,AtomicInteger>显式锁Lock>synchronized关键字,在该基准测试的配置中,我们增加了StackProfiler,窥探出AtomicInteger表现优异的原因如下:
AtomicInteger线程的RUNNABLE状态高达91%,并且没有BLOCKED状态,而synchronized关键字则相反,BLOCKED状态高达68.5%,因此AtomicInteger高性能的表现也就不足为奇了。
举一反三: a++/++a 不是线程安全的,那么a-- /–a也同样不是线程安全的, 同样也可以使用synchronized 、显示锁lock、AtomicInteger 的方式保证线程安全。