看山聊并发:如果非要在多线程中使用 ArrayList 会发生什么?(第二篇)-阿里云开发者社区

开发者社区> 看山灬> 正文

看山聊并发:如果非要在多线程中使用 ArrayList 会发生什么?(第二篇)

简介: 前面写过一篇文章 《如果非要在多线程中使用 ArrayList 会发生什么?》,有读者反馈,Java 11 代码已经修复,还会出现 null 元素。 为了便于理解,当时只是通过代码执行顺序说明了异常原因。
+关注继续查看

image.png

你好,我是看山。


前面写过一篇文章 《如果非要在多线程中使用 ArrayList 会发生什么?》,有读者反馈,Java 11 代码已经修复,还会出现 null 元素。


为了便于理解,当时只是通过代码执行顺序说明了异常原因。其实多线程中还会涉及 Java 内存模型,本文就从这方面说明一下。


对比源码

我们先来看看 Java 11 中,add方法做了什么调整。


Java 8 中add方法的实现:


public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

Java 11 中add方法的实现:


public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}

private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}

两段逻辑的差异在于数组下标是否确定:


elementData[size++] = e;,Java 8 中直接使用size定位并赋值,然后通过size++自增

elementData[s] = e; size = s + 1;,Java 11 借助临时变量s定位并赋值,然后通过size = s + 1给size赋新值

Java 11 的优点在于,为数组指定元素赋值的时候,下标值是确定的。也就是说,只要进入add(E e, Object[] elementData, int s)方法中,就只会处理指定位置的数组元素。并且,size的值也是根据s增加。按照执行顺序推断,最终的结果可能会丢数,但是不会出现 null。(多个线程向同一个下标赋值,即s相等,那最终size也相等。)


验证一下

让我们来验证下。


package com.kuaishou.is.datamart;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        CountDownLatch latch = new CountDownLatch(1);
        CountDownLatch waiting = new CountDownLatch(3);
        Thread t1 = new Thread(() -> {
            try {
                latch.await();
                for (int i = 0; i < 1000; i++) {
                    list.add("1");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                waiting.countDown();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                latch.await();
                for (int i = 0; i < 1000; i++) {
                    list.add("2");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                waiting.countDown();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                latch.await();
                for (int i = 0; i < 1000; i++) {
                    list.add("2");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                waiting.countDown();
            }
        });
        t1.start();
        t2.start();
        latch.countDown();
        waiting.await();
        System.out.println(list);
    }
}

在 Java 8 和 Java 11 中分别执行,果然,出现了ArrayIndexOutOfBoundsException和null的情况。如果没有出现,那就是姿势不对,需要多试几次或者多几个线程。


换个角度想问题

上一篇通过代码执行顺序解释了出现问题的原因,这次再看看 JMM 的原因。

image.png



从上图我们可以看到,Java 为每个线程创建了一个本地内存区域,也就是说,代码运行过程中使用的数据,是线程本地缓存的数据。这份缓存的数据,会与主内存的数据做交换(更新主内存数据或更新本次缓存中的数据)。


我们通过一个时序图看下为什么会出现 null(数组越界异常同理):

image.png



从时序图我们可以看出现,在执行过程中,两个线程取的size值和elementData数组地址,大部分是操作自己本地缓存中的,执行一段时间后,会将本地缓存中的数据写回主内存数据,然后还会从主内存中读取最新数据更新本地缓存数据。异常就在这个交换过程中发生了。


这个时候,可能有读者会想,是不是把size和elementData两个变量加上volatile就可以解决了。如果这样想,那你就想简单。线程安全是整个类设计实现时已经确定了,除了属性需要考虑多线程的影响,方法(主要是会修改属性元素的方法)也需要考虑。


ArrayList的定位是非线程安全的,其中的所有方法都没有考虑多线程下为共享资源加锁。即使size和elementData两个变量都是实时读写主内存,但是add和grow方法还是可能会覆盖另一个线程的数据。


我们从ArrayList的add方法注释可以得知,方法拆分不是为了实现线程安全,而是为了执行效率和内存占用:


This helper method split out from add(E) to keep method bytecode size under 35 (the -XX:MaxInlineSize default value), which helps when add(E) is called in a C1-compiled loop.


所以说,在多线程场景下使用ArrayList,该出现的异常,一个也不会少。


推荐阅读

如果非要在多线程中使用 ArrayList 会发生什么?

如果非要在多线程中使用 ArrayList 会发生什么?(第二篇)

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
10018 0
Tomcat使用线程池配置高并发连接
Tomcat使用线程池配置高并发连接1:配置executor属性 打开/conf/server.xml文件,在Connector之前配置一个线程池: namePrefix="tomcatThreadPool-" maxThreads="1000" maxIdleTime="300000" minSpareThreads="200"/> 重要参数说明:name:共享线程池的名字。
1054 0
WPF 同一窗口内的多线程/多进程 UI(使用 SetParent 嵌入另一个窗口)
原文 WPF 同一窗口内的多线程/多进程 UI(使用 SetParent 嵌入另一个窗口) WPF 的 UI 逻辑只在同一个线程中,这是学习 WPF 开发中大家几乎都会学习到的经验。如果希望做不同线程的 UI,大家也会想到使用另一个窗口来实现,让每个窗口拥有自己的 UI 线程。
1942 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
13821 0
一起谈.NET技术,在.NET Workflow 3.5中使用多线程提高工作流性能
  最近在工作上碰到一个性能问题,由于项目是基于SOA的架构,使得整个系统完全依赖于各种各样的Service,其中用于处理业务逻辑的Business Services全部都用.NET Workflow 3.5实现(历史原因,项目还没升级到Workflow 4)。
830 0
夯实Java基础系列17:一文搞懂Java多线程使用方式、实现原理以及常见面试题
本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下Star哈 文章首发于我的个人博客: www.how2playlife.com Java中的线程 Java之父对线程的定义是: 线程是一个独立执行的调用序列,同一个进程的线程在同一时刻共享一些系统资源(比如文件句柄等)也能访问同一个进程所创建的对象资源(内存资源)。
3502 0
java多线程--线程池的使用
程序启动一个新线程的成本是很高的,因为涉及到要和操作系统进行交互,而使用线程池可以很好的提高性能,尤其是程序中当需要创建大量生存期很短的线程时,应该优先考虑使用线程池.       线程池的每一个线程执行完毕后,并不会死亡,会再次回到线程池中变成空闲状态,等待下一个对象来调用,类比于数据库连接池.
680 0
+关注
看山灬
专注后端开发、架构相关知识分享,个人网站 https://howardliu.cn/。
136
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载