Integer 错误的加锁

多线程同时访问一个Integer加锁的问题,程序运行和想要的结果相差甚远,让我百思不得其解,就下来研究了一下:

  在进行多线程同步时,加锁是保证线程安全的重要手段之一。synchronized是大多数程序员必须要掌握的同步锁,但是这个问题非常的隐晦,大家可以参考一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class BadLockOnInteger implements Runnable {
 
    public static Integer i = 0;
 
    public static BadLockOnInteger instance = new BadLockOnInteger();
 
    @Override
    public void run() {
        for (int j = 0; j < 10000000; j++) {
            synchronized (i){
                i++;
            }
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
 
        t1.start();
        t2.start();
 
        t1.join();
        t2.join();
 
        System.out.println(i);
    }
}  

程序运行结果:

1
12953898

注意:结果和我们想要的结果相差甚远,得到的是一个比20000000 小很多的数字。这说明一定是这段程序并没有真正做到线程安全!但把锁加在变量 i 上似乎逻辑也是无懈可击的,为啥得不到准确的结果呢?问题就在这一块,synchronized(i) 底层有问题!

  要解释这个问题,得从Integer说起。在Java中,Integer是不可变对象。也就是对象一旦创建了就不可能被修改!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class Integer extends Number implements Comparable<Integer> {
 
           ......
 
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);  //这个方法会新创建一个新的Integer对象
    }
 
    private final int value; //封装的value变量 final关键字修饰,不可变
 
    ......
}

  

  如果我们使用javap反编译这段代码的 run() 方法,我们可以看到:

  在第19 ~ 22 行,实际上使用了 Integer.valueOf(i) 方法新建了一个新的 value 对象,并将它赋值给变量 i。也就是说 i++ 变成了:

1
i = Integer.valueOf(i.intValue() + 1);  //涵盖了包装类与基本类型的装箱和拆箱原理成分

   进一步查看 Integer.valueOf(),我们可以看到:

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

  Integer.valueOf() 实际上是一个工厂方法,它会倾向于返回一个代表指定数值的 Integer 实例。因此,i++ 的本质是创建一个新的 Integer 对象,并将它的引用赋值给 i 。

  如此一来,我们就可以明白问题所在了,由于在多个线程间,并不一定能够看到同一个 i 对象(因为 i  对象一直在变),因此,两个线程每次加锁可能都加在了不同的对象实例上,从而导致对临界区代码控制出现问题。

  修改这个问题也很容易,只需要将

1
synchronized(i)

  改为:

1
synchronized(instance)

  即可!

  

 

posted @   菜鸟的奋斗之路  阅读(428)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示