诡异的线程加锁问题

时间:2022-03-09 21:54:00

引言:在Java中,对于互斥的代码块,我们需要使用synchronized来进行线程安全的保证,本文将针对某个synchronized的锁实例中发生的问题来分析。

1. 测试代码

直接上代码,简单明了:

public class BadLockOnInteger implements Runnable {
public static Integer i = 0;
static BadLockOnInteger instance = new BadLockOnInteger();

@Override
public void run() {
for (int j=0; j<1000000; 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);
}
}
预期行为结果: 2000000

实际的行为结果:1584637,1160643,.....

貌似结果是随机的,且每次的结果都有不同,好像哪里出了问题.......

2.  代码分析

 这段代码其实很简单,就是针对i进行累加,每个线程增加1000000次,两个线程应该是2000000,针对两个线程使用了synchronized来保证互斥区域的线程安全。但是结果确实远远小于2百万的预期结果,只能证明这个synchronized的锁没有起到应有的作用,synchronized(i)好像没有问题呀,那问题处在哪里呢?

3. 原因分析

 在实际的代码执行过程中,i++在真实执行时变成了下面这个样子?

i = Integer.valueOf(i.intValue() + 1)
  在我们进一步查看起Integer.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);   } 

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

  因此我们就知道两个线程每次加锁都加在了不同的对象实例上,从而导致对临界区代码控制出现问题。

  以下是摘在Integer.class的源代码:

    /**     * The value of the {@code Integer}.     *     * @serial     */    private final int value;
在其内部实现上,value是一个final类型的整形变量,其在创建之后,就无法被改变了,故在我们尝试修改其值的过程中,只能创建新的Integer对象。

4. 问题修复

修正这个问题只需要将

synchronized(i)
替换为:

synchronized(instance)
5. 总结

 synchronized锁定的对象Integer在实际的代码中,是使用final来指定的。所以每次值的改变都会创建新的Integer对象;同样的道理类似于Long, 之类的都是这样的情况,所以最好不要作为锁对象,因为其有可能在值变更的时候创建出新对象,故锁对象发生了变更,锁机制失效。