为什么StringBuilder是线程不安全的?StringBuffer是线程安全的?
面试中经常问到的一个问题:StringBuilder
和StringBuffer
的区别是什么?
我们非常自信的说出:StringBuilder
是线程不安全的,StirngBuffer
是线程安全的
面试官:StringBuilder
不安全的点在哪儿?
这时候估计就哑巴了。。。
分析
StringBuffer
和StringBuilder
的实现内部是和String
内部一样的,都是通过 char[]
数组的方式;不同的是String
的char[]
数组是通过final
关键字修饰的是不可变的,而StringBuffer
和StringBuilder
的char[]
数组是可变的。
首先我们看下边这个例子:
public class Test {
public static void main(String[] args) throws InterruptedException {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 10000; i++){
new Thread(() -> {
for (int j = 0; j < 1000; j++){
stringBuilder.append("a");
}
}).start();
}
Thread.sleep(100L);
System.out.println(stringBuilder.length());
}
}
直觉告诉我们输出结果应该是10000000
,但是实际运行结果并非我们所想。
从上图可以看到输出结果是9970698
,并非是我们预期的1000000
,并且还抛出了一个异常ArrayIndexOutOfBoundsException
{非必现}
为什么输出结果并非预期值?
我们先看一下StringBuilder
的两个成员变量(这两个成员变量实际上是定义在AbstractStringBuilder
里面的,StringBuilder
和StringBuffer
都继承了AbstractStringBuilder
)
StringBuilder
的append
方法
StringBuilder
的append
方法调用了父类的append
方法
我们直接看第七行代码,count += len;
不是一个原子操作,实际执行流程为
- 首先加载
count
的值到寄存器 - 在寄存器中执行
+1
操作 - 将结果写入内存
假设我们count
的值是10
,len
的值为1
,两个线程同时执行到了第七行,拿到的值都是10
,执行完加法运算后将结果赋值给count
,所以两个线程最终得到的结果都是11
,而不是12
,这就是最终结果小于我们预期结果的原因。
为什么会抛出ArrayIndexOutOfBoundsException异常?
我们看回AbstractStringBuilder的追加()方法源码的第五行,ensureCapacityInternal()方法是检查StringBuilder的对象的原字符数组的容量能不能盛下新的字符串,如果盛不下就调用expandCapacity()方法对字符数组进行扩容。
private void ensureCapacityInternal(int minimumCapacity) {
//溢出意识代码
if (minimumCapacity - value .length> 0)
expandCapacity(minimumCapacity);
}
扩容的逻辑就是新一个新的字符数组,新的字符数组的容量是原来字符数组的两倍再加2,再通过System.arryCopy()函数将原数组的内容复制到新数组,最后将指针指向新的字符数组。
void expandCapacity(int minimumCapacity) {
//计算新的容量
int newCapacity = value .length * 2 + 2 ;
//中间省略了一些检查逻辑
...
value = Arrays.copyOf( value,newCapacity);
}
Arrys.copyOf()方法
public static char [] copyOf(char [] original, int newLength) {
char [] copy = new char [newLength];
//拷贝数组
System.arraycopy(original, 0,copy, 0,
Math.min(original.length,newLength));
返回 副本;
}
AbstractStringBuilder的追加()方法源码的第六行,是将字符串对象里面字符数组里面的内容拷贝到StringBuilder的对象的字符数组里面,代码如下:
str.getChars(0,len, value,count);
则GetChars()方法
public void getChars(int srcBegin, int srcEnd, char dst [], int dstBegin) {
//中间省略了一些检查
...
System.arraycopy( value,srcBegin,dst,dstBegin,srcEnd - srcBegin);
}
拷贝流程见下图
假设现在有两个线程同时执行了StringBuilder
的append()
方法,两个线程都执行完了第五行的ensureCapacityInternal()
方法,此刻count=5
这个时候线程1
的cpu
时间片用完了,线程2
继续执行。线程2执行完整个append()
方法后count
变成6
了。
线程1
继续执行第六行的str.getChars()
方法的时候拿到的count
值就是6
了,执行char[]
数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException
异常。
至此,StringBuilder
为什么不安全已经分析完了。如果我们将测试代码的StringBuilder
对象换成StringBuffer
对象会输出什么呢?
结果肯定是会输出 1000000
,至于StringBuffer
是通过什么手段实现线程安全的呢?看下源代码就明白了了。。。
为什么StringBuilder是线程不安全的?StringBuffer是线程安全的?的更多相关文章
- 多线程----Thread类,Runnable接口,线程池,Callable接口,线程安全
1概念 1.1进程 进程指正在运行的程序.确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能. 任务管理器中: 1.2线程 线程是进程中的一个执行单元 ...
- 深入Java线程管理(五):线程池
这几天主要是狂看源程序,在弥补了一些以前知识空白的同时,也学会了不少新的知识(比如 NIO),或者称为新技术吧. 线程池就是其中之一,一提到线程,我们会想到以前<操作系统>的生产者与消费者 ...
- python——有一种线程池叫做自己写的线程池
这周的作业是写一个线程池,python的线程一直被称为鸡肋,所以它也没有亲生的线程池,但是竟然被我发现了野生的线程池,简直不能更幸运~~~于是,我开始啃源码,实在是虐心,在啃源码的过程中,我简略的了解 ...
- Android线程之主线程向子线程发送消息
和大家一起探讨Android线程已经有些日子了,谈的最多的就是如何把子线程中的数据发送给主线程进行处理,进行UI界面的更新,为什么要这样,请查阅之前的随笔.本篇我们就来讨论一下关于主线程向子线程如何发 ...
- C# 多线程join的用法,等待多个子线程结束后再执行主线程
等待多个子线程结束后再执行主线程 class MultiThread{ #region join test public void MultiThreadTest() { Thread[] ths = ...
- 一个线程加一运算,一个线程做减一运算,多个线程同时交替运行--synchronized
使用synchronized package com.pb.thread.demo5; /**使用synchronized * 一个线程加一运算,一个线程做减法运算,多个线程同时交替运行 * * @a ...
- android 进程/线程管理(二)----关于线程的迷思
一:进程和线程的由来 进程是计算机科技发展的过程的产物. 最早计算机发明出来,是为了解决数学计算而发明的.每解决一个问题,就要打纸带,也就是打点. 后来人们发现可以批量的设置命令,由计算机读取这些命令 ...
- Linux 进程与线程二(等待--分离--取消线程)
int pthread_join(pthread_t thr,void **thr_return); pthread_join函数用于挂起当前线程,直至th指定的线程终止为止. 如果另一个线程返回值不 ...
- 【Linux】为啥查某个进程的线程,查出来的所有线程的pid不一样啊
楼上说的linux线程和进程是一样的,这个说法是错误的. 看了楼主的问题,感觉楼主是被PID给弄混了,线程进程都会有自己的ID,这个ID就叫做PID,PID是不特指进程ID,线程ID也可以叫做PID. ...
- C# 线程(二):关于线程的相关概念
From : http://kb.cnblogs.com/page/42528/ 什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源. 而一个进程又 ...
随机推荐
- Navicat连接MYsql报错
在Windows中安装mysql8后,使用Navicat连接数据库是出现“ Client does not support authentication protocol requested by s ...
- SAP-批量删除生产订单
1.SE38运行:PPARCHP1 2.先用COOIS导出订单,已经CLSD,没有删除的
- 配置Windows Server 2008环境
上一章已经把Windows Server2008操作系统安装完毕,接下来配置一下Windows Server环境.配置网络和共享中心.配置桌面环境.配置用户IE设置.安装Telnet远程工具.配置文件 ...
- 并查集模板题----P3367 【模板】并查集
题目描述 如题,现在有一个并查集,你需要完成合并和查询操作. 输入格式 第一行包含两个整数N.M,表示共有N个元素和M个操作. 接下来M行,每行包含三个整数Zi.Xi.Yi 当Zi=1时,将Xi与Yi ...
- C#编写dll进行sql server数据库扩展储存过程
一.编写C#函数文件 1.新建一个类库文件 备注:sqlserver 2008只能用.net3.5版本. 2.如有想加入强命名的话可如下步骤: 参考博文:https://blog.csdn.net/d ...
- Java基础之Collection与Collections浅析
Java基础之Collection与Collections浅析 一.前言: 位于Java.util包下的Collection与Collections都是Java中重要的工具类,它们都是Java集合框架 ...
- 使用docker快速搭建Permeate渗透测试系统实践
一.背景 笔者最近在做一场Web安全培训,其中需要搭建一套安全测试环境:在挑选渗透测试系统的时候发现permeate渗透测试系统比较满足需求,便选择了此系统:为了简化这个步骤,笔者将系统直接封装到了d ...
- javaScript 基础知识汇总(四)
1.对象 概念:对象可以通过花括号{...} 和其中包含一些可选的属性来创建. 属性时一个键值对,键是一个字符串,值可以是任何类型. 对象的创建 let user = new Object(); // ...
- 新手学习FFmpeg - 调用API完成录屏并进行H.264编码
Screen Record H.264 目前在网络传输视频/音频流都一般会采用H.264进行编码,所以尝试调用FFMPEG API完成Mac录屏功能,同时编码为H.264格式. 在上一篇文章中,通过调 ...
- ImportError: cannot import name '_obtain_input_shape' from 'keras.applications.imagenet_utils'
报错 Using TensorFlow backend. Traceback (most recent call last): File "D:/PyCharm 5.0.3/WorkSpac ...