一.场景引入
- 举例说明线程不安全
public class NotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
//List<String> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 1; i <= 3; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8)); //8位随机字串 写操作
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
这个main函数中,使用ArrayList新建了一个集合,通过for循环创建三个线程,每个线程的任务是向list中添加一个8位长度的随机字符串然后打印。
执行结果:
"C:\Program Files\Java\jdk1.8.0_181\bin\java.exe" "-javaagent:D:\idea\IntelliJ IDEA 2019.2.3\lib\idea_rt.jar=58142:D:\idea\IntelliJ IDEA 2019.2.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_181\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\rt.jar;C:\Users\李肇京\IdeaProjects\JUC\out\production\JUC" JUC_01_SellTicket.Collection.NotSafeDemo
[null, a91896f4]
[null, a91896f4, 9dc4fd58]
[null, a91896f4]
Process finished with exit code 0
此时你会发现,每次结果居然都不一样,有时候三行都是三个,有时候一行一个一行两个,这是由于我们的线程都是在很短的时间内执行的,有可能i=1的线程还没打印,i=2的线程就已经对list进行了修改,所以结果是千变万化的
但是,没有出现异常状况
接下来,把循环的条件 i<=3 改为 i<=30
会发现出现了异常
"C:\Program Files\Java\jdk1.8.0_181\bin\java.exe" "-
java.util.ConcurrentModificationException
[null, a5f886b3, e6e730cc, a2d2b02d, f7cf9da7, a79881ad, 31926de4, 0c5d0cc9, e35ca5f2, 80e67e6f, defddeb6, 0c63eb2d, 987d693b, a45d90e3, 607bf94a, ace2ab82, 0ade8d9f, 9abd3bba, 677d33cc, 8af5fcb5, 2c17d11b, 6c03c8f9, 13533ea9, ffd592eb, c7eadf2c, dc946997, 4e619171]
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at java.util.AbstractCollection.toString(AbstractCollection.java:461)
at java.lang.String.valueOf(String.java:2994)[null, a5f886b3, e6e730cc, a2d2b02d, f7cf9da7, a79881ad, 31926de4, 0c5d0cc9, e35ca5f2, 80e67e6f, defddeb6, 0c63eb2d, 987d693b, a45d90e3, 607bf94a, ace2ab82, 0ade8d9f, 9abd3bba, 677d33cc, 8af5fcb5, 2c17d11b, 6c03c8f9, 13533ea9, ffd592eb, c7eadf2c, dc946997]
at java.io.PrintStream.println(PrintStream.java:821)
at JUC_01_SellTicket.Collection.NotSafeDemo.lambda$main$0(NotSafeDemo.java:40)
at java.lang.Thread.run(Thread.java:748)
[null, a5f886b3, e6e730cc, a2d2b02d, f7cf9da7, a79881ad, 31926de4, 0c5d0cc9, e35ca5f2, 80e67e6f, defddeb6, 0c63eb2d, 987d693b, a45d90e3, 607bf94a, ace2ab82, 0ade8d9f, 9abd3bba, 677d33cc, 8af5fcb5, 2c17d11b, 6c03c8f9, 13533ea9, ffd592eb, c7eadf2c, dc946997, 4e619171, 5d1c66a8, cda9d58a]
让我们跟进java.util.ConcurrentModificationException 这个异常出现的位置看一看:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
是这个checkForComodification()函数报错,其内部逻辑是:只要modCount这个值和expectedModCount这个值不同,那么就会报我们刚才遇见的异常
- modCount是集合类内部维护的一个属性,它会在集合被改变,比如add,remove的操作中自增,具体可以看我之前的文章:modCount是什么
那么出错的流程是什么呢,我们用println后,会调用list的toString方法:
public String toString() { //toString方法调用了next()
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();//就是在这里出错了!!!
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
再往前看一步,此处我们通过System.out.println打印了集合,其中调用了list的toString方法,其内部会调用.next()函数,这个函数在遍历的时候,每次都会调用checkForComodification()进行检查,检查modCount是否跟expectedModCount不一样了,如果不一样了,说明别的线程改变了集合结构,这样就会报异常
结论:
- 多线程状态进行操作有可能不报异常,如果你多试几次,会发现有时即便是30个线程同时操作,都不会出错,但是线程越多,出错几率越大
- 出错的原因是,我们的print语句中调用了list的toString方法,其内部会调用Iterator的next函数,如果在我们遍历的过程中,有线程改变了这个list的结构,让modCount属性值发生了变化,那么会报异常
- 3个线程不报异常的原因:在核心的checkForComodification方法中,也就是在迭代器调用next()方法这短暂的一个时间内,这个list并没有发生改变,所以就没有抛出异常,这也能说明为什么线程越多,就越容易出错,因为多线程很挤,所以在那个短暂的时间内,其他线程有更大的机会改变modCount属性。
解决策略
- 用Vector(线程安全) 原因:它的add方法被Synchronized修饰
- Collections.synchronizedList Collections工具类
- JUC中的一些类 比如CopyOnWriteArrayList 这个占个坑之后写