一次由于Java内存管理机制导致的线程异常阻塞之旅
引言
一转眼已经两年多没写多博客了;一转眼也要快工作两三年了;一转眼我又开始写Java代码了。希望自己可以坚持写写博客,总结总结的习惯!加油。
今天在调试代码的时候,发现两个毫不相关的thread用jstack看竟然其中一个在等待另一个的线程持有的锁,很是奇怪。经过研究,是因为Integer类的实现机制导致的。
一、异常阻塞代码
1 package xxx; 2 3 public class TestDeadLock { 4 public static void main(String[] argvs) { 5 6 new A("threadA").start(); 7 new B("threadB").start(); 8 try { 9 Thread.sleep(15000); 10 } catch (Exception e) { 11 12 } 13 } 14 } 15 class A extends Thread { 16 public A(String name) {super(name);} 17 private final Object updateLock = 0; 18 @Override 19 public void run() { 20 System.out.println(updateLock.getClass()); 21 synchronized (updateLock) { 22 System.out.println(System.currentTimeMillis() + " in A"); 23 try { 24 sleep(100000); 25 } catch (InterruptedException e) { 26 27 } 28 } 29 } 30 } 31 class B extends Thread { 32 public B(String name) {super(name);} 33 private final Object updateLock = 0; 34 @Override 35 public void run() { 36 System.out.println(updateLock.getClass()); 37 synchronized (updateLock) { 38 System.out.println(System.currentTimeMillis() + " in B"); 39 try { 40 sleep(100000); 41 } catch (InterruptedException e) { 42 43 } 44 } 45 } 46 }
上面的代码很简单,建立了两个thread,分别打印一句话,每个线程拥有一个本地的变量updateLock用来进行自己线程内部的同步。问题就出在了这个updateLock上。运行上面的代码会发现这两个线程会互相阻塞,updateLock为线程的内部对象,为什么会互相阻塞呢?问题就出现在了17以及33行的0赋值的问题。0这个值本身是一个基本类型int,updateLock是一个Object的类型,因为实际上会把0强转为Integer类型赋值给updateLock对象,那么Java内部是如何完成这个操作的呢,这个赋值是依赖于Integer的ValueOf方法实现的。
二、Integer的ValueOf实现
1 public static Integer valueOf(int i) { 2 if (i >= IntegerCache.low && i <= IntegerCache.high) 3 return IntegerCache.cache[i + (-IntegerCache.low)]; 4 return new Integer(i); 5 }
可以看到valueOf的实现依赖于IntegerCache的实现,从名字就能看出来IntegerCache是一个Interger的缓存,IntegerCache缓存这从-128到127(上限127可以通过jvm配置进行修改)。valueOf的逻辑就是如果存在缓存,返回缓存,不存在就返回一个新的Integer对象。
三、IntegerCache的实现
1 private static class IntegerCache { 2 static final int low = -128; 3 static final int high; 4 static final Integer cache[]; 5 6 static { 7 // high value may be configured by property 8 int h = 127; 9 String integerCacheHighPropValue = 10 sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); 11 if (integerCacheHighPropValue != null) { 12 try { 13 int i = parseInt(integerCacheHighPropValue); 14 i = Math.max(i, 127); 15 // Maximum array size is Integer.MAX_VALUE 16 h = Math.min(i, Integer.MAX_VALUE - (-low) -1); 17 } catch( NumberFormatException nfe) { 18 // If the property cannot be parsed into an int, ignore it. 19 } 20 } 21 high = h; 22 23 cache = new Integer[(high - low) + 1]; 24 int j = low; 25 for(int k = 0; k < cache.length; k++) 26 cache[k] = new Integer(j++); 27 28 // range [-128, 127] must be interned (JLS7 5.1.7) 29 assert IntegerCache.high >= 127; 30 } 31 32 private IntegerCache() {} 33 }
IntegerCache的实现比较简单,其实就是内部维护了一个cache数组,提前new好对象,需要的时候直接返回,不再进行对象的创建,因为对象的创建成本是很高的,这样做可以提高效率。这里有一个有意思的地方是jvm需要通过java.lang.Integer.IntegerCache.high配置来修改cache缓存的上限,但是没有提供修改下限的配置,不知道这样做的目的是什么,还有待于考察。
四、深入Object updateLock = 0
因为第一节中的额代码产生了线程互相阻塞的问题,所以可以猜测出两个线程中的updateLock其实是一个相同的对象,即两个线程中的updateLock是两个相同的Integer对象,而相同的Integer对象很大可能是产生自Integer的valueOf方法返回的对象。为了验证我们的猜想,我们将断点断到java.lang.Interger的valueOf函数上进行debug,查看调用栈,如下图所示
上面的图验证了我们的猜想。因此如何解决这个问题也就很简单了。可以将一个new Object()赋值给updateLock就可以了。
总结
如果一个对象的目的是用在synchronized关键字后进行同步处理,那么不允许进行int或其他内部类型的赋值,如第一节中的示例代码,标准的做法应该如下
final Object updateLock = new Object();