关于并发操作容器的一些心得

当多个线程同时访问修改容器数据时,可能会造成数据不安全问题

1.不安全版

ArrayList<String> arrayList = new ArrayList<>();
        for (int i = 0;i<10000;i++){
            new Thread(()->{
                arrayList.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(arrayList.size());//9992

可以看到输出结果容器的长度是小于10000的,当时我想会不会是因为延迟小的关系呢?
我再次将延迟数调大(20000ms),发现容器的长度依然不到10000,仿佛有些添加进去的数据凭空消失了。咱们再来一个安全版的。

2.安全版 CopyOnWriteArrayList

CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();
        for (int i = 0;i<10000;i++){
            new Thread(()->{
                arrayList.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(arrayList.size());//10000

运行后发现这次的容器长度确实达到了10000,没缺没漏,这是因为new CopyOnWriteArrayList() 返回的是一个线程安全的ArrayList对象

----------------------------------------------------------------------------------------------
现在咱们来分析一下:

单核CPU在执行多线程任务的时候,同一时间CPU只能执行一个线程,对于多个线程是实行时间片这个概念的,就是说CPU在多个线程之间来回切换,宏观上感觉好像是同时进行的,但其实并不是。这就是并发。

如果并发操作对于共享数据是只读的,那么不会有安全问题,如果修改共享数据,就可能会出现数据安全(线程安全问题)。

例如咱们创建了10000个线程,每个线程共享这个容器,都要往容器里放东西,
又因为CPU在10000个线程之间来回切换(这里让主线程阻塞了10S,足够其他线程运行完毕),而这个切换时机,频率完全由CPU控制,

所以可能会出现这种情况,当一个线程(线程1)准备往容器第一个空位放东西的时候CPU切换到另一个线程(线程2),而恰巧这个线程2也要往容器中放东西,此时刚刚呢个线程1和当前线程2发现同一个地方的位置是空的,假设这次线程2放入了东西,而当下次CPU切换到线程1执行的时候,线程1并不知道它准备放的地方已经有线程2放了,所以线程1就会覆盖这个位置,所以线程2的东西丢失,容器的总长度就不会达到10000。(这里不要看容器放入的add方法只有一条语句,点开看看)。

那么如何保证线程的安全性呢,保证线程安全就要保证数据一致性,就是同步操作。CopyOnWriteArrayList 内部就是对ArrayList对象保持一致性。在一个线程访问容器的时候给该容器加上一把锁,当CPU切换到其他线程访问该容器的时候发现自己没有钥匙,所以就会阻塞,只能等待加这把锁的线程释放锁才能继续访问(所以线程安全的运行效率不高),这样是不是意味着下一个拿到锁的线程总是访问上一个线程访问过的(修改过的)容器,这样就可以保证容器数据的安全性了。下面咱们手动给容器加个锁

3.安全版 同步 synchronized 同步块

ArrayList<String> arrayList = new ArrayList<>();
        for (int i = 0;i<10000;i++){
            new Thread(()->{
                synchronized (arrayList) {//这里对容器加锁
                    arrayList.add(Thread.currentThread().getName());
                }//代码块结束,释放锁 此时线程名已经放进容器中啦
            }).start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(arrayList.size());//10000

结果如分析的一致,完整向容器中加入了10000个线程名。这里要注意如果主线程的阻塞时间太小的话,还没有完全把10000个线程放入容器,可能就会执行输出容器长度语句,但是同步过的容器 添加多少就是多少 不会漏,比如这个循环语句执行了230次,就输出了长度,那么容器的长度就是230,不会有覆盖的情况出现。

其实并发线程安全问题的场景还有很多,如银行存取钱,网上购票,多点登陆。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值