开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

线上报了内存溢出异常,又不完全是内存溢出 (一)前言 (二)

发表于 2021-11-03

听说微信搜索《Java鱼仔》会变更强哦!

本文收录于github和gitee ,里面有我完整的Java系列文章,学习或面试都可以看看哦

(一)前言

最近一直忙于对付即将上线的系统,期间也碰到了很多问题。最近印象比较深的是一个内存溢出的报错。测试告诉我最近某个功能总是没有效果,于是我就去线上看了一下错误日志,这不看不知道,一看吓一跳,满屏的OutOfMemoryError ,出于隐私保护,这里只展示其中的一点异常信息:

(二)思考场景

一般查问题首先是看日志,然后是思考场景,为什么在这个场景下会发生内存溢出的异常。于是我就思考了一下这段代码的逻辑,这里是一个异步线程的数据提取功能:通过dubbo接口,每次调用1000条数据,再对这些数据做一些处理后落入库,数据的总量在几万至几十万不等。

这里如果会出现内存溢出,唯一有可能的是每次调用的1000条数据在数据处理后没有清空,导致几十万数据都加入进内存中,最后内存溢出。于是去检查了这部分的代码:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码List<EmrTreatment> resultList = new ArrayList<>();
//如果数据还有,则不跳出循环
while (CollectionUtils.isNotEmpty(data.getRecords())) {
resultList.addAll(dataPage.getRecords());
//业务处理
//.......
//清空集合防止内存溢出
resultList.clear();
// 通过dubbo接口请求接下来的1000条
//... ...
data=searchDataByScroll(dataRequest);
}

我特意在1000条处理完成后清空了List,就不存在内存溢出的情况。

(三)查看GC日志

因为是堆内存溢出,于是立刻想到了去看看GC日志,但是一点内存溢出的意思也没有,顺便重温一下GC日志的内容表示的含义:

以其中的单条为例:

1
java复制代码GC (Allocation Failure) 2021-10-29T16:37:45.177+0900: 2686.339  [ParNew: 283195K->3579K(314560K), 0.0256691 secs] 396015K->116915K(1013632K), 0.0258253 secs] [Times: user=0.03 sys=0.02, real=0.03 secs]

GC: 表明进行了一次垃圾回收,属于MinorGC

Allocation Failure:GC发生原因是因为年轻代空间不足

ParNew:本次GC年轻代使用的是ParNew垃圾收集器

283195K->3579K(314560K):GC前年轻代使用量->GC后年轻代使用量(年轻代总容量)

396015K->116915K(1013632K):堆区垃圾回收前使用量->堆区垃圾回收后使用量(堆大小)

[Times: user=0.04 sys=0.00, real=0.01 secs]:

user:垃圾收集线程消耗的所有CPU时间

sys:系统等待时间

real:应用暂停总时间(STW)

既然GC日志中没有堆内存溢出的信息,说明不是我们应用的内存溢出,又仔细看了一下报错信息,有很明显的错误指向dubbo,说明之前的路走歪了。

(四)检查dubbo接口配置

依稀记得dubbo接口调用时设置了每次的调用大小,于是上nacos检查配置,果然dubbo接口设置了16M的大小,这一下就定位到问题了。每次从dubbo接口取1000条数据在某些数据量比较大的情况下超过了16M,返回了一个OutOfMemory Error。

(五)解决方案

既然定位到了问题,解决方案也就简单了,首先根据实际情况调整dubbo接口的消费者和生产者限制大小,其次将每次调用1000次修改略微小一点。再者按照原来的设计1000条数据是不会超过16M的,于是检查了数据,发现有的数据单条就超过了2M,这类数据的使用价值不大,因此在产品上考虑是否过滤掉这些数据。最终上线验证没有再报同样的问题,算是解决了。

(六)总结

虽然问题是解决了,但是还是走了一些歪路。碰到紧急问题时脑子不会像事后那么清晰,但是踩的坑越多,学到的也就越多。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

你想知道的所有Git分支命令都在这 创建一个本地分支

发表于 2021-11-03

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」。

创建一个本地分支

1
复制代码git branch 分支名

通过git branch 命令就可以快速创建一个本地分支,这个分支会基于当前提交对象创建一个指针。而这个命令只会创建一个分支,并不会自动切换到新分支中,本例中我们是处于master分支中。

image-20211103211153983

HEAD相当于一个标识,用来指向当前所在的分支

分支切换

要想切换分支,就需要使用gIt checkout命令

1
2
3
4
5
6
7
arduino复制代码//切换分支
% git checkout testing
​
//查看分支
% git branch -a
master
* testing

此时HEAD就指向testing分支了

image-20211103211752154

当我们针对master分支和testing分支分别做修改,看看分支份分叉会变成什么样

image-20211103215019877

可以看到两个分支从一开始的位置走上了不同的道路

合并分支

假如testing分支是你同事的分支,现在需要合到master分支然后push到远程分支,那就需要使用merge命令进行合并

1
erlang复制代码% git merge testing

此命令会将testing分支合并到当前分支中,我目前的分支是master,合并后两个分支的代码就到一起了,后续就可以基于master进行push或者继续提交代码。

合并后如果你不想要testing分支,可以将该分支进行删除

删除分支

1
复制代码git branch -d testing

分支冲突时如何合并

假如你有两个分支都对同一个文件进行了修改,当你将其中一个分支合并到另一个分支就会出现冲突

1
2
3
scss复制代码 % git merge testing
Auto-merging test.md
CONFLICT (content): Merge conflict in test.md

