背景

如题所述,笔者在前段时间面试某家金融科技公司时被问到了上述问题,脑海中的记忆一时也不是太清楚,特地前来进行整理并分享。


概述

int是基础的变量类型;Integer是包装类型;AtomicInteger是来自JUC的一个在并发编程场景下重要的包,对于Java开发人员来说,确实需要对其都有充分的认识与了解。

int

int 是 Java 的基本数据类型,它是一个 32 位的有符号整数,取值范围为 -2^31 到
2^31-1。
int 类型在性能上比 Integer 和 AtomicInteger 更优越,因为它是一个简单的原生类型,没有额外的封装和开销。

Integer

Integer x = 2;     // 装箱 调用了 Integer.valueOf(2)
int y = x;         // 拆箱 调用了 X.intValue()

Integer 是 Java 的一个包装类,它对应的基本类型是 int。**Integer 类型的所有实例都共享一个静态的缓存池,用于存储 int 类型的值。**当需要使用一个整数时,Java 会优先从缓存池中获取一个已有的 Integer 实例,而不会创建一个新的实例。这样可以提高性能,尤其是在处理大量整数时。

Integer 和 int的区别?

  • Integer是int的包装类,int则是java的一种基本的数据类型;

  • Integer变量必须实例化之后才能使用,而int变量不需要实例化;

  • Integer实际是对象的引用,当new一个Integer时,实际上生成一个指针指向对象,而int则直接存储数值

  • Integer的默认值是null,而int的默认值是0。

  • 包装类Integer和基本数据类型比较的时候,java会自动拆箱为int,然后进行比较

缓存池

基本类型对应的缓冲池如下

Java 基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean;

  • 前4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据
  • Character创建了数值在[0,127]范围的缓存数据
  • Boolean 直接返回True 或 False。如果超出对应范围仍然会去创建新的对象。

在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。

在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界可调,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax=<size> 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。

在 Java 8 中,已知Integer 缓存池的大小默认为 -128~127,其底层代码实现依据:

static final int low = -128;
static final int high;
static final Integer cache[];

static {
    // high value may be configured by property
    int h = 127;
    String integerCacheHighPropValue =
        sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    if (integerCacheHighPropValue != null) {
        try {
            int i = parseInt(integerCacheHighPropValue);
            i = Math.max(i, 127);
            // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
        } catch( NumberFormatException nfe) {
            // If the property cannot be parsed into an int, ignore it.
        }
    }
    high = h;

    cache = new Integer[(high - low) + 1];
    int j = low;
    for(int k = 0; k < cache.length; k++)
        cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
}
缓存池Java实践

new Integer(123) 与 Integer.valueOf(123) 的区别在于:

  • new Integer(123) 每次都会新建一个对象;
  • Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y);    // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k);   // true

具体分析,我们可结合valueOf方法的源码进行解读,其过程先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。

Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true

AtomicInteger

Number 的原子类 AtomicInteger 和 AtomicLong 是可变的。

J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。

以下代码使用了 AtomicInteger 执行了自增的操作。

private AtomicInteger cnt = new AtomicInteger();

public void add() {
    cnt.incrementAndGet();
}

以下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。

可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

为什么AtomicInteger使用CAS完成?因为传统的锁机制需要陷入内核态,造成上下文切换,但是一般持有锁的时间很短,频繁的陷入内核开销太大,所以随着机器硬件支持CAS后,JAVA推出基于compare and set机制的AtomicInteger,实际上就是一个CPU循环忙等待。因为持有锁时间一般较短,所以大部分情况CAS比锁性能更优。

最初是没有CAS,只有陷入内核态的锁,这种锁当然也需要硬件的支持。后来硬件发展了,有了CAS锁,把compare 和 set 在硬件层次上做成原子的,才有了CAS锁。

重要性

AtomicInteger 能保证多个线程修改的原子性。

10-15 10:49