诡异的线程加锁问题

原创 2016年09月19日 22:12:54

引言:在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, 之类的都是这样的情况,所以最好不要作为锁对象,因为其有可能在值变更的时候创建出新对象,故锁对象发生了变更,锁机制失效。


 

多线程程序何时需要加锁

简单的说三条: 多人读,不需要 一读一写要加 多人写要加 常见错误 1读1写没事,读写的内存约多,越容易出事,因为不是原子操作 对int/int64/char型是原子操作, 可不加...
  • ma100
  • ma100
  • 2016-06-12 14:01:20
  • 2404

对于多线程编程,很多人概念不清,写代码的时候要么是处处加锁,影响性能不说,还容易莫名其妙的死锁,还有人对多线程敬而远之。

对于多线程编程,很多人概念不清,写代码的时候要么是处处加锁,影响性能不说,还容易莫名其妙的死锁,还有人对多线程敬而远之。所以学习多线程编程最重要的不是学习API,而是理解什么才是多线程安全的代码从例子...
  • ly131420
  • ly131420
  • 2007-08-14 21:45:00
  • 3870

多线程访问同一变量是否需要加锁

对于多线程访问同一变量是否需要加锁的问题,先前大家都讨论过。今天用代码验证了一下之前的猜想:32位CPU与内存的最小交换数据为4字节/次,这也是结构体要对齐4字节的原因。在物理上,CPU对于同一4字节...
  • liuyueyue0921
  • liuyueyue0921
  • 2015-09-08 21:49:06
  • 536

多线程的几种加锁方式详解

NSLock NSLock是Cocoa提供给我们最基本的锁对象,这也是我们经常使用的,除lock和unlock外,NSLock还提供了tryLock和lockBeforeDate:两个方法,前一...
  • qq_35247219
  • qq_35247219
  • 2016-07-17 09:47:19
  • 22217

Java 多线程加锁的方式总结及对比

参考博文:http://www.cnblogs.com/handsomeye/p/5999362.html 一.Java多线程可以通过: 1. synchronized关键字 2. Ja...
  • u010842515
  • u010842515
  • 2017-03-28 14:27:53
  • 8567

在一个线程加锁,另一个线程解锁

一般来讲,一个线程加锁,另一个线程解锁,是很容易死锁的。 产生死锁的四个必要条件: (1) 互斥条件:一个资源每次只能被一个进程使用。 (2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资...
  • p2016
  • p2016
  • 2017-08-03 15:03:30
  • 848

【Linux C 多线程编程】互斥锁与条件变量

一、互斥锁互斥量从本质上说就是一把锁, 提供对共享资源的保护访问。  1. 初始化:  在Linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化:  对于...
  • xing_hao
  • xing_hao
  • 2011-07-22 16:25:34
  • 23865

多线程加锁

Linux pthread_mutex演示程序 C++语言: Linux pthread_mutex演示程序 Linux 下pthread 中使用mutex 进行互斥的程序和结果 #...
  • ffeng271
  • ffeng271
  • 2012-02-08 12:58:07
  • 5308

Java多线程——加锁机制

Java的锁分为内置锁和显式锁。内置锁在我们平时使用synchronized关键字的时候获取。显式锁则是通过获取java.util.concurrent.locks包下面的ReentrantLock类...
  • sakurairo_maukoro
  • sakurairo_maukoro
  • 2015-03-08 19:54:32
  • 2457

Integer做锁

在并发编程时,对象锁是无法回避的问题,什么样的对象可以用了做锁呢?就Java语法而言,只要是对象就能作为锁来使用,然而,仍有几点必须遵守: 1 锁不能为空,即用作锁的对象不能为空,这种错误很容易暴露,...
  • Dancen
  • Dancen
  • 2013-07-01 00:27:14
  • 1055
收藏助手
不良信息举报
您举报文章:诡异的线程加锁问题
举报原因:
原因补充:

(最多只允许输入30个字)