因为testing分支和master分支都对test.md文件做了修改,当将testing分支合并到master分支时就会出现冲突

任何因包含合并冲突而有待解决的文件,都会以未合并状态标识出来。 Git 会在有冲突的文件中加入标准的冲突解决标记,这样你可以打开这些包含冲突的文件然后手动解决冲突。 出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:

1
2
3
4
5
6
7
8
9
10
markdown复制代码sssssTEST
<<<<<<< HEAD
ssss
ssssssssasasa
=======
​
sss
ssssss
>>>>>>> testing
​

这表示 HEAD 所指示的版本(也就是你的 master 分支所在的位置,因为你在运行 merge 命令的时候已经检出到了这个分支)在这个区段的上半部分(======= 的上半部分),而 testing 分支所指示的版本在 ======= 的下半部分。 为了解决冲突,你必须选择使用由 ======= 分割的两部分中的一个,或者你也可以自行合并这些内容。 例如,你可以通过把这段内容换成下面的样子来解决冲突:

1
2
3
4
复制代码sssssTEST
ssss
ssssssssasasa
ssssss

在你解决了所有文件里的冲突之后,对每个文件使用 git add 命令来将其标记为冲突已解决。 一旦暂存这些原本有冲突的文件,Git 就会将它们标记为冲突已解决。

分支的管理

查看所有分支

1
2
3
4
bash复制代码 % git branch
* master
test
​

会显示出所有分支,*号代表现在检出的那一个分支(也就是说,当前 HEAD 指针所指向的分支)。

查看每一个分支最后提交

1
2
3
bash复制代码% git branch -v
* master bc5b518 [ahead 5] sss
test   1be830c ss

查看所有本地分支对应跟踪的远程分支

1
2
3
bash复制代码 % git branch -vv             
master d230c50 [origin/master: ahead 6] ss
* test   1be830c ss

此时可以看到,master对应的远程分支是origin/master,test还没有和远程分支建立关联

设置已有的本地分支跟踪一个远程分支

1
2
3
4
5
6
bash复制代码% git branch -u origin/master
​
% git branch -vv            
master d230c50 [origin/master: ahead 6] ss
* test   1be830c [origin/master] ss
​

该命令会将当前所使用的本地分支与一个远程分支建立映射关系,当然此命令也可以作为修改对应的远程分支

删除远程分支

1
perl复制代码% git push origin --delete 远程分支名

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java冷门包javabeans有什么用? 前言 Bean

发表于 2021-11-03

前言

java.beans这个包好像很少见人提到过,看名字就知道这是有关处理java bean的,在jdk 8中一共26个类,但是能有一点点用的感觉没几个,相似的功能都可以用更好的办法解决,在网上找了半天,也没有什么有用的例子,真是糟糕的设计。

但是里面有几个类,好像还是有点用处的。

BeanDescriptor

第一个就是BeanDescriptor,但是这玩意有点迷,不知道怎么用,看名字就是对Bean的描述,我以为给他传入一个bean的class,可以从中获取到里面的字段信息,可是想错了,压根不能。

里面只有一些set/get bean名字的,默认请求下,如getDisplayName会返回传入的class名称,更谜的是,里面getValue、setValue、attributeNames方法就是对一个map的操作。

单独使用BeanDescriptor好像也没啥用处,应该在某些情况下封装中的吧。

PropertyDescriptor

