synchoronized 为什么不能锁 int 或者 long 类型

732 阅读4分钟

背景

昨天 review 同事代码的时候,有一块代码是这么加锁的。

synchronized(guild.guildId) { // 对公会的 id 进行加锁
    val guildGold = guild.guildGold // 获取公会现有的公会币
    val newGuildGold = guildGold + 100 // 加 100
    guild.guildGold = newGuildGold // 赋值新的公会币数量给公会
}

这块代码的逻辑是一个先读后写的一个过程,因为此操作不是原子的,为了避免并发所产生的数据不一致问题,所以同事使用 synchroinzed 关键字来保证同一时刻只有一个线程来执行这个代码块。

同事的初衷是好的,可是锁对象是公会的 id,这是一个 long 类型的整数,如果这么做的话,锁会生效吗?

如果没有生效,是为什么没有生效。如果生效了,那么是在什么情况下才生效?

下面的代码可以一一解释上面的问题。

测试代码

@Test
fun lockWithPrimitiveType() {

    val startTime = Instant.now()
    Thread {
        while (true) {
            synchronized(100L) {
                TimeUnit.SECONDS.sleep(2L) // 模拟业务代码耗时
                val endTime = Instant.now()
                val duration = Duration.between(startTime, endTime)
                println("current thread:${Thread.currentThread().name}, I am guild one, seconds: ${duration.seconds}")
            }
        }
    }.start()

    Thread {
        while (true) {
            synchronized(100L) {
                TimeUnit.SECONDS.sleep(2L) // 模拟业务代码耗时
                val endTime = Instant.now()
                val duration = Duration.between(startTime, endTime)
                println("current thread:${Thread.currentThread().name}, I am guild two, seconds: ${duration.seconds}")
            }
        }
    }.start()

    while (true) {}
}

让我们来看一下打印结果:

current thread:Thread-3, I am guild one, seconds: 2
current thread:Thread-4, I am guild two, seconds: 4
current thread:Thread-4, I am guild two, seconds: 6
current thread:Thread-4, I am guild two, seconds: 8
current thread:Thread-4, I am guild two, seconds: 10
current thread:Thread-4, I am guild two, seconds: 12
current thread:Thread-4, I am guild two, seconds: 14
current thread:Thread-4, I am guild two, seconds: 16
current thread:Thread-4, I am guild two, seconds: 18
current thread:Thread-4, I am guild two, seconds: 20

从日志来看,Thread-3线程在执行业务代码的时候,Thread-4线程并没有被执行(阻塞等待)。Thread-4线程在执行业务代码的时候, Thread-3线程也没有被执行。由此可见,锁已经生效了。于是得出了一个结论:“synchornized 锁一个 long 类型的整数是可行的”。

可事实是真是这样吗?

下面对代码做一个简单的修改,将 synchronzied(100L) 改为 synchronized(1000L),再进行测试。

@Test
fun lockWithPrimitiveType() {

    val startTime = Instant.now()
    Thread {
        while (true) {
            synchronized(1000L) { // 将 100L 改为 1000L
                TimeUnit.SECONDS.sleep(2L) // 模拟业务代码耗时
                ..... // 与之前代码一致
            } 
        }
    }.start()

    Thread {
        while (true) {
            synchronized(1000L) { // 将 100L 改为 1000L
                TimeUnit.SECONDS.sleep(2L) // 模拟业务代码耗时
                ...... // 与之前代码一致
            }
        }
    }.start()

    while (true) {}
}

让我们继续看一下打印结果:

current thread:Thread-4, I am guild two, seconds: 2
current thread:Thread-3, I am guild one, seconds: 2
current thread:Thread-4, I am guild two, seconds: 4
current thread:Thread-3, I am guild one, seconds: 4
current thread:Thread-4, I am guild two, seconds: 6
current thread:Thread-3, I am guild one, seconds: 6
current thread:Thread-3, I am guild one, seconds: 8
current thread:Thread-4, I am guild two, seconds: 8
current thread:Thread-4, I am guild two, seconds: 10
current thread:Thread-3, I am guild one, seconds: 10

这个时候,我们会发现,Thread-4线程在执行业务代码的时候,Thread-3线程同时也在执行业务代码,那么这个时候,锁为什么没有生效呢?

答案是:synchronized 只能锁对象,那么当我们传入 int 类型或者 long 类型整数的时候,java 会调用 Integer.valueOf() 或者 Long.valueOf() 方法将它们转为一个新的对象,所以锁不会生效。

那么为什么锁 100L 这个整数时,锁会生效,这个时候我们看源码可以得知,Java 会把 -128 到 127 所对应的包装类对象都给缓存起来,分别存入了 IntegerCacheLongCache 对象里。

源码分析

Integer.valueOf() 方法

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high) // 如果 i >= -128 且 i <= 127
        return IntegerCache.cache[i + (-IntegerCache.low)]; // 返回 IntegerCache 里的对象
    return new Integer(i); // 返回一个新对象
}

IntegerCache

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        int h = 127
        ...... // 省略非关键性代码
        heigh = h

        cache = new Integer[(high - low) + 1]; // 设置数组的大小(256)
        int j = low;
        for(int k = 0; k < cache.length; k++) // 为 -128 到 127 之间的整数创建 Integer 对象
            cache[k] = new Integer(j++); 
       ...... // 省略非关键性代码
    }

    private IntegerCache() {}
}

Long.valueOf() 方法

public static Long valueOf(long l) {
    final int offset = 128;
    if (l >= -128 && l <= 127) { // will cache
        return LongCache.cache[(int)l + offset]; // 返回 LongCache 里的对象
    }
    return new Long(l); // 返回新对象
}

LongCache

private static class LongCache {
    private LongCache(){}

    static final Long cache[] = new Long[-(-128) + 127 + 1];

    static {
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Long(i - 128);
    }
}

可以发现,Integer.valueOf()Long.valueOf 实现方法略有不同,IntegerCacheLongCache 实现方法也略有不同,可是结果都是一样的:都是对-128 到 127 之间的整数所对应的包装类对象进行缓存。

为什么实现没有保持统一,我们也不得而知了,我猜可能是实现代码的作者不同吧,哈哈哈。

阿里巴巴开发规范

在阿里巴巴开发规范,OOP 规约里第 7 条也有提到这么一项的强制规则。

image.png