概要
问题: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
![](https://img.haomeiwen.com/i3768281/da0ecdd3eac6b83a.png)
网友评论