这个还算有点用,是对bean 中字段的封装,比如bean中有一个String类型的字段value,通过他,可以获取到set/get的Method。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class MyBean {
private String value="s";
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "MyBean{" +
"value='" + value + ''' +
'}';
}
}
1
2
3
4
5
6
7
8
9
10
java复制代码
try {
MyBean myBean = new MyBean();
PropertyDescriptor value = new PropertyDescriptor("value", MyBean.class);
Method writeMethod = value.getWriteMethod();
writeMethod.invoke(myBean, "张三");
System.out.println(myBean);
} catch (IntrospectionException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}

最后来看一个好玩的,隐约觉得这个功能很有潜力,他允许你对字段get/set的时候对他进行加工,也可以看作代理,但是和代理也有点不一样。

通过PropertyDescriptor拿到指定字段的时候描述后,在对他设置一个”加工工厂”的类,这个类要继承PropertyEditor,并且一定要重写他的有参构造方法,接着通过createPropertyEditor创建这个类的实例,他的参数是目标bean的实例。

但是,最后改变的数据并不能反映到最终的bean上,虽然给了一个属性改变的通知接口,我以为会传入旧数据和新数据,其实不然,在源码中全部都传入的null。

如果改变要反映的最终的bean上,那就要自己通过反射写了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码public class Main {
public static class MyStringEditor extends PropertyEditorSupport {
public MyStringEditor(Object source) {
super(source);
}

@Override
public void setValue(Object value) {
PropertyDescriptor propertyDescriptor = null;
try {
propertyDescriptor = new PropertyDescriptor("value", MyBean.class);
propertyDescriptor.getWriteMethod().invoke(getSource(), value);

} catch (IntrospectionException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}

@Override
public void setAsText(String text) throws IllegalArgumentException {
super.setAsText(text);
}

@Override
public Object getValue() {
return "新的值" + getSource();
}

}

public static void main(String[] args) throws IOException {

try {
MyBean myBean = new MyBean();
PropertyDescriptor value = new PropertyDescriptor("value", MyBean.class);
value.setPropertyEditorClass(MyStringEditor.class);
PropertyEditor propertyEditor = value.createPropertyEditor(myBean);
propertyEditor.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
System.out.println("数据改变" + evt.getSource());
}
});
propertyEditor.setValue(new String("test"));
System.out.println(myBean.getValue());
} catch (IntrospectionException e) {
e.printStackTrace();
}

}
}
1
2
3
4
5
6
7
8
9
java复制代码  try {
BeanInfo beanInfo = Introspector.getBeanInfo(MyBean.class);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
System.out.println(propertyDescriptor.getName());
}
} catch (IntrospectionException e) {
e.printStackTrace();
}

Introspector

这个类是个工具类,用来获取目标bean的信息,比如所有public方法,所有符合java bean规范的字段。

从中也可以获取到BeanDescriptor的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码 try {
BeanInfo beanInfo = Introspector.getBeanInfo(MyBean.class);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();
for (MethodDescriptor methodDescriptor : methodDescriptors) {
System.out.println(methodDescriptor.getName() +" "+methodDescriptor);
}
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
System.out.println(propertyDescriptor.getName());
}
} catch (IntrospectionException e) {
e.printStackTrace();
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java中Spring的Assert断言的使用 1 Spri

发表于 2021-11-03

这是我参与11月更文挑战的第2天,活动详情查看:11月更文挑战

Java中Assert的使用日常较少, 使用Assert可以提高代码的可读性,但过度使用,会导致不利于后期代码的维护.

1 Spring的Assert的简介

Java中断言assert是一个关键字, 主要是验证某些条件是否成立. 在一些编辑器中,需要开启assert断言功能,才可以使用,如IDEA,Eclipse等 ,很不方便使用,在生产等环境不适用, 故常常使用的是Spring框架封装的org.springframework.util.Assert断言.

部分源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
java复制代码public abstract class Assert {

/**
* Assert a boolean expression, throwing an {@code IllegalStateException}
* if the expression evaluates to {@code false}.
* <p>Call {@link #isTrue} if you wish to throw an {@code IllegalArgumentException}
* on an assertion failure.
* <pre class="code">Assert.state(id == null, "The id property must not already be initialized");</pre>
* @param expression a boolean expression
* @param message the exception message to use if the assertion fails
* @throws IllegalStateException if {@code expression} is {@code false}
*/
public static void state(boolean expression, String message) {
if (!expression) {
throw new IllegalStateException(message);
}
}

/**
* Assert a boolean expression, throwing an {@code IllegalStateException}
* if the expression evaluates to {@code false}.
* <p>Call {@link #isTrue} if you wish to throw an {@code IllegalArgumentException}
* on an assertion failure.
* <pre class="code">
* Assert.state(entity.getId() == null,
* () -&gt; "ID for entity " + entity.getName() + " must not already be initialized");
* </pre>
* @param expression a boolean expression
* @param messageSupplier a supplier for the exception message to use if the
* assertion fails
* @throws IllegalStateException if {@code expression} is {@code false}
* @since 5.0
*/
public static void state(boolean expression, Supplier<String> messageSupplier) {
if (!expression) {
throw new IllegalStateException(nullSafeGet(messageSupplier));
}
}

/**
* Assert a boolean expression, throwing an {@code IllegalStateException}
* if the expression evaluates to {@code false}.
* @deprecated as of 4.3.7, in favor of {@link #state(boolean, String)}
*/
@Deprecated
public static void state(boolean expression) {
state(expression, "[Assertion failed] - this state invariant must be true");
}

/**
* Assert a boolean expression, throwing an {@code IllegalArgumentException}
* if the expression evaluates to {@code false}.
* <pre class="code">Assert.isTrue(i &gt; 0, "The value must be greater than zero");</pre>
* @param expression a boolean expression
* @param message the exception message to use if the assertion fails
* @throws IllegalArgumentException if {@code expression} is {@code false}
*/
public static void isTrue(boolean expression, String message) {
if (!expression) {
throw new IllegalArgumentException(message);
}
}

/**
* Assert a boolean expression, throwing an {@code IllegalArgumentException}
* if the expression evaluates to {@code false}.
* <pre class="code">
* Assert.isTrue(i &gt; 0, () -&gt; "The value '" + i + "' must be greater than zero");
* </pre>
* @param expression a boolean expression
* @param messageSupplier a supplier for the exception message to use if the
* assertion fails
* @throws IllegalArgumentException if {@code expression} is {@code false}
* @since 5.0
*/
public static void isTrue(boolean expression, Supplier<String> messageSupplier) {
if (!expression) {
throw new IllegalArgumentException(nullSafeGet(messageSupplier));
}
}

/**
* Assert a boolean expression, throwing an {@code IllegalArgumentException}
* if the expression evaluates to {@code false}.
* @deprecated as of 4.3.7, in favor of {@link #isTrue(boolean, String)}
*/
@Deprecated
public static void isTrue(boolean expression) {
isTrue(expression, "[Assertion failed] - this expression must be true");
}
}

从代码可知,Assert断言的表达式基本分为两类

  • Assert.方法名 (布尔表达式) : 这类方法都标记过时, 都调用第二类方法
  • Assert.方法名 (布尔表达式,错误提示信息) : 这类方法, 如果布尔表达式不满足,会抛出异常,并将异常信息封装.

在使用方法时不满足布尔表达式, 就会抛出相应的异常,如IllegalStateException和IllegalArgumentException, 此类异常都属于运行时异常.

2 Spring的Assert的使用

1
2
3
java复制代码        if (条件为真) {
// 业务处理
}

上述可替换成

1
2
java复制代码Assert.isTrue(条件为真, "条件不满足");
// 业务处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码    public static void main(String[] args) {

Assert.isTrue(1>0);
System.out.println("断言 1>0 成功");
Assert.isTrue(1 < 0, "断言1 < 0失败");
}


/*
运行结果:
Exception in thread "main" 断言 1>0 成功
java.lang.IllegalArgumentException: 断言1 < 0失败
at org.springframework.util.Assert.isTrue(Assert.java:121)
at com.cf.demo.config.MailTest2.main(MailTest2.java:61)

*/

使用时发现,Spring中主要抛出非法参数异常和非法状态异常, 主要用来做参数的校验,可以提高代码的可读性.对于一些说法, 关于可以替代if-else语句, 是不合理的. if-else主要用来做条件分支判断,满足那个就进入那个, 而Assert断言,满足条件才会向下运行,不满足,直接抛出异常,二者应用场景不一致.

参考资料:

www.runoob.com/w3cnote/c-a…

zhuanlan.zhihu.com/p/265444322

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java集合之List去重

发表于 2021-11-03

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

List 去重的方式较多,本人对常用的几种去重方式进行整理、分析,如有错漏欢迎指正。

HashSet

利用 Set 元素的不重复特性去重,去重后不保留原顺序。无法直接对新对象(new 创建的对象)去重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class Test {
public static void main(String[] args) {
// 1.构造 List
List<Person> list = new ArrayList<>();
Person p = new Person("张三", 20, '男');
list.add(p);
list.add(new Person("小丽", 20, '女'));
list.add(new Person("小丽", 20, '女'));
// 2.去重
Set<Person> hashSet = new HashSet<>(list);
List<Person> newList = new ArrayList<>(hashSet);
System.out.println(newList);//没有对“小丽”去重
}
}

HashSet + ArrayList

通过 HashSet 判断元素是否重复,不重复则放入新的 List 中。这种方法去重后保留原顺序。无法直接对新对象(new 创建的对象)去重。

1
2
3
4
5
6
7
8
java复制代码Set<Person> hashSet = new HashSet<>();
List<Person> newList = new ArrayList<>();
for (Iterator<Person> iter = list.iterator(); iter.hasNext();) {
Person element = iter.next();
if (hashSet.add(element)) {
newList.add(element);
}
}

TreeSet

利用 TreeSet 的元素不重复特性去重,可自定义排序,默认自然排序。

1
2
java复制代码Set<String> treeSet = new TreeSet<String>(list);
List<String> newList = new ArrayList<>(treeSet);

ArrayList

使用两个 List,遍历原 List,然后通过检查新 List 中是否存在原 List 中的元素来去重,这种去重保留原顺序。无法直接对新对象(new 创建的对象)去重。

1
2
3
4
5
6
java复制代码List<String> newList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
if (!newList.contains(list.get(i))) {
newList.add(list.get(i));
}
}

java8 的 stream

以流的方式去重会保留原顺序。无法直接对新对象(new 创建的对象)去重。

注意:流不会对原集合进行操作,所以要用新集合接收操作后的流。

1
java复制代码list.stream().distinct().collect(Collectors.toList());

实体单属性之自定义方法去重

上述方法不能根据实体的某个属性去重,因此只能通过自定义方法实现。利用流的 filter 来自定义方法,这种方式去重保留原顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class Test {
public static void main(String[] args) {
// 1.构造 List
...
// 2.去重
List<Person> newList = new ArrayList<>();
newList = list.stream()
.filter(distinctByKey(o -> o.getName() + ";" + o.getAge()))
.collect(Collectors.toList());
System.out.println(newList);
}

/**
* 自定义的去重方法
* @param <T> 待去重实体
* @param keyExtractor 去重标记(如:o.getName() + ";" + o.getAge())
* @return
*/
private static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
Map<Object, Boolean> seen = new ConcurrentHashMap<>();
return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

}

实体单属性之 stream + TreeSet

stream + TreeSet,不保留原顺序,可自定义排序,默认自然排序。

1
2
3
4
java复制代码List<Person> newList = list.stream().collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(
Comparator.comparing(person -> person.getName() + ";" + person.getAge()))), ArrayList::new)
);

总结

stream 去重的效率最低,耗费时间大概为 HashSet + ArrayList 去重所耗时间的五倍。

建议:对性能要求不高使用 stream 方式去重,代码简洁;对性能有较高要求用 HashSet + ArrayList 或者 ArrayList ,后者性能略低,但是都比 HashSet 直接去重效率高;需要自定义排序用 TreeSet。

HashSet HashSet + ArrayList TreeSet ArrayList stream
写法 简单 较难 简单 较难 最简单
效率 较低 高 较低 高 最低
顺序 无序 原顺序 自然排序 原顺序 原顺序

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go高阶16,面试官问我go逃逸场景有哪些,我??? 什么是

发表于 2021-11-03

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

逃逸分析就是程序运行时内存的分配位置(栈或堆),是由编译器来确定的,而非开发者。

什么是栈

栈只允许从线性表的同一端放入和取出数据,按照后进先出(LIFO,Last InFirst Out)的顺序,如下图:

什么是堆

对于堆在内存中的分配,我们可以类比成一个房间,分配内存时,需要找一块足够装下家具的空间来摆放家具。经过反复摆放和腾空家具后,房间里的空间会变得乱七八糟,此时再往这个空间里摆放家具会发现虽然有足够的空间,但各个空间分布在不同的区域,没有一段连续的空间来摆放家具。此时,内存分配器就需要对这些空间进行调整优化,如下图:

对比栈和堆可知,在编译时,一切无法确定大小或大小可以改变的数据,最好放到堆上,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。

函数中申请一个新的对象:

  • 如果分配在栈中,则函数执行结束可自动将内存回收;
  • 如果分配在堆中,则函数执行结束可交给GC(垃圾回收)处理;

逃逸分析的好处应该是减少了 gc 的压力,栈的分配比堆快,性能好,如果变量都分配到栈上,可以避免 Go 频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销。

逃逸分析基本原则

编译器会根据变量是否被外部引用来决定是否逃逸:

  1. 如果函数外部没有引用,则优先放到栈中;
  2. 如果函数外部存在引用,则必定放到堆中;
  3. 如果栈上放不开,则必定放到堆上;

逃逸场景

指针逃逸

我们知道Go可以返回局部变量指针,这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数结束而回收,因此只能分配在堆上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package main

type Person struct {
Name string
Age int
}

func PersonRegister(name string, age int) *Person {
p := new(Person) //局部变量s逃逸到堆

p.Name = name
p.Age = age

return p
}

func main() {
PersonRegister("微客鸟窝", 18)
}

函数 PersonRegister() 内部 p 为局部变量,其值通过函数返回值返回, p 本身为一指针,其指向的内存地址不会是栈而是堆,这就是典型的逃逸案例。

通过编译参数-gcflag=-m可以查看编译过程中的逃逸分析:

1
2
3
4
5
6
7
8
go复制代码$ go build -gcflags=-m
# ceshi
.\main.go:8:6: can inline PersonRegister
.\main.go:17:6: can inline main
.\main.go:18:16: inlining call to PersonRegister
.\main.go:8:21: leaking param: name
.\main.go:9:10: new(Person) escapes to heap
.\main.go:18:16: new(Person) does not escape

代码第9行显示”escapes to heap”,表示该行内存分配发生了逃逸现象。

栈空间不足逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码package main

func Slice() {
s := make([]int, 1000, 1000)

for index, _ := range s {
s[index] = index
}
}

func main() {
Slice()
}

上面代码 Slice() 函数中分配了一个1000个长度的切片,是否逃逸取决于栈空间是否足够大。直接查看编译提示,如下:

1
2
3
4
go复制代码$ go build -gcflags=-m
# ceshi
.\main.go:11:6: can inline main
.\main.go:4:11: make([]int, 1000, 1000) does not escape

发现并没有发生逃逸。我们把切片长度扩大10倍再试试:

s := make([]int, 10000, 10000)

1
2
3
4
go复制代码$ go build -gcflags=-m
# ceshi
.\main.go:11:6: can inline main
.\main.go:4:11: make([]int, 10000, 10000) escapes to heap

发现当切片长度扩大到10000时就会逃逸。
当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。

动态类型逃逸

在 Go 中,空接口 interface{} 可以表示任意的类型,如果函数参数为 interface{},编译期间很难确定其参数的具体类型,也会发生逃逸。

1
2
3
4
5
6
7
go复制代码package main
import "fmt"

func main() {
s := "wekenw"
fmt.Println(s)
}

因为 fmt.Println() 的参数类型定义为 interface{},因此也发生了逃逸。

1
2
3
4
5
6
7
go复制代码$ go build -gcflags=-m
# ceshi
.\main.go:6:13: inlining call to fmt.Println
.\main.go:6:13: s escapes to heap
.\main.go:6:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape
<autogenerated>:1: .this does not escape

闭包引用对象逃逸

回Fibonacci数列的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码package main

func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
f()
}
}
func fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}

Fibonacci()函数中原本属于局部变量的a和b由于闭包的引用,不得不将二者放到堆上,以致产生逃逸:

1
2
3
4
5
6
go复制代码$ go build -gcflags=-m
# ceshi
.\main.go:11:9: can inline fibonacci.func1
.\main.go:10:2: moved to heap: a
.\main.go:10:5: moved to heap: b
.\main.go:11:9: func literal escapes to heap

总结

  • 栈上分配内存比在堆中分配内存效率更高
  • 栈上分配的内存不需要 GC 处理,而堆需要
  • 逃逸分析目的是决定内分配地址是栈还是堆
  • 逃逸分析在编译阶段完成

传值 VS 传指针

函数传递指针真的比传值效率高吗?如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,增加垃圾回收(GC)的负担,所以传递指针不一定
是高效的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

python学习--协程

发表于 2021-11-03

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

什么是协程

协程,其实可以理解为一种特殊的程序调用。特殊的是在执行过程中,在子程序(或者说函数)内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

注意,它有两个特征:

可中断,这里的中断不是普通的函数调用,而是类似CPU的中断,CPU在这里直接释放转到其他程序断点继续执行。

可恢复,等到合适的时候,可以恢复到中断的地方继续执行,至于什么是合适的时候,我们后面再探讨。

和进程线程的区别

上面两个特点就导致了它相对于线程和进程切换来说极高的执行效率,为什么这么说呢?我们先老生常谈地说一下进程和线程。

进程是操作系统资源分配的基本单位,线程是操作系统调度和执行的最小单位。

  • 进程是程序的启动实例,拥有代码和打开的文件资源、数据资源、独立的内存空间。
  • 线程从属于进程,是程序的实际执行者,一个进程至少包含一个主线程,也可以有更多的子线程,线程拥有自己的栈空间。
  • 无论是进程还是线程,都是由操作系统所管理和切换的。

协程

它又叫做微线程,但其实它和进程还有线程完全不是一个维度上的概念。

  • 进程和线程的切换完全是用户无感,由操作系统控制,从用户态到内核态再到用户态。
  • 协程的切换完全是程序代码控制的,在用户态的切换,就像函数回调的消耗一样,在线程的栈内即可完成。

python的协程(coroutine)

python的协程其实是我们通常意义上的协程Goroutine。

  • python的协程同样是在适当的时候可中断可恢复。
  • 那么什么是适当的时候呢,就是你认为适当的时候,因为程序在哪里发生协程切换完全控制在开发者手里。
  • 对于python来说,由于GIL锁,在CPU密集的代码上做协程切换是没啥意义的,CPU本来就在忙着没偷懒,切换到其他协程,也只是在单核内换个地方忙而已。
  • 我们应该在IO密集的地方来起协程,这样可以让CPU不再空等转而去别的地方干活,才能真正发挥协程的威力。
  • 还可以将协程理解为生成器+调度策略,生成器中的yield关键字,就可以让生成器函数发生中断,而调度策略,可以驱动着协程的执行和恢复。这样就实现了协程的概念。
  • 这里的调度策略可能有很多种,简单的例如忙轮循:while True,更简单的甚至是一个for循环。就可以驱动生成器的运行,因为生成器本身也是可迭代的。

我们看一个简单的协程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ini复制代码import time


def consumer():
    res = ''

    while True:
        n = yield res
        if not n:
            return

        print('[CONSUMER] Consuming %s...' % n)
        time.sleep(1)
        res = '200 OK'

def produce(c):
    c.next()
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        res = c.send(n)
        print('[PRODUCER] Consumer return: %s' % res)
    c.close()


if __name__=='__main__':
    c = consumer()
    produce(c)

很明显这是一个传统的生产者-消费者模型,这里consumer函数就是一个协程(生成器),它在n = yield r 的地方发生中断,生产者produce中的c.send(n),可以驱动协程的恢复,并且向协程函数传递数据n,接收返回结果r。而while n < 5,就是我们所说的调度策略。在生产中,这种模式很适合我们来做一些pipeline数据的消费,我们不需要写死几个生产者进程几个消费者进程,而是用这种协程的方式,来实现CPU动态地分配调度。

python协程的特点

  • 单线程内切换,适用于IO密集型程序中,可以最大化IO多路复用的效果。
  • 无法利用多核。
  • 协程间完全同步,不会并行。不需要考虑数据安全。
  • 用法多样,可以用在web服务中,也可用在pipeline数据/任务消费中

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java并发编程之线程状态

发表于 2021-11-03

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

线程状态概述

线程由生到死的完整过程:

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态:

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread只有线程对象,没有线程特征。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典教法)
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

在这里插入图片描述

睡眠sleep方法

状态中有一个状态叫做计时等待,可以通过Thread类的方法来进行演示。
public static void sleep(long time) 让当前线程进入到睡眠状态,到毫秒后自动醒来继续执行

1
2
3
4
5
6
7
8
9
java复制代码//主线程执行到sleep方法会休眠1秒后再继续执行
public class Test{
public static void main(String[] args){
for(int i = 1;i<=5;i++){
Thread.sleep(1000);
System.out.println(i)
}
}
}
等待和唤醒

public void wait() : 让当前线程进入到等待状态 此方法必须锁对象调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class Demo1_wait {
public static void main(String[] args) throws InterruptedException {
// 步骤1 : 子线程开启,进入无限等待状态, 没有被唤醒,无法继续运行.
new Thread(() -> {
try {

System.out.println("begin wait ....");
synchronized ("") {
"".wait();
}
System.out.println("over");
} catch (Exception e) {
}
}).start();
}

public void notify() : 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码public class Demo2_notify {
public static void main(String[] args) throws InterruptedException {
// 步骤1 : 子线程开启,进入无限等待状态, 没有被唤醒,无法继续运行.
new Thread(() -> {
try {

System.out.println("begin wait ....");
synchronized ("") {
"".wait();
}
System.out.println("over");
} catch (Exception e) {
}
}).start();

//步骤2: 加入如下代码后, 3秒后,会执行notify方法, 唤醒wait中线程.
Thread.sleep(3000);
new Thread(() -> {
try {
synchronized ("") {
System.out.println("唤醒");
"".notify();
}
} catch (Exception e) {
}
}).start();
}
}
等待唤醒的一个小例子

定义一个集合,包子铺线程完成生产包子,包子添加到集合中;吃货线程完成购买包子,包子从集合中移除。

  1. 当包子没有时(包子状态为false),吃货线程等待.
  2. 包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码public class BaoZiPu extends Thread{
private List<String> list ;
public BaoZiPu(String name,ArrayList<String> list){
super(name);
this.list = list;
}
@Override
public void run() {
int i = 0;
while(true){
//list作为锁对象
synchronized (list){
if(list.size()>0){
//存元素的线程进入到等待状态
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

//如果线程没进入到等待状态 说明集合中没有元素
//向集合中添加元素
list.add("包子"+i++);
System.out.println(list);
//集合中已经有元素了 唤醒获取元素的线程
list.notify();
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码public class ChiHuo extends Thread {

private List<String> list ;
public ChiHuo(String name,ArrayList<String> list){
super(name);
this.list = list;
}

@Override
public void run() {
//为了能看到效果 写个死循环
while(true){
//由于使用的同一个集合 list作为锁对象
synchronized (list){
//如果集合中没有元素 获取元素的线程进入到等待状态
if(list.size()==0){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果集合中有元素 则获取元素的线程获取元素(删除)
list.remove(0);
//打印集合 集合中没有元素了
System.out.println(list);
//集合中已经没有元素 则唤醒添加元素的线程 向集合中添加元素
list.notify();
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class Demo {
public static void main(String[] args) {
//等待唤醒案例
List<String> list = new ArrayList<>();
// 创建线程对象
BaoZiPu bzp = new BaoZiPu("包子铺",list);
ChiHuo ch = new ChiHuo("吃货",list);
// 开启线程
bzp.start();
ch.start();
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java中如何将接口返回的字节串转为文件

发表于 2021-11-03

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

讲一下现在的需求场景

最近公司要在项目中访问一个第三方服务,在这个第三方服务中,需要下载一个报告文件,通过一个接口反馈回来。

这个下载接口返回了一个字节串,如[1,2,3,4,5,6,7],当然真实的数据不会是这个样子的。

但是我们如何将这个字节串转成文件流呢?

接下来就一起来看看吧,也跟大家分享一下我处理的思路。

试一下直接转字符串后转字节数组

我首先拿到了这个字节串,但是并没有办法直接转成字节数组byte[]。

这个时候我想到一个方法,那就是直接讲这个字节串转成字符串,也就是下面的代码:

1
2
3
4
5
6
7
java复制代码Object obj = "[1,2,3,4,5,6,7]";

String string = (String) obj;

byte[] bytes = string.getBytes();

InputStream inputStream = new ByteArrayInputStream(target);

很遗憾,这样是错的,不明所以。

没有办法,只能再去另寻方法。

再试一下是不是特殊符合[],还有逗号造成的失败。

于是我又再次尝试了这种方式,代码给到大家,如下:

1
2
3
4
5
6
7
8
9
java复制代码Object obj = "[1,2,3,4,5,6,7]";

String string = (String) obj;

string = string.replace("[","").replace("]","").replace(",","");

byte[] bytes = string.getBytes();

InputStream inputStream = new ByteArrayInputStream(target);

如上代码,我们先将字符串中的逗号和中括号替换掉,然后再转成字节数组。

可惜,同样也失败了,没办法只能再次另寻他法。

最后还是不想浪费太多时间,找了一下第三方服务是否提供了代码示例

哇唔,还真有的,于是真给了一段代码示例,虽然有点草率,但是很关键。

我把代码示例中如何转字节数组的方法贴出来,供大家学习一下。

1
2
3
4
5
java复制代码Object obj = "[1,2,3,4,5,6,7]";
String string = (String) obj;
ObjectMapper mapper = new ObjectMapper();
byte[] target = mapper.readValue(string , new TypeReference<byte[]>() {
});

最佳解决方案也就这个了,不免感叹,大家还是要去多多利用可以使用的资源,有些情况可能很简单就能解决了,没必要去太过深扣,希望大家可以有所借鉴吧。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

StringBuilder为什么线程不安全【源码分析】 一、

发表于 2021-11-03

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

作者的其他平台:

| CSDN:blog.csdn.net/qq_4115394…

| 掘金:juejin.cn/user/651387…

| 知乎:www.zhihu.com/people/1024…

| GitHub:github.com/JiangXia-10…

| 公众号:1024笔记

本文大概1770字,读完共需11分钟

一、前言

StringBuilder和StringBuffer的区别是面试的时候被提及最多的问题之一了,我们都知道stringbuffer是线程安全的,而stringbuilder不是线程安全的。通过stringbuffer和stringbuilder的源码,我们可以发现stringbuilder和stringbuffer都是继承了abstractstringbuilder这个抽象累,然后实现了Serializable, CharSequence接口。其次stringbuilder和stringbuffer的内部实现其实跟String是一样的,都是通过一个char类型的数组进行存储字符串的,但是是String类中的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer中的char数组没有被final修饰,是可变的。

1
2
3
4
5
java复制代码public final class StringBuilder

extends AbstractStringBuilder

implements java.io.Serializable, CharSequence
1
2
3
4
5
java复制代码public final class StringBuffer

extends AbstractStringBuilder

implements java.io.Serializable, CharSequence
1
2
3
4
5
6
7
arduino复制代码public final class String

implements java.io.Serializable, Comparable<String>, CharSequence {

/** The value is used for character storage. */

private final char value[];
1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码abstract class AbstractStringBuilder implements Appendable, CharSequence {

* The value is used for character storage.

//stringbuilder和stringbuffer都继承了AbstractStringBuilder 但AbstractStringBuilder 中的

//char数组没有使用final修饰,这就是为什么string是不可变,但stringbuffer和stringbuilder是可变的

* The count is the number of characters used.

* This no-arg constructor is necessary for serialization of subclasses.

那么为什么stringbuilder和stringbuffer一个是线程安全一个不是的呢?如果在多线程中分别使用stringbuilder和stringbuffer会是什么样呢?

二、stringbuffer

首先来看看stringbuffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码    public static void main(String[] args) throws InterruptedException {

StringBuffer sb = new StringBuffer();

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

new Thread(new Runnable() {

@Override

public void run() {

for(int j=0;j<10000;j++){

sb.append("嗯");

//线程休眠300毫秒,这里要抛出异常

Thread.sleep(300);

//输出sb的长度是多少,理论上来说最后应该输出100000

System.out.println(sb.length());

最后的输出结果是

三、stringbuilder

与理论值一样。接下来再看看使用stringbuilder。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码    public static void main(String[] args) throws InterruptedException {

StringBuilder sb = new StringBuilder();

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

new Thread(new Runnable(){

@Override

public void run() {

for(int j=0;j<10000;j++){

sb.append("嗯");

Thread.sleep(300);

System.out.println(sb.length());

理论上来说结果应该跟stringbuffer一样输出100000,但是实际结果是85560与预期结果不一样,而且多执行几次,每次结果也不一样(都小于预期值)(stringbuffer执行多次结果都一样),而且有时候会抛ArrayIndexOutOfBoundsException异常(数组索引越界异常)。

所以我们可以发现在多线程中使用stringbuilder确实是线程不安全的。为什么实际的输出值不对呢?

四、分析

前面提到过因为stringbuffer和stringbuilder都是继承了AbstractStringBuilder,在AbstractStringBuilder中我们可以看到定义了一个char数组和一个count变量

1
2
3
4
5
kotlin复制代码abstract class AbstractStringBuilder implements Appendable, CharSequence {

* The value is used for character storage.

* The count is the number of characters used.

另外stringbuilder和stringbuffer通过append方法来进行字符串的增加,我们先看看stringbuilder中append方法

1
2
3
4
5
6
7
8
9
typescript复制代码    public StringBuilder append(Object obj) {

return append(String.valueOf(obj));

public StringBuilder append(String str) {

//调用的是AbstractStringBuilder的append的方法

super.append(str);

在看看父类abstractstringbuilder中的append方法

1
2
3
4
5
6
7
8
9
python复制代码public AbstractStringBuilder append(String str) {

return appendNull();

int len = str.length();

ensureCapacityInternal(count + len);

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

在多线程编程中有个重要的概念是叫原子操作,原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有切换到任何的一个其他的线程)。上述代码中的count+=len就不是一个原子操作,它等同于count=count+len,比如在上诉代码中,执行到count的值为99998的时候,新建一个len长度为1,但是当有两个线程同时执行到了count+=len的时候,他们的count的值都是99998,然后分别各自都执行了count+=len,则执行完之后的值都是99999,然后将值赋给count,则count最后的结果是99999,不是正确的100000,所以在多线程中执行stringbuilder的值始终会小于正确的结果。

但是stringbuilder和stringbuffer都是继承了abstractstringbuilder为什么结果不一样呢。既然abstractstringbuilder中的append方法肯定都是一样的我们再来看看stringbuffer中的append方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码    //append操作被synchronized 关键字修饰了

public synchronized StringBuffer append(Object obj) {

toStringCache = null;

super.append(String.valueOf(obj));

//append操作被synchronized 关键字修饰了

public synchronized StringBuffer append(String str) {

toStringCache = null;

super.append(str);

可以发现stringbuffer中的append操作被synchronized关键字修饰了。这个关键字肯定不会陌生,主要用来保证多线程中的线程同步和保证数据的准确性。所以再多线程中使用stringbuffer是线程安全的。

再AbstractStringBuilder的append方法中有这样的两个个操作

1
2
3
scss复制代码ensureCapacityInternal(count + len);   //1

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

转到第一个操作方法的源码,可以发现这是一个是检查StringBuilder对象的原r数组的大小是否能装下新的字符串的方法,如果装不下了就new一个新的数组,新的数组的容量是原来char数组的两倍,再通过CopyOf()方法将原数组的内容复制到新数组.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码     * For positive values of {@code minimumCapacity}, this method

* behaves like {@code ensureCapacity}, however it is never

* If {@code minimumCapacity} is non positive due to numeric

* overflow, this method throws {@code OutOfMemoryError}.

private void ensureCapacityInternal(int minimumCapacity) {

// overflow-conscious code

if (minimumCapacity - value.length > 0) {

value = Arrays.copyOf(value,

newCapacity(minimumCapacity));

然后第二步操作是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组里面。getchars源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码   public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {

throw new StringIndexOutOfBoundsException(srcBegin);

if (srcEnd > value.length) {

throw new StringIndexOutOfBoundsException(srcEnd);

if (srcBegin > srcEnd) {

throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);

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

可以看到原来在这里会抛出StringIndexOutOfBoundsException的异常。接着分析:

假设之前的代码中有两个线程a和b同时执行了append方法,并且都执行完了ensureCapacityInternal()方法,此刻count=99997,如果当线程a执行完了,则轮到线程2继续执行,线程b执行完了append方法之后,count变成了99998,这个时候如果线程a执行到了上面的getchars方法的时候他得到的count的值就是99998,这个时候就会抛ArrayIndexOutOfBoundsException的异常了。

五、总结

stringbuilder是线程不安全的,因为stringbuilder继承了父类abstractstringbuilder的append方法,该方法中有一个count+=len的操作不是原子操作,所以在多线程中采用stringbuilder会丢失数据的准确性并且会抛ArrayIndexOutOfBoundsException的异常。

stringbuffer是线程安全的因为他的append方法被synchronized关键字修饰了,所以他能够保证线程同步和数据的准确性。

往期精彩回顾

  • Spring注解(二):@ComponentScan自动扫描组件
  • Spring常用注解大全,值得你的收藏!!!
  • Spring注解(七):使用@Value对Bean进行属性赋值
  • SpringBoot开发Restful风格的接口实现CRUD功能
  • 分布式缓存中间件Redis入门

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…425426427…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%