Java Integer缓存与线程安全问题
首先感谢文章作者的答疑解惑
System.identityHashCode()方法
System.identityHashCode()方法说明如下
/**
* Returns the same hash code for the given object as
* would be returned by the default method hashCode(),
* whether or not the given object's class overrides
* hashCode().
* The hash code for the null reference is zero.
*
* @param x object for which the hashCode is to be calculated
* @return the hashCode
* @since JDK1.1
*/
public static native int identityHashCode(Object x);
无论给定的x对象是否覆盖了hashCode()方法,都会调用默认的hashCode()方法返回hashCode,如果x == null, 返回0。 这个默认的hashCode()方法就是Object类中的hashCode方法。
Object对象hashCode说明如下
/**
* Returns a hash code value for the object. This method is
* ...
* As much as is reasonably practical, the hashCode method defined by
* class {@code Object} does return distinct integers for distinct
* objects. (This is typically implemented by converting the internal
* address of the object into an integer, but this implementation
* technique is not required by the
* Java™ programming language.)
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
*/
public native int hashCode();
对象的hashCode是由对象内存地址转换所得。
Integer对象演示如下:
Integer a = 3;
Integer b = 3;
Integer c = 200;
Integer d = 200;
log.info("a hashCode: " + a.hashCode());
log.info("b hashCode: " + b.hashCode());
log.info("c hashCode: " + c.hashCode());
log.info("d hashCode: " + d.hashCode());
log.info("a i hashCode: " + System.identityHashCode(a));
log.info("b i hashCode: " + System.identityHashCode(b));
log.info("c i hashCode: " + System.identityHashCode(c));
log.info("d i hashCode: " + System.identityHashCode(d));
结果:
19:06:17.872 [main] INFO - a hashCode: 3
19:06:17.901 [main] INFO - b hashCode: 3
19:06:17.901 [main] INFO - c hashCode: 200
19:06:17.901 [main] INFO - d hashCode: 200
19:06:17.901 [main] INFO - a i hashCode: 1267032364
19:06:17.901 [main] INFO - b i hashCode: 1267032364
19:06:17.901 [main] INFO - c i hashCode: 661672156
19:06:17.901 [main] INFO - d i hashCode: 96639997
a与b、c与d的hashCode值相同。
System.identityHashCode结果中,a与b相同、c与d不同。
Java中的装箱与拆箱
什么是装箱?什么是拆箱?
在前面的文章中提到,Java为每种基本数据类型都提供了对应的包装器类型,至于为什么会为每种基本数据类型提供包装器类型在此不进行阐述,有兴趣的朋友可以查阅相关资料。在Java SE5之前,如果要生成一个数值为10的Integer对象,必须这样进行:
Integer i = new Integer(10);
而在从Java SE5开始就提供了自动装箱的特性,如果要生成一个数值为10的Integer对象,只需要这样就可以了:
Integer i = 10;
这个过程中会自动根据数值创建对应的 Integer对象,这就是装箱。
那什么是拆箱呢?顾名思义,跟装箱对应,就是自动将包装器类型转换为基本数据类型:
Integer i = 10; //装箱
int n = i; //拆箱
简单一点说,装箱就是 自动将基本数据类型转换为包装器类型;拆箱就是 自动将包装器类型转换为基本数据类型。
Integer装箱与缓存
Integer装箱是通过类内valueOf()方法,其中缓存了-128 to 127的数据。所以System.identityHashCode()中的a与b通过3装箱后,得到的Integer对象相同,c与d通过200装箱获取不同对象,所以内存地址不同。
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.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作为锁
@Slf4j
public class SyncDemo1 {
public static void main(String[] args) {
Thread why = new Thread(new TicketConsumer(), "test-1");
Thread mx = new Thread(new TicketConsumer(), "test-2");
why.start();
mx.start();
}
}
@Slf4j
class TicketConsumer implements Runnable {
private static volatile Integer ticket = 20;
public TicketConsumer() {
}
@Override
public void run() {
while (true) {
log.info(Thread.currentThread().getName() + "开始抢第" + ticket + "张票,对象加锁之前:" + System.identityHashCode(ticket));
synchronized (TicketConsumer.ticket) {
log.info(Thread.currentThread().getName() + "抢到第" + ticket + "张票,成功锁到的对象:" + System.identityHashCode(ticket));
if (ticket > 0) {
try {
//模拟抢票延迟
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread().getName() + "抢到了第" + ticket-- + "张票,票数减一");
} else {
return;
}
}
}
}
}
通过上方代买执行结果为:
19:19:15.087 [test-1] INFO - test-1开始抢第20张票,对象加锁之前:1390995099
19:19:15.087 [test-2] INFO - test-2开始抢第20张票,对象加锁之前:1390995099
19:19:15.120 [test-1] INFO - test-1抢到第20张票,成功锁到的对象:1390995099
19:19:20.122 [test-1] INFO - test-1抢到了第20张票,票数减一
19:19:20.122 [test-1] INFO - test-1开始抢第19张票,对象加锁之前:2037644894
19:19:20.122 [test-2] INFO - test-2抢到第19张票,成功锁到的对象:2037644894
19:19:20.122 [test-1] INFO - test-1抢到第19张票,成功锁到的对象:2037644894
19:19:25.123 [test-2] INFO - test-2抢到了第19张票,票数减一
19:19:25.123 [test-1] INFO - test-1抢到了第18张票,票数减一
19:19:25.123 [test-2] INFO - test-2开始抢第17张票,对象加锁之前:2132725761
19:19:25.123 [test-1] INFO - test-1开始抢第17张票,对象加锁之前:2132725761
19:19:25.123 [test-2] INFO - test-2抢到第17张票,成功锁到的对象:2132725761
出现了test-1、test-2同时输出抢到第19张票,成功锁到的对象:2037644894的情况。而且从System.identityHashCode(ticket))输出结果相同相同。
查看内存说明出现如下情况
抢第20张票
"test-1" #12 prio=5 os_prio=0 tid=0x000000001f640800 nid=0x61f4 waiting for monitor entry [0x000000002092e000]
java.lang.Thread.State: BLOCKED (on object monitor)
at org.hr.thread.examples.TicketConsumer.run(SyncDemo1.java:32)
- waiting to lock <0x000000076b5fbca0> (a java.lang.Integer)
at java.lang.Thread.run(Thread.java:745)
"test-2" #13 prio=5 os_prio=0 tid=0x000000001f641800 nid=0x6270 waiting on condition [0x0000000020a2f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at org.hr.thread.examples.TicketConsumer.run(SyncDemo1.java:36)
- locked <0x000000076b5fbca0> (a java.lang.Integer)
at java.lang.Thread.run(Thread.java:745)
抢第19张票
"test-2" #13 prio=5 os_prio=0 tid=0x000000001f641800 nid=0x6270 waiting on condition [0x0000000020a2f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at org.hr.thread.examples.TicketConsumer.run(SyncDemo1.java:36)
- locked <0x000000076b5fbc90> (a java.lang.Integer)
at java.lang.Thread.run(Thread.java:745)
"test-1" #12 prio=5 os_prio=0 tid=0x000000001f640800 nid=0x61f4 waiting on condition [0x000000002092e000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at org.hr.thread.examples.TicketConsumer.run(SyncDemo1.java:36)
- locked <0x000000076b5fbca0> (a java.lang.Integer)
at java.lang.Thread.run(Thread.java:745)
test-1、test-2第一轮抢票持有内存地址0x000000076b5fbca0的锁。
但是到了第二轮,分别持有0x000000076b5fbca0、0x000000076b5fbc90的锁。
说明对象地址发生了变化,并且锁不在存在互斥关系,所以synchronized失效!线程出现安全问题。
问题分析
文章中提出了非常重要的解析
第一次 Dump 的时候,ticket 都是 10,其中 mx 没有抢到锁,被 synchronized 锁住。
why 线程执行了
ticket--操作,ticket 变成了 9,但是此时 mx 线程被锁住的 monitor 还是 ticket=10 这个对象,它还在 monitor 的 _EntryList 里面等着的,并不会因为 ticket 的变化而变化。所以,当 why 线程释放锁之后,mx 线程拿到锁继续执行,发现 ticket=9。
而 why 也搞到一把新锁,也可以进入 synchronized 的逻辑,也发现 ticket=9。
好家伙,ticket 都是 9, System.identityHashCode 能不一样吗?
按理来说,why 释放锁一后应该继续和 mx 竞争锁一,但是却不知道它在哪搞到一把新锁。
后续跟文章作者继续请教帮助分析问题,按照我的思路理解(可能存在错误)
因ticket是static修饰,锁归属于类。在Integer封箱过程中产生的新对象,也被类的monitor的加入_EntryList。所以why、mx线程都能获取到自己的锁,避免了互斥。
解决思路
- 锁改为使用类
- 不使用Integer
- 使用Map方式获取Integer值(例如ConcurrentHashMap)
巨人的肩膀
当Synchronized遇到这玩意儿,有个大坑,要注意! (qq.com)
深入剖析Java中的装箱和拆箱 - Matrix海子 - 博客园 (cnblogs.com)
本文来自博客园,作者:疯狂马铃薯,转载请注明原文链接:https://www.cnblogs.com/hr0552/p/15933287.html

浙公网安备 33010602011771号