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的锁。

但是到了第二轮,分别持有0x000000076b5fbca00x000000076b5fbc90的锁。

说明对象地址发生了变化,并且锁不在存在互斥关系,所以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线程都能获取到自己的锁,避免了互斥。

解决思路

  1. 锁改为使用类
  2. 不使用Integer
  3. 使用Map方式获取Integer值(例如ConcurrentHashMap)

巨人的肩膀

当Synchronized遇到这玩意儿,有个大坑,要注意! (qq.com)

深入剖析Java中的装箱和拆箱 - Matrix海子 - 博客园 (cnblogs.com)

由一个多线程共享Integer类变量问题引起的。。。 - silyvin - 博客园 (cnblogs.com)

(19条消息) System.identityHashCode()方法_shiwenbo1994的博客-CSDN博客

posted @ 2022-02-24 19:46  疯狂马铃薯  阅读(365)  评论(0)    收藏  举报