美文网首页线程集合安全
ArrayMap完全剖析之线程安全

ArrayMap完全剖析之线程安全

作者: 码上就说 | 来源:发表于2018-10-30 16:35 被阅读809次

概要

问题:java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Object[]

  • 问题背景介绍
  • root cause定位
  • 构建复现场景
  • 如何修改

背景介绍

《ArrayMap完全剖析》文章已经强调了ArrayMap是非线程安全的,多线程多谢ArrayMap的时候会出现异常,报错。当然ArrayMap设计的时候已经在代码中包含了这层意思。

/**
     * Attempt to spot concurrent modifications to this data structure.
     *
     * It's best-effort, but any time we can throw something more diagnostic than an
     * ArrayIndexOutOfBoundsException deep in the ArrayMap internals it's going to
     * save a lot of development time.
     *
     * Good times to look for CME include after any allocArrays() call and at the end of
     * functions that change mSize (put/remove/clear).
     */
    private static final boolean CONCURRENT_MODIFICATION_EXCEPTIONS = true;

这个变量就是为了保证多线程操作的时候ArrayMap会抛出ArrayIndexOutOfBoundsException或者ConcurrentModificationException,但是aosp代码没有覆盖到一种情况,下面请看操作ArrayMap发生问题的堆栈:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Object[]
at android.util.ArrayMap.allocArrays(ArrayMap.java:213)
at android.util.ArrayMap.put(ArrayMap.java:499)
at android.os.BaseBundle.putBoolean(BaseBundle.java:542)
at com.android.server.content.SyncManager.scheduleLocalSync(SyncManager.java:1241)
at com.android.server.content.ContentService.notifyChange(ContentService.java:461)
at android.content.ContentResolver.notifyChange(ContentResolver.java:2062)
at com.android.providers.settings.SettingsProvider$SettingsRegistry$MyHandler.handleMessage(SettingsProvider.java:2909)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:173)
at com.android.server.SystemServer.run(SystemServer.java:434)
at com.android.server.SystemServer.main(SystemServer.java:283)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:......

java.lang.ClassCastException 发生在android.util.ArrayMap.allocArrays,显然这个问题是ArrayMap没有考虑到的,那这个问题是什么情况下会发生的了?这是本文主要探讨的问题。

root cause定位

既然发生了这样的问题,我们就要根据问题分析出可能发生这个问题的场景,即root cause,只有分析出root cause,才能从根本上解决这类问题。

190    private void allocArrays(final int size) {
191        if (mHashes == EMPTY_IMMUTABLE_INTS) {
192            throw new UnsupportedOperationException("ArrayMap is immutable");
193        }
194        if (size == (BASE_SIZE*2)) {
195            synchronized (ArrayMap.class) {
196                if (mTwiceBaseCache != null) {
197                    final Object[] array = mTwiceBaseCache;
198                    mArray = array;
199                    mTwiceBaseCache = (Object[])array[0];
200                    mHashes = (int[])array[1];
201                    array[0] = array[1] = null;
202                    mTwiceBaseCacheSize--;
203                    if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
204                            + " now have " + mTwiceBaseCacheSize + " entries");
205                    return;
206                }
207            }
208        } else if (size == BASE_SIZE) {
209            synchronized (ArrayMap.class) {
210                if (mBaseCache != null) {
211                    final Object[] array = mBaseCache;
212                    mArray = array;
213                    mBaseCache = (Object[])array[0];
214                    mHashes = (int[])array[1];
215                    array[0] = array[1] = null;
216                    mBaseCacheSize--;
217                    if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
218                            + " now have " + mBaseCacheSize + " entries");
219                    return;
220                }
221            }
222        }
223
224        mHashes = new int[size];
225        mArray = new Object[size<<1];
226    }

问题发生在 213行mBaseCache = (Object[])array[0]; 这儿发生了
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Object[]
那就是说本来这个array[0]应该是一个Object[]数组类型,但是现在不知怎么变成了String,这个array[0]来自mBaseCache,查看整个ArrayMap源码,发现只有在allocArrays与freeArrays中用到了mBaseCache

    private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
        if (hashes.length == (BASE_SIZE*2)) {
            synchronized (ArrayMap.class) {
                if (mTwiceBaseCacheSize < CACHE_SIZE) {
                    array[0] = mTwiceBaseCache;
                    array[1] = hashes;
                    for (int i=(size<<1)-1; i>=2; i--) {
                        array[i] = null;
                    }
                    mTwiceBaseCache = array;
                    mTwiceBaseCacheSize++;
                    if (DEBUG) Log.d(TAG, "Storing 2x cache " + array
                            + " now have " + mTwiceBaseCacheSize + " entries");
                }
            }
        } else if (hashes.length == BASE_SIZE) {
            synchronized (ArrayMap.class) {
                if (mBaseCacheSize < CACHE_SIZE) {
                    array[0] = mBaseCache;
                    array[1] = hashes;
                    for (int i=(size<<1)-1; i>=2; i--) {
                        array[i] = null;
                    }
                    mBaseCache = array;
                    mBaseCacheSize++;
                    if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
                            + " now have " + mBaseCacheSize + " entries");
                }
            }
        }
    }

