java数组下标越界_BUG-并行流与数组下标越界-思考与总结

BUG-并行流与数组下标越界-思考与总结

今天线上环境报异常,发现了一个之前没注意过的问题,记录一下。

1. 异常信息

异常信息如下:

···

Caused by: java.lang.ArrayIndexOutOfBoundsException

at java.lang.String.getChars(String.java:826)

at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:449)

at java.lang.StringBuilder.append(StringBuilder.java:136)

···

产生bug的代码改写后如下:

public static void main(String[] args) {

List lists = Lists.newArrayList();

for (int i = 0; i < 10; i++) {

lists.add(i);

}

for (int i = 0; i < 100; i++) {

StringBuilder sb = new StringBuilder();

// StringBuffer sb = new StringBuffer();

lists.parallelStream().forEach(p -> {

sb.append(p);

// 可以明显看到,拼接的字符串长度越大,异常越容易发生

sb.append("----------------------------------------");

// stringBuilder.append("-");

});

System.out.println(i + ": " + sb.toString());

}

}

2. 异常追踪分析

从上面的信息可以看出,是StringBuilder.append使用时,产生了数组下标越界异常。下面是代码追踪:

@Override

public StringBuilder append(String str) {

// 1

super.append(str);

return this;

}

public AbstractStringBuilder append(String str) {

if (str == null)

return appendNull();

int len = str.length();

// 2.1

// 检查现有字符串加上要拼接的字符串以后,长度是否超出内部数组的最大长度,如果超出,则会分配一个新的内部数组,确保数组能装的下拼接后的字符串

ensureCapacityInternal(count + len);

// 2.2

str.getChars(0, len, value, count);

count += len;

return this;

}

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {

if (srcBegin < 0) {

throw new StringIndexOutOfBoundsException(srcBegin);

}

if (srcEnd > value.length) {

throw new StringIndexOutOfBoundsException(srcEnd);

}

if (srcBegin > srcEnd) {

throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);

}

// 3

System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);

}

public static native void arraycopy(Object src, int srcPos,

Object dest, int destPos,

int length);

一路追踪代码,可以无论是2.1还是2.2,最终都调用了本地方法arraycopy(),这里抛出的异常。

本地方法定义如下:

/* Only register the performance-critical methods */

static JNINativeMethod methods[] = {

{"currentTimeMillis", "()J", (void *)&JVM_CurrentTimeMillis},

{"nanoTime", "()J", (void *)&JVM_NanoTime},

{"arraycopy", "(" OBJ "I" OBJ "II)V", (void *)&JVM_ArrayCopy},

};

JNIEXPORT void JNICALL

JVM_ArrayCopy(JNIEnv *env, jclass ignored, jobject src, jint src_pos,

jobject dst, jint dst_pos, jint length);

/*

java.lang.System中的arraycopy方法

*/

JVM_ENTRY(void, JVM_ArrayCopy(JNIEnv *env, jclass ignored, jobject src, jint src_pos,

jobject dst, jint dst_pos, jint length))

JVMWrapper("JVM_ArrayCopy");

// Check if we have null pointers

if (src == NULL || dst == NULL) {

THROW(vmSymbols::java_lang_NullPointerException());

}

arrayOop s = arrayOop(JNIHandles::resolve_non_null(src));

arrayOop d = arrayOop(JNIHandles::resolve_non_null(dst));

assert(oopDesc::is_oop(s), "JVM_ArrayCopy: src not an oop");

assert(oopDesc::is_oop(d), "JVM_ArrayCopy: dst not an oop");

// Do copy

s->klass()->copy_array(s, src_pos, d, dst_pos, length, thread);

JVM_END

这里发现JVM_ArrayCopy()只是简单的检测源数组和目的数组不为空,排除一些异常情况,并没有复制数组,而是调用了s->klass()->copy_array()方法来实现。源码如下:

/**

* java.lang.System中的arraycopy方法具体实现

*

* @param s 源数组

* @param src_pos 源数组开始复制的下标

* @param d 目标数组

* @param dst_pos 目标数组开始覆盖的下标

* @param length 要复制的数组元素数量

* @param TRAPS 线程信息

*/

void ObjArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d,

int dst_pos, int length, TRAPS) {

···

// Check is all offsets and lengths are non negative

// 检查所有的偏移量和长度是否非负

if (src_pos < 0 || dst_pos < 0 || length < 0) {

···

THROW_MSG(vmSymbols::java_lang_ArrayIndexOutOfBoundsException(), ss.as_string());

}

// Check if the ranges are valid

// 检查数组边界是否合法,如果

// 1.要复制的数组元素数量 + 源数组开始复制的下标 > 源数组长度

// 2.要复制的数组元素数量 + 目标数组开始覆盖的下标 > 目标数组长度

// 两种情况中有一种,就抛出数组下标越界异常

if ((((unsigned int) length + (unsigned int) src_pos) > (unsigned int) s->length()) ||

(((unsigned int) length + (unsigned int) dst_pos) > (unsigned int) d->length())) {

···

THROW_MSG(vmSymbols::java_lang_ArrayIndexOutOfBoundsException(), ss.as_string());

}

···

}

阅读源码及注释可以知道,上面两种情况下,都会抛出ArrayIndexOutOfBoundsException。

到这里我们可以猜测出异常抛出的原因了:因为append()方法是在多线程(parallelStream并行流)中调用的,所以可能有两个或者多个线程通过了ensureCapacityInternal()方法的空间校验,而实际空间不足而导致了数组下标越界。

例如有A、B两个线程,都需要拼接一个长度为40的字符串,而当前剩余空间为50。

当A通过ensureCapacityInternal()检验且为执行getChars()方法时被挂起,这时B线程通过ensureCapacityInternal()对空间进行校验是可以通过的,因为40<50。

接下来当A、B线程进行数组复制时,后复制的那个线程将出现数组下标越界异常,因为第一个线程复制完成后,剩下空间只有10。10<40而导致空间不足,下标越界。

3. 其他问题

3.1在测试代码中,我们可以很容易观察到,拼接的字符串长度越大,异常越容易发生。

我们分析下面的源码:

private void ensureCapacityInternal(int minimumCapacity) {

// overflow-conscious code

if (minimumCapacity - value.length > 0) {

value = Arrays.copyOf(value,

newCapacity(minimumCapacity));

}

}

private int newCapacity(int minCapacity) {

// 默认新数组容量为原数组的两倍+2

int newCapacity = (value.length << 1) + 2;

// 如果原数组的两倍+2还是小于需要的最小所需容量,则取最小所需容量为新数组容量

if (newCapacity - minCapacity < 0) {

newCapacity = minCapacity;

}

// 如果获取容量数值溢出,或者大于最大数组容量,则特殊处理(小于int最大值,则正常返回,否则抛出异常)

return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)

? hugeCapacity(minCapacity)

: newCapacity;

}

public static char[] copyOf(char[] original, int newLength) {

char[] copy = new char[newLength];

System.arraycopy(original, 0, copy, 0,

Math.min(original.length, newLength));

return copy;

}

可以看到ensureCapacityInternal()方法的入参minimumCapacity是源内部数组已存放的字符串长度+要拼接的字符串长度,只有源内部数组的总长度小于minimumCapacity,才会调用newCapacity()方法获取新内部数组的长度,然后调用copyOf()方法将源数组的元素复制到新内部数组。

分析可以得出原因:

字符串的内部数组,默认的长度是16,如果循环拼接最终的字符串长度小于16,则这个异常不会发生。

因为内部数组每次扩容,都是原数组长度x2+2,所以拼接的字符串长度越长,循环前几次,遇到长度不够报异常的可能性越大,触发异常所需要的的循环次数越少。

3.2 并行流parallelStream()使用问题

parallelStream提供了流的并行处理,它是Stream的另一重要特性,其底层使用Fork/Join框架实现。

Fork/Join 框架的核心是采用分治法的思想,将一个大任务拆分为若干互不依赖的子任务,把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务。

