Java开发,参数类型如何选?int, Integer, AtomicInteger?

背景

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


概述

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

int

int 是 Java 的基本数据类型,它是一个 32 位的有符号整数,取值范围为 -2^31 到

2^31-1。

int 类型在性能上比 Integer 和 AtomicInteger 更优越,因为它是一个简单的原生类型,没有额外的封装和开销。

Integer

首先,Integer属于包装类。包装类型的出现就是我们可以在对象里面定义一些方法,因为封装的数据类型就是一个对象,可以拥有属性和方法,有了这些属性和方法我们就可以用它们来处理数据,比如Integer对象里的parseInt(String s),可以把字符串转换成int类型等。

Java 复制代码
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,其底层代码实现依据:

Java 复制代码
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) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
Java 复制代码
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方法的源码进行解读,其过程先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。

Java 复制代码
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 实例使用自动装箱来创建,那么就会引用相同的对象。

Java 复制代码
Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true

AtomicInteger

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

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

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

java 复制代码
private AtomicInteger cnt = new AtomicInteger();

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

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

java 复制代码
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() 在一个循环中进行,发生冲突的做法是不断的进行重试。

java 复制代码
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 能保证多个线程修改的原子性。

相关推荐
ok!ko1 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
2402_857589361 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰2 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
ChinaDragonDreamer2 小时前
Kotlin:2.0.20 的新特性
android·开发语言·kotlin
IT良2 小时前
c#增删改查 (数据操作的基础)
开发语言·c#
哎呦没3 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
Kalika0-03 小时前
猴子吃桃-C语言
c语言·开发语言·数据结构·算法
_.Switch3 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
编程、小哥哥3 小时前
netty之Netty与SpringBoot整合
java·spring boot·spring
代码雕刻家3 小时前
课设实验-数据结构-单链表-文教文化用品品牌
c语言·开发语言·数据结构