在allocArrays与freeArrays的时候都用到了 synchronized(ArrayMap.class)同步锁, 正常情况下是不太可能出现脏数据的,allocArrays使用的freeArrays中缓存的mBaseCache对象,但是我们要记住,这儿的赋值都死引用赋值。 这儿一定要记住,是对象,不是值。
那么是否存在这样的情况呢?

  • 我们再仔细查看freeArrays的代码,新缓存的mBaseCache[0]被赋值为mBaseCache,而在allocArrays中复用时读出来的值却是String,说明“array[0] = mBaseCache”操作后又重新修改了array[0] 。
  • freeArrays函数操作的hashes和array都是需要回收的对象,我们看看freeArrays四处调用,其中ensureCapacity(int minimumCapacity) 和 put(K key, V value)执行freeArrays前ArrayMap已经不再引用这对hashes和value,而removeAt(int index)和clear()是执行freeArrays回收操作后再对ArrayMap的mHashes和mArray重新赋值,也就是其它线程有可能在freeArrays“array[0] = mBaseCache”操作后对mArray进行修改,产生脏数据。对array[0] 进行重新赋值,可能是执行put操作时,新增的key的hash值刚好排第一,导致当前array第一个索引的值后移,或者remove操作时删除的刚好是第一个key,导致array后面的值前移。

构建复现场景

前面我们查看源码知道ArrayMap是非线程安全的,测试在用多线程中同时读写同一个ArrayMap可以产生各种crash,唯独ClassCastException很难触发。

  • ClassCastException需要满足两个条件:
  • 1、线程A刚好释放长度为4或者8的数组(clear或者remove操作)
  • 2、线程B在A执行“array[0] = mBaseCache”后修改了array[0] ,即新增或者删除的key的hash值刚好是mHash[0]

下面我制造了一个特别的测试用例,保证每次添加key-value时,刚好放到修改mHash[0],即每次都修改array[0];经过测试,发现发生ClassCastException的概率大于50%。

如果上述并发操作时put的key对应hash值没有添加到mHash[0],则并不会引发复用数组时的ClassCastException问题,但会带来脏数据,导致复用的Array是已经含有脏数据,且这种概率相对ClassCastException大得多。

测试代码:

ArrayMap<String, String> mMap;
 
private void testArrayMap() {
    new Thread() {
        public void run() {
            for (int i = 0; i < 100000; i++) {
                mMap = new ArrayMap<String, String>(4);//直接分配一个长度为4的数组,直接复用cache
                mMap.clear();//执行clear操作
            }
        }
    }.start();
 
    new Thread() {
        public void run() {
            for (int i = 0; i < 100000; i++) {
                mMap.remove("test1");   //清空数据
                mMap.put("test1", "haha");//保证新增数据添加到values[0]
            }
        }
    }.start();
 
}

本次测试出来的堆栈:

10-29 16:04:25.722 E/AndroidRuntime(24157): FATAL EXCEPTION: Thread-4
10-29 16:04:25.722 E/AndroidRuntime(24157): Process: company.xiaomi.com.testarraymap, PID: 24157
10-29 16:04:25.722 E/AndroidRuntime(24157): java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Object[]
10-29 16:04:25.722 E/AndroidRuntime(24157):     at android.util.ArrayMap.allocArrays(ArrayMap.java:214)
10-29 16:04:25.722 E/AndroidRuntime(24157):     at android.util.ArrayMap.<init>(ArrayMap.java:291)
10-29 16:04:25.722 E/AndroidRuntime(24157):     at android.util.ArrayMap.<init>(ArrayMap.java:274)
10-29 16:04:25.722 E/AndroidRuntime(24157):     at company.xiaomi.com.testarraymap.MainActivity$3.run(MainActivity.java:47)
10-29 16:04:25.723 E/AndroidRuntime(24157): FATAL EXCEPTION: main
10-29 16:04:25.723 E/AndroidRuntime(24157): Process: company.xiaomi.com.testarraymap, PID: 24157
10-29 16:04:25.723 E/AndroidRuntime(24157): java.lang.RuntimeException: Unable to start activity ComponentInfo{company.xiaomi.com.testarraymap/company.xiaomi.com.testarraymap.MainActivity}: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Object[]
10-29 16:04:25.723 E/AndroidRuntime(24157):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2817)
10-29 16:04:25.723 E/AndroidRuntime(24157):     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2895)
10-29 16:04:25.723 E/AndroidRuntime(24157):     at android.app.ActivityThread.-wrap11(Unknown Source:0)
10-29 16:04:25.723 E/AndroidRuntime(24157):     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1616)
10-29 16:04:25.723 E/AndroidRuntime(24157):     at android.os.Handler.dispatchMessage(Handler.java:106)
10-29 16:04:25.723 E/AndroidRuntime(24157):     at android.os.Looper.loop(Looper.java:173)
10-29 16:04:25.723 E/AndroidRuntime(24157):     at android.app.ActivityThread.main(ActivityThread.java:6653)
10-29 16:04:25.723 E/AndroidRuntime(24157):     at java.lang.reflect.Method.invoke(Native Method)
10-29 16:04:25.723 E/AndroidRuntime(24157):     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
10-29 16:04:25.723 E/AndroidRuntime(24157):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:821)
10-29 16:04:25.723 E/AndroidRuntime(24157): Caused by: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Object[]
10-29 16:04:25.723 E/AndroidRuntime(24157):     at android.util.ArrayMap.allocArrays(ArrayMap.java:214)
10-29 16:04:25.723 E/AndroidRuntime(24157):     at android.util.ArrayMap.put(ArrayMap.java:501)
10-29 16:04:25.723 E/AndroidRuntime(24157):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2810)
10-29 16:04:25.723 E/AndroidRuntime(24157):     ... 9 more

测试结果汇总:
1.测试50次,出现 java.lang.ClassCastException 11次,测试了10轮,每轮都出现在9次以上。
2.测试结果说明:说明这种测试方法可以稳定复现 java.lang.ClassCastException

如何修改

  • 我们当然知道导致这样的结果是多线程调用ArrayMap导致的,但是因为android中Intent/Bundle/BaseBundle底层都是调用ArrayMap的,从目前手机framework的监控情况,还是有很多多线程操作ArrayMap的情况的,排查这些问题费时费力,工作量太大,而且ArrayMap本身只能解决两种多线程操作的情况:
    ArrayIndexOutOfBoundsException
    ConcurrentModificationException
    源码本身就没有提供解决ClassCastException的情况,这不得不说是源码的一个纰漏。
  • 而且最根本的方法还是修改ArrayMap的源码。修改的思路也很简单,就是在put函数中添加hashes数组的第一个索引时加锁。至于修改的源码,可以私信我。感谢大家。

Updated:
google源码已经提交了这部分的修改,修改方案如下:
http://androidxref.com/9.0.0_r3/history/frameworks/base/core/java/android/util/ArrayMap.java

相关文章

  • ArrayMap完全剖析之线程安全

    概要 问题:java.lang.ClassCastException: java.lang.String cann...

  • ArrayMap完全剖析

    ArrayMap是一种通用的key-value映射的数据结构,旨在提高内存效率,它与传统的HashMap有很大的不...

  • ArrayIndexOutOfBoundsException异常

    Hashmap不是线程安全的,不能并发使用android.util.ArrayMap 可能也不是线程安全的,不能并...

  • 线程安全之 ReentrantLock 完全解析

    线程互斥同步除了使用最基本的 synchronized 关键字外(关于 synchronized 关键字的实现原理...

  • 代码审查:从 ArrayList 说线程安全

    本文从代码审查过程中发现的一个 ArrayList 相关的「线程安全」问题出发,来剖析和理解线程安全。 案例分析 ...

  • 多线程之线程安全性

    多线程环境下使用非线程安全类会导致线程安全问题。线程安全问题表现为原子性,有序性,可见性 在讲述线程安全三大特性之...

  • 待看文章

    内存相关 【基本功】深入剖析Swift性能优化 渲染相关 关于iOS离屏渲染的深入研究 线程相关 iOS-线程安全

  • Guava(5) collect.Immutable

    前面 不可变代表着线程安全,线程调用时完全不用考虑线程安全的问题(不过个人认为意义不大,因为这个不可变是在封锁修改...

  • Android 多线程探索(四)— 同步集合

    前言 Android JDK 提供了一系列线程安全的集合,避免多线程环境下由于线程安全导致的各种问题。 一、程序之...

  • 线程同步及通信机制

    线程同步 线程同步是保证多线程安全访问竞争资源的一种手段 线程间通信 线程间往往需要协调,共同完全某项工作,需要线...

网友评论

    本文标题:ArrayMap完全剖析之线程安全

    本文链接:https://www.haomeiwen.com/subject/ynawtqtx.html