同时,为了最大限度地提高并行处理能力,采用了工作窃取算法来运行任务,也就是说当某个线程处理完自己工作队列中的任务后,尝试当其他线程的工作队列中窃取一个任务来执行,直到所有任务处理完毕。所以为了减少线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

Fork/Join 的运行流程图

4f972297adda520bc32235e025d5547c.png

用下面的示例演示一下parallelStream的使用。

public static void main(String[] args) {

List lists = Lists.newArrayList();

for (int i = 0; i < 10; i++) {

lists.add(i);

}

lists.parallelStream().forEach(System.out::println);

lists.parallelStream().map(p->++p).forEach(System.out::println);

}

输出:

6 5 8 9 7 1 0 2 4 3

7 9 3 10 2 6 1 8 5 4

我们发现,使用parallelStream后,结果并不按照集合原有顺序输出。为了进一步证明该操作是并行的,我们打印出线程信息。

public static void main(String[] args) {

List lists = Lists.newArrayList();

for (int i = 0; i < 10; i++) {

lists.add(i);

}

lists.parallelStream().forEach(num -> System.out.println(num + "--" +

Thread.currentThread().getName()));

}

输出:

6--main

8--ForkJoinPool.commonPool-worker-2

1--ForkJoinPool.commonPool-worker-3

2--ForkJoinPool.commonPool-worker-1

9--ForkJoinPool.commonPool-worker-2

5--main

3--ForkJoinPool.commonPool-worker-2

4--ForkJoinPool.commonPool-worker-1

7--ForkJoinPool.commonPool-worker-4

0--ForkJoinPool.commonPool-worker-3

如上,可以确信parallelStream是利用多线程进行的,这可以很大程度简化我们在需要的时候,进行并行操作。例如第一个例子中,对所有集合元素进行自增操作。尤其是当数据量非常庞大的时候,并行流的数据处理将具有无与伦比的优势。

但同时,并行流也是一把双刃剑,使用不当,也会引发不好后果,比如我这次碰到的线上bug,还有就是bug代码中,使用并行流进行字符串拼接,我认为也是一种非常不好的用法,因为字符串拼接是,我们往往是追求有序地拼接,这样文本语意才会符合我们的预期,但是使用并行流很明显不能满足这一点。

由于并行流使用多线程,则一切线程安全问题都应该是需要考虑的问题,如:资源竞争、死锁、事务、可见性等等。

4. 总结

4.1 bug修复方法

使用串行流stream();

使用线程安全的StringBuffer。

结合前面讨论与思考,适合第一种方式,毕竟数据量也不大。

4.2 并行流 or 串行流

parallelStream是一把同时有巨大隐患和好处的双刃剑,那么,使用如何选择,我们可以考虑以下几个问题:

是否需要并行?

任务之间是否是独立的?是否会引起任何竞态条件?

结果是否取决于任务的调用顺序?

对于问题1,在回答这个问题之前,你需要弄清楚你要解决的问题是什么,数据量有多大,计算的特点是什么?并不是所有的问题都适合使用并发程序来求解,比如当数据量不大时,顺序执行往往比并行执行更快。毕竟,准备线程池和其它相关资源也是需要时间的。但是,当任务涉及到I/O操作并且任务之间不互相依赖时,那么并行化就是一个不错的选择。通常而言,将这类程序并行化之后,执行速度会提升好几个等级。

对于问题2,如果任务之间是独立的,并且代码中不涉及到对同一个对象的某个状态或者某个变量的更新操作,那么就表明代码是可以被并行化的。

对于问题3,由于在并行环境中任务的执行顺序是不确定的,因此对于依赖于顺序的任务而言,并行化也许不能给出正确的结果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/525674.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IDC敲黑板啦:未来企业IT以混合云为主

数字化是企业转型的必由之路数字化技术正在融入企业的血液里。IDC 认为&#xff0c;基于第三平台的 46 技术是企业在数字化转型过程中的关键要素&#xff1a;• 4 是指以云计算、大数据、社交商业与移动技术为代表的第三平台技术。• 6 则是基于第三平台智商的 6 大创新加速器。…

