Java对字符串操作做了许多的优化,使用符号“+”来作为字符串拼接操作就是其中之一。
今天来抠一下这个东西的细节。
对于大部分Java开发来说,都知道Java会使用StringBuilder
来优化字符串拼接操作。这种优化的一个极为重要的出发点就是,String
在Java里面是一个不可变的对象,所谓的字符串拼接不过是用被拼接字符串的内容来创建一个新的字符串。
如果在Java没有优化的情况下,字符串拼接就变成不断创建新的String
对象,每一个创建的对象都是一次拼接的结果。
StringBuilder
可以减少这种开销。StringBuilder
的原理非常接近ArrayList
,即内部维持一个数组,在容量不足的情况下扩容。因此在使用StringBuilder
的情况下,可以有效减少创建对象的次数。
那么问题来了:
- 是所有的字符串拼接都会被优化吗?
- 编译器做这种优化的时候,是如何确保线程安全的?
所有的字符串拼接都会被优化吗?
答案是YES,但是并不是所有的字符串拼接都会被同种方式——即使用StringBuilder
——所优化。
这里存在一种更加强大的优化:编译器如果在编译期间就能确定字符串拼接的结果,那么编译器会将字符串拼接操作去掉,改为直接使用拼接后的结果——即编译器自身完成这个拼接操作。
实际上,这是编译器优化的一小部分工作。除了字符串拼接以外,还有数值计算,也会有类似的优化。换句话来说,在现代编译器里面,编译器会努力把计算提前做完——前提是它能够确切结算出来结果。与之类似的一个东西是Java的类加载过程会完成部分方法解析,即将方法调用指向真正的方法。这些体现的核心理念就是能在运行前完成的,就做完。
如:
字节码是:
也就是它实际上是直接使用hello world
作为打印参数的值。
这里有意思的是,它在0,2,3,5的四条指令,实际上是可以忽略的。不过这并不是字符串拼接造成的,实际上是编译整体不够智能造成的。编译器其实在这个时候并没有断定后面除了用于字符串拼接以外,a
和b
两个局部变量没有再使用过。所以编译器只能非常保守的继续保留着四条指令。
这四条指令在JIT阶段有极大的可能会被优化掉。不过那都是在运行期的时候了。
另外,是否注意到图中我将两个局部变量都声明成了final
了。那是因为,只有声明成final
,编译器才能确定该变量的值,并且可以肯定这个变量的值在拼接操作并未被修改过。
如果没有final
关键字,那么会变成:
也就是使用StringBuiler
。
这里我要额外讨论一个所谓的事实final
变量。在Java里面,最开始使用内部匿名类的时候,内部匿名类要使用外部变量,那么只能将该外部变量声明成final
。
否则编译器会报错。
直到后来(忘了是哪个版本,好像是Java8引入lambda表达式的时候),如果编译器确定你这个变量中途并未被修改过,那么即便不声明成final
都可以在内部匿名类使用。
这就是所谓的事实final
变量。这个名词是我杜撰的,专业的说法不知道叫什么。
所以理论上,编译器是完全可以断定在这个过程中字符串变量有没有被修改过,而后执行这种优化的。很可惜,编译器并没有利用这个信息。这是我一直觉得稍微有点遗憾的地方。
StringBuilder的优化是如何保证线程安全的?
这是一个看起来没什么营养的问题,一思考又觉得很有营养的问题,考虑清楚之后终于确定的确没什么营养的问题。
答案是,其实它不保证线程安全,它只是保证和不用StringBuilder
优化时候的语义一致。这就是指,如果不是用StringBuiler
的地方线程不安全,那么使用StringBuilder
优化也不安全。
首先,大部分优化是安全的,这种线程安全的第一条保证是:String是不可变类型。这个无需多说,稍微思考一下就知道的。
再深入一点的话,如前面的例子,因为字符串只出现在方法里面,是作为方法的局部变量出现的,所以天然是线程安全的。
那么,如果我的代码是这样的呢?
这是一个初看起来会线程安全的代码,实际上却并没有的代码。
先来分析staticC
。staticC
是在类初始化的时候完成计算的。JVM的类加载机制可以确保,对于一个类加载器来说,staticC
那句代码,只会被一个线程执行。在完成类初始化完成之前,staticA
和staticB
是无法被修改的。所以这个可以保证是线程安全的。
而变量c
就要复杂一点了,理论上,c
会在调用构造初始化方法之前完成初始化。如果从字节码的角度来解释,就是c
会在构造方法里面的任何代码执行之前完成。
然而创建对象,在JVM层面上并不是一个原子步骤,它大概是有两步:
-
new
指令执行,大体上可以理解为分配内存; - 调用初始化方法
;
所以在JIT的情况下,可能第一步执行完之后,引用就被外部获取了,这个时候他们就可能并发修改变量a
或者b
的值:
-
new
指令执行,大体上可以理解为分配内存; -
a
或者b
的值被修改---JIT情况下; - 调用初始化方法
;
所以我才说,这种优化只保证和没有优化的语义一致。和线程安全没什么关系。