Java 的源代码学习(1)——基本类型和对应的类

由于国内的互联网企业基本不用 C#,所以想要去得赶快研究下 Java。虽然 Java 和 C# 在很多地方都差不多,不过还是有不少区别。这个系列就是专门用来学习 JDK 的源码的。

Java 的设计不如 C#的好,从这篇文章你就能看出。很多东西如果不知道背后的具体实现会觉得很莫名其妙。

考虑如下代码:

		Integer i1 = 18;
		Integer i2 = 18;
		Long l1 = 18l;
		Long l2 = 18l;
		
		System.out.println(i1 == i2);
		System.out.println(l1 == l2);
		
		System.out.println(l1.equals(i1));

在 Java 中,Integer 不是基元类型 int 的别名,所以这里的i1、i2、l1、l2都是在栈上分配的对象(和 .NET 有很大区别)。而 Java 不支持运算符重载,因此对象间的 == 运算符肯定是检查的实例对象是否是同一个,而 equals 方法则基本都会被覆盖。

执行一下程序,发现输出为true/true/false。那么把程序改一下:

		Integer i1 = 200;
		Integer i2 = 200;
		Long l1 = 200l;
		Long l2 = 200l;
		
		System.out.println(i1 == i2);
		System.out.println(l1 == l2);
		
		System.out.println(l1.equals(i1));

此时输出 false/false/false。

看上去好像比较小的整数会有缓存。我们再改写程序:

		Integer i1 = new Integer(18);
		Integer i2 = new Integer(18);
		Long l1 = new Long(18);
		Long l2 = new Long(18);
		
		System.out.println(i1 == i2);
		System.out.println(l1 == l2);

程序输出 false/false。

Java 的这个行为令人觉得非常怪异,那么只能查看源代码。在 Integer.java 中,我们发现了一个内部类:

    private static class IntegerCache {
        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) {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low));
            }
            high = h;

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

        private IntegerCache() {}
    }

显然,Java 把-128~127间的整数对应的实例对象进行了缓存。在 Long 中也有类似的 LongCache,不过 LongCache 的长度是不可配置的。那么这个 Cache 在什么时候用呢?查看源代码,发现只有 valueOf 方法才会使用:

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

那么原因就清楚了:只有调用 valueOf 或者进行自动装箱的时候这个 Cache 才会派上用场,直接使用构造方法生成的对象则不会是 Cache 中的对象,所以此时会返回 false。

由于 Java 的这个不一致的实现,所以无论如何都不可以用 == 号进行装箱后的数字的判断。那么 equals 方法呢?

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

只有另一个对象也是 Integer ,并且它们拆箱后的值相等的时候才返回 true。也就是说你把一个数字量相等的 Long 与它进行 equals 判断仍然是不相等。所以,也不要使用 equals 来进行判断。用 == 符号判断 Integer 和 Long 更不可行,因为类型不一致编译器直接报错。

因此如果进行数字量判断,唯一的办法是使用拆箱后的值进行比较,否则别说是你,Oracle 自己估计都弄不清会发生什么!

		Integer i1 = new Integer(18);
		Long l1 = new Long(18);
		
		long l3 = l1;
		long l4 = i1;
		
		System.out.println(l3 == l4);  //true

所以说 Java 在基础类型这一块设计得很烂。.NET 就不会有这个问题,因为基础类型对应的类都是 int 之类的别名,在 CLR 中都是当作值类型进行处理的。

有一个家公司的笔试题和这些基元类型对应的对象的 == 和 equals 方法有关。个人觉得很不合适,因为采用这种做法是不提倡的,完全没必要用这个去考察一个面试者,除非是 Oracle 在招聘。

接下来看一些比较有意思的实现。

取符号位:

    public static int signum(int i) {
        // HD, Section 2-7
        return (i >> 31) | (-i >>> 31);
    }

按位反续:

    public static int reverse(int i) {
        // HD, Figure 7-1
        i = (i & 0x55555555) << 1 | (i >>> 1) & 0x55555555;
        i = (i & 0x33333333) << 2 | (i >>> 2) & 0x33333333;
        i = (i & 0x0f0f0f0f) << 4 | (i >>> 4) & 0x0f0f0f0f;
        i = (i << 24) | ((i & 0xff00) << 8) |
            ((i >>> 8) & 0xff00) | (i >>> 24);
        return i;
    }

大家可以自行想想原理,很考验智商。

AtomicInteger (以及其他类似的类)是提供整数相加、相减操作的线程安全的类。私有字段必然是:

private volatile int value;

直接设置、获取值本身是线程安全的。但是相加(包括++)和相减的操作肯定不是线程安全的了。

    public final int getAndAdd(int delta) {
        for (;;) {
            int current = get();
            int next = current + delta;
            if (compareAndSet(current, next))
                return current;
        }
    }

    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

compareAndSet 会调用一个名为 compareAndSwapInt 的 native 的方法。代码如下:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

jint 是在 native 的 C++ 语言中对应的 Java 的 int 类型,可以参考任意一本关于JNI的书。

cmpxchg 是机器指令支持的原子操作,和 .NET 里面的 Interlocked.CompareExchage 一样,在 Windows 上是调用的 WINAPI。

也就是说会用目前真实的值与 current 进行比较,如果相等,则其他线程没有进行修改,可以安全地把结果 next 写入。否则,当前值不是最新的,再取一遍当前值进行运算,直到成功为止。在 cmpxchg 函数中,返回值是待修改区域的当前值(在进行替换之前),用它和程序保存的 current 进行比较,就可以知道是否被修改过了。

与 .NET 的 ConcurrentQueue 之类的实现不同,Java 没有使用自旋锁而是直接马上进行下一次尝试。目测是 Java 认为操作很快,以至于不需要考虑 SpinWait 吧。

还有一个叫做 lazySet 的方法。上面的这种原子性的操作虽然使用了机器指令保证原子性,但是相对于直接读写代价还是比较高的。如果我们允许其他读线程可以不读最新的值,那么可以考虑使用 lazySet 提高性能。

UNSAFE_ENTRY(void, Unsafe_SetOrderedInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint x))
  UnsafeWrapper("Unsafe_SetOrderedInt");
  SET_FIELD_VOLATILE(obj, offset, jint, x);
UNSAFE_END

这里使用了 xchgq 指令,貌似没有找到相关的说明。如果你们找到了请告诉我。

还有一个 BigInteger 的实现,这个貌似比较复杂,以后再分析吧。

✏️ 有任何想法?欢迎发邮件告诉老夫:daozhihun@outlook.com