Java并发编程之集合类安全问题-------举例说明线程不安全 面试必备

一.场景引入

  • 举例说明线程不安全
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不一样了,如果不一样了,说明别的线程改变了集合结构,这样就会报异常


结论:

  1. 多线程状态进行操作有可能不报异常,如果你多试几次,会发现有时即便是30个线程同时操作,都不会出错,但是线程越多,出错几率越大
  2. 出错的原因是,我们的print语句中调用了list的toString方法,其内部会调用Iterator的next函数,如果在我们遍历的过程中,有线程改变了这个list的结构,让modCount属性值发生了变化,那么会报异常
  3. 3个线程不报异常的原因:在核心的checkForComodification方法中,也就是在迭代器调用next()方法这短暂的一个时间内,这个list并没有发生改变,所以就没有抛出异常,这也能说明为什么线程越多,就越容易出错,因为多线程很挤,所以在那个短暂的时间内,其他线程有更大的机会改变modCount属性。

解决策略

  1. 用Vector(线程安全) 原因:它的add方法被Synchronized修饰
  2. Collections.synchronizedList Collections工具类
  3. JUC中的一些类 比如CopyOnWriteArrayList 这个占个坑之后写
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值