诡异的线程加锁问题

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


 

浅谈多线程编程以及锁的效率测试

锁在多线程应用上非常广泛,虽然这个影响效率,但这也是在不影响计算结果上最直观的方法了。多线程编程主要有四种思路,一种是加锁,一种是无锁式编程,一种是 STM  软件事务内存,一种是使用 Erlang ...
  • fawdlstty
  • fawdlstty
  • 2015年04月15日 21:56
  • 1223

诡异的线程加锁问题

引言:在Java中,对于互斥的代码块,我们需要使用synchronized来进行线程安全的保证,本文将针对某个synchronized的锁实例中发生的问题来分析。     1. 测试代码 ...
  • mmp591
  • mmp591
  • 2017年07月27日 17:56
  • 44

【多线程】线程中的同步锁synchronized

当多个线程同时执行时,由于cpu是随机分片的,所以,一个线程在执行过程中被另一个线程打断的情况是经常发生的。 这在某些情况下是会影响到正常的程序的输出结果的。比如银行转账一个人的账户在转账时是不能允许...
  • wangyy130
  • wangyy130
  • 2016年07月25日 21:23
  • 936

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

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

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

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

Android多线程-----同步锁

一、同步机制关键字synchronized 对于java来说,最常用的同步机制就是synchronized关键字,他是一种基于语言的粗略锁,能够作用于对象、函数、class。每个对象都...
  • a992036795
  • a992036795
  • 2016年05月11日 10:27
  • 7426

java线程研究---(9)Thread同步:如何加锁

继续上一文。 运行结果如下: Thread-1: 1 Thread-1: 2 Thread-1: 3 Thread-0: 4 Thread-0: 5 Thread-1: 6 Thread-1...
  • miqi770
  • miqi770
  • 2015年09月02日 16:22
  • 1640

多线程编程的锁问题解析(锁竞争死锁活锁及Date Race等)

为了防止例1中的数据竞跑现象,我们可以使用锁来保证每个线程对counter++操作的独占访问(即保证该操作是原子的)。在例3的程序中,我们使用mutex锁将counter++操作放入临界区中,这样同一...
  • FreeeLinux
  • FreeeLinux
  • 2017年01月05日 12:28
  • 1267

java线程安全和锁机制详解

java线程安全和锁机制详解 . 在开始这篇blog之前应该先了解几个概念: 临界区: 保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行...
  • beyond59241
  • beyond59241
  • 2016年03月14日 19:59
  • 2043

Java多线程——加锁机制

Java的锁分为内置锁和显式锁。内置锁在我们平时使用synchronized关键字的时候获取。显式锁则是通过获取java.util.concurrent.locks包下面的ReentrantLock类...
  • sakurairo_maukoro
  • sakurairo_maukoro
  • 2015年03月08日 19:54
  • 2140
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:诡异的线程加锁问题
举报原因:
原因补充:

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