Service Mesh 在华为公有云的实践

我们在构建微服务而构建微服务是困难的微服务是一个很大的概念&#xff0c;从团队组织到最佳实践似乎都有实施微服务的一些指导。我们这里只提构建微服务的架构模式&#xff0c;也就是关乎到你用什么样的方式来构建你以微服务架构来组织的应用系统。近些年随着微服务的火热&…

分布式NewSQL数据库实践——民生银行经典案例

前言此前&#xff0c;金融信息化建设主要依托原有集中型 IT 架构进行维护扩展&#xff0c;系统规模及复杂程度呈指数级增长&#xff0c;各类瓶颈逐渐暴露&#xff0c;日益增长的数字金融需求同旧式的系统架构缺陷之间的矛盾愈加凸显。中国人民银行、中国银行保险监督管理委员会…

「拨云见日」英特尔揭秘短视频背后的二三事

“像一棵海草海草&#xff0c;随风飘摇&#xff0c;海草海草&#xff0c;浪花里舞蹈……”看到这样魔性的歌词&#xff0c;你是不是有立刻跟着唱的冲动&#xff0c;甚至还很想起来跳一段呢&#xff1f;去火锅店点餐&#xff0c;你会掏出手机告诉服务员按照视频内容操作吗&#…

英特尔助力金山云带你畅游云端的游戏世界

科技的发展让人们可以在任意时间、任意地点与不同的玩家一同畅游游戏世界。不论是拥挤的通勤路上&#xff0c;还是热闹的餐厅&#xff0c;都少不了痴迷于手游的玩家。来自《2017年中国游戏产业报告&#xff08;摘要版&#xff09;》的数据表明&#xff0c;去年中国游戏市场全年…

数据洪流时代,企业转型需要修建自己的“都江堰”

科技的进步推动着人类文明的进化&#xff0c;从农业采集社会到如今的网络智能社会&#xff0c;文明的进化也同样带动了企业的“进化”。今天&#xff0c;人工智能、云计算、大数据等技术的不断突破&#xff0c;让网络产生的数据量呈爆发式增长&#xff0c;数据洪流汹涌奔来&…

时代变了

阅读文本大概需要 2.6 分钟。最近一段时间&#xff0c;经常有人问我这么一个问题&#xff0c;说&#xff0c;张哥&#xff0c;现在市面上有各种付费网课和付费专栏&#xff0c;但我总觉得只有看书学习才是正途&#xff0c;不知道张哥怎么看&#xff1f;到底哪种学习方式最好呢&…

终于有人把什么是云计算、大数据和人工智能讲明白了!云计算是什么?

今天跟大家讲讲云计算、大数据和人工智能。为什么讲这三个东西呢&#xff1f;因为这三个东西现在非常火&#xff0c;并且它们之间好像互相有关系&#xff0c;可是很多人却不知道什么是云计算或者云计算应用在哪&#xff1a;一般谈云计算的时候会提到大数据、谈人工智能的时候会…

数据洪流来袭,企业转型势不可挡,如何四两拨千斤?

在漫长的历史里&#xff0c;文明的进步都是伴随着科技的发展&#xff0c;企业也在不断进化&#xff0c;无论是商业战略还是商业模式&#xff0c;在科技的推动下与时俱进&#xff0c;不断更迭创新。历史的长河流入数据洪流的时代&#xff0c;人工智能、大数据、云计算等新技术掀…

福利 | 2018 OpenInfra Days China限量版免费票任性放出

号外号外&#xff01;福利来袭&#xff0c;手速up up up~春困夏乏秋盹冬眠暑气炎炎&#xff0c;OpenInfra帮你提神醒脑——特别好礼限量放送Ready&#xff1f;Go&#xff01;2018 年 6 月 21-22 日&#xff0c;OpenInfra Days China将于国家会议中心北京升级回归&#xff0c;汇…

开源不止,前进不息:2018 OpenInfra Days China来了!

