Java多线程高并发系列:(五)i++的原子性分析

2024-10-25 16:54
37
0

在此先抛出结果:Java中i++不是原子操作,存在竞态条件,线程不安全。

这是为何?

在回答这个问题之前,先了解一个指令:javap -v -p Counter.class

javap是JDK自带的反编译工具,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。

我们写一个简单的i++的代码示例:

public class Counter {
    volatile int i = 0;
    
    public void add(){
        i++;
    }
}

再进入Counter类的class文件所在目录,用上面的javap指令把class文件反编译一下,反编译的内容如下:

public class myTest.Counter
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // myTest/Counter.i:I
   #8 = Class              #10            // myTest/Counter
   #9 = NameAndType        #11:#12        // i:I
  #10 = Utf8               myTest/Counter
  #11 = Utf8               i
  #12 = Utf8               I
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               LmyTest/Counter;
  #18 = Utf8               add
  #19 = Utf8               SourceFile
  #20 = Utf8               Counter.java
{
  volatile int i;
    descriptor: I
    flags: ACC_VOLATILE

  public myTest.Counter();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_0
         6: putfield      #7                  // Field i:I
         9: return
      LineNumberTable:
        line 2: 0
        line 3: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LmyTest/Counter;

  public void add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #7                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #7                  // Field i:I
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   LmyTest/Counter;
}

i是一个成员变量,在堆内存中。一个i++,在编译后会有4个指令:

  • getfield      #7:是从堆内存中拿出#7的数据放入操作数栈,#7里就是i的值0。
  • iconst_1:是将一个常数1放入操作数栈。
  • iadd:是把1和0出栈相加然后放入操作数栈。
  • putfield      #7:是把操作数栈中的栈顶元素放入堆内存 #7里去。

(更多指令可以百度Java字节码指令https://www.cnblogs.com/longjee/p/8675771.html)

为什么这4个指令就能得出i++不是原子性?

按如上代码,如果是多线程操作,比如:2个线程同时执行了getfield指令,拿到相同的值都去加1然后放入堆内存中,那么2个线程应该加2的,其实就只加了1。
这不是可见性的问题,上面代码中i用volatile修饰,是没有用的。

如何解决保证i++的原子性?

  1. 在add方法上加synchronized
  2. 加锁,可重入锁。
  3. 用Java提供的AtomicInteger实现加一操作。
  4. 通过Unsafe类实现CAS机制(JDK9开始sun.misc.Unsafe 类不再对第三方代码公开,不过可以通过反射的方式调用

更多Unsafe相关知识可查看另一篇文章

前3种方式是相对简单和实用的,这里就不多做介绍了。在此通过代码简单介绍下调用Unsafe类实现i++的方法:

public class CounterUnsafe {
    private int i = 0;
    // Unsafe工具类对象
    private static Object unsafe = null;

    // 字段偏移量的值
    private static long valueOffset;
    static {
        try {
            // 通过反射获取Unsafe类
            Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
            Field field = unsafeClass.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = field.get(null);
            //指定要修改的字段 i
            Field iFiled = CounterUnsafe.class.getDeclaredField("i");
            // 获得字段 i的偏移量
            valueOffset = (long) unsafe.getClass().getMethod("objectFieldOffset",Field.class).invoke(unsafe, iFiled);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void add() throws Exception {
        // 自旋调用unsafe的compareAndSwapInt方法对i进行+1
        for(;;){
            if ((boolean)unsafe.getClass().getMethod("compareAndSwapInt", Object.class, long.class, int.class, int.class)
                .invoke(unsafe,this, valueOffset, i, i + 1)) {
                return; // 执行成功返回
            }
        }
    }

    public static void main(String[] args) throws Exception {
        CounterUnsafe counterUnsafe = new CounterUnsafe();
        System.out.println(counterUnsafe.i);
        counterUnsafe.add();
        System.out.println(counterUnsafe.i);
    }
}

 

 

 

 

 

全部评论