从多个维度剖析a++/++a为什么不是线程安全的

18 篇文章 0 订阅

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 的方式保证线程安全。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

弯_弯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值