OpenStack Days China是由一群热衷并专注于开源的中国志愿者为中国开源社区组织和举办的年度社区活动。近两年来&#xff0c;志愿者团队成功激起广泛关注&#xff0c;获得了中国各行各业和来自全球开源开发者社区的巨大支持。会议注册人数共计超过 1 万人&#xff0c;参与人数逾…

短暂相逢却回味无穷,全球最具影响力的以太坊技术会议视频,你保存了吗!...

关注我们&#xff0c;了解更多精彩内容自2008年中本聪发表的那篇仅短短9页的比特币白皮书后&#xff0c;毁誉参半的比特币对当今互联网及物联网的世界格局产生了重大的影响&#xff0c;其后延伸出来的区块链技术成为了全球最时髦的名词。相比比特币&#xff0c;以太坊是区块链技…

互联网+2.0:技术有多强 梦想才有多近

在过去不到十年的时间里&#xff0c;互联网行业高速发展。先是以手机、pad等智能终端为主的移动互联网打破了PC端互联网商业发展瓶颈&#xff0c;实体经济也依托互联网进行改造升级&#xff0c;“互联网”成为行业图腾和符号。后是随着人工智能、大数据、云计算等技术的融入&am…

java定时器 并发_【java多线程与并发库】— 定时器的应用 | 学步园

定时器的应用1、 定时器主要涉及到两个类(java.util包中)-》public class Timer extendsObject(一种工具&#xff0c;线程用其安排以后在后台线程中执行的任务。可安排任务执行一次&#xff0c;或者定期重复执行。 )-->public abstract class TimerTask extendsObjectimple…

效率提升,英特尔助力企业驶入“快车道”

随着越来越多的企业加入数字化转型大军&#xff0c;每个企业都在期待着数字化带来的业务创新及优化。从云平台的应用、大数据的决策分析&#xff0c;再到工作流程自动化&#xff0c;企业的IT部门不再仅仅是维护企业本身的业务运作以及数据处理&#xff0c;而是需要接入整个生态…

java怎么写事件listener_java 事件监听器ActionListener

/** 功能:java事件监听器ActionListener*/package com.events;import java.awt.BorderLayout;import java.awt.Color;import java.awt.event.ActionEvent;import java.awt.event.ActionListener;import javax.swing.*;public class changebgcolor extends JFrame implements Ac…

Spring AOP 使用介绍,从前世到今生

前面写过 Spring IOC 的源码分析&#xff0c;很多读者希望可以出一个 Spring AOP 的源码分析&#xff0c;不过 Spring AOP 的源码还是比较多的&#xff0c;写出来不免篇幅会大些。本文不介绍源码分析&#xff0c;而是介绍 Spring AOP 中的一些概念&#xff0c;以及它的各种配置…

java怎么用doss窗口_GitHub - doss128/symphony: 一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台。...

下一代的社区系统&#xff0c;为未来而构建&#x1f4a1; 简介Symphony([ˈsɪmfəni]&#xff0c;n.交响乐)是一个现代化的社区平台&#xff0c;因为它&#xff1a;实现了面向内容讨论的论坛实现了面向知识问答的社区包含了面向用户分享、交友、游戏的社交网络100% 开源⚡ 动机…

机器学习算法比较

本文主要回顾下几个常用算法的适应场景及其优缺点&#xff01;&#xff08;提示&#xff1a;部分内容摘自网络&#xff09;。机器学习算法太多了&#xff0c;分类、回归、聚类、推荐、图像识别领域等等&#xff0c;要想找到一个合适算法真的不容易&#xff0c;所以在实际应用中…

还在用 Python 2.x?Python 3.7.0 正式发布!

6 月 27 日&#xff0c;期待已久的 Python 3.7.0 正式发布&#xff0c;与之同行的还有 3.6.6 版本的更新。此次&#xff0c;最新版的 Python 3.7.0 带来了诸多的新功能和优化&#xff0c;接下来&#xff0c;让我们一睹为快。Python 3.7.0 主要更新新的语法特性&#xff1a;PEP …