Java對字符串操作做了許多的優(yōu)化,使用符號“+”來作為字符串拼接操作就是其中之一潜叛。
今天來摳一下這個東西的細節(jié)秽褒。
對于大部分Java開發(fā)來說,都知道Java會使用StringBuilder
來優(yōu)化字符串拼接操作威兜。這種優(yōu)化的一個極為重要的出發(fā)點就是销斟,String
在Java里面是一個不可變的對象,所謂的字符串拼接不過是用被拼接字符串的內(nèi)容來創(chuàng)建一個新的字符串椒舵。
如果在Java沒有優(yōu)化的情況下蚂踊,字符串拼接就變成不斷創(chuàng)建新的String
對象,每一個創(chuàng)建的對象都是一次拼接的結(jié)果笔宿。
StringBuilder
可以減少這種開銷犁钟。StringBuilder
的原理非常接近ArrayList
棱诱,即內(nèi)部維持一個數(shù)組,在容量不足的情況下擴容涝动。因此在使用StringBuilder
的情況下迈勋,可以有效減少創(chuàng)建對象的次數(shù)。
那么問題來了:
- 是所有的字符串拼接都會被優(yōu)化嗎醋粟?
- 編譯器做這種優(yōu)化的時候靡菇,是如何確保線程安全的?
所有的字符串拼接都會被優(yōu)化嗎米愿?
答案是YES厦凤,但是并不是所有的字符串拼接都會被同種方式——即使用StringBuilder
——所優(yōu)化。
這里存在一種更加強大的優(yōu)化:編譯器如果在編譯期間就能確定字符串拼接的結(jié)果育苟,那么編譯器會將字符串拼接操作去掉较鼓,改為直接使用拼接后的結(jié)果——即編譯器自身完成這個拼接操作。
實際上宙搬,這是編譯器優(yōu)化的一小部分工作笨腥。除了字符串拼接以外,還有數(shù)值計算勇垛,也會有類似的優(yōu)化脖母。換句話來說,在現(xiàn)代編譯器里面闲孤,編譯器會努力把計算提前做完——前提是它能夠確切結(jié)算出來結(jié)果谆级。與之類似的一個東西是Java的類加載過程會完成部分方法解析,即將方法調(diào)用指向真正的方法讼积。這些體現(xiàn)的核心理念就是能在運行前完成的肥照,就做完。
如:
字節(jié)碼是:
也就是它實際上是直接使用hello world
作為打印參數(shù)的值勤众。
這里有意思的是舆绎,它在0,2们颜,3吕朵,5的四條指令,實際上是可以忽略的窥突。不過這并不是字符串拼接造成的努溃,實際上是編譯整體不夠智能造成的。編譯器其實在這個時候并沒有斷定后面除了用于字符串拼接以外阻问,a
和b
兩個局部變量沒有再使用過梧税。所以編譯器只能非常保守的繼續(xù)保留著四條指令。
這四條指令在JIT階段有極大的可能會被優(yōu)化掉。不過那都是在運行期的時候了第队。
另外哮塞,是否注意到圖中我將兩個局部變量都聲明成了final
了。那是因為斥铺,只有聲明成final
彻桃,編譯器才能確定該變量的值,并且可以肯定這個變量的值在拼接操作并未被修改過晾蜘。
如果沒有final
關(guān)鍵字邻眷,那么會變成:
也就是使用StringBuiler
。
這里我要額外討論一個所謂的事實final
變量剔交。在Java里面肆饶,最開始使用內(nèi)部匿名類的時候,內(nèi)部匿名類要使用外部變量岖常,那么只能將該外部變量聲明成final
驯镊。
否則編譯器會報錯。
直到后來(忘了是哪個版本竭鞍,好像是Java8引入lambda表達式的時候)板惑,如果編譯器確定你這個變量中途并未被修改過,那么即便不聲明成final
都可以在內(nèi)部匿名類使用偎快。
這就是所謂的事實final
變量冯乘。這個名詞是我杜撰的,專業(yè)的說法不知道叫什么晒夹。
所以理論上裆馒,編譯器是完全可以斷定在這個過程中字符串變量有沒有被修改過,而后執(zhí)行這種優(yōu)化的丐怯。很可惜喷好,編譯器并沒有利用這個信息。這是我一直覺得稍微有點遺憾的地方读跷。
StringBuilder的優(yōu)化是如何保證線程安全的梗搅?
這是一個看起來沒什么營養(yǎng)的問題,一思考又覺得很有營養(yǎng)的問題效览,考慮清楚之后終于確定的確沒什么營養(yǎng)的問題些膨。
答案是,其實它不保證線程安全钦铺,它只是保證和不用StringBuilder
優(yōu)化時候的語義一致。這就是指肢预,如果不是用StringBuiler
的地方線程不安全矛洞,那么使用StringBuilder
優(yōu)化也不安全。
首先,大部分優(yōu)化是安全的沼本,這種線程安全的第一條保證是:String是不可變類型噩峦。這個無需多說,稍微思考一下就知道的抽兆。
再深入一點的話识补,如前面的例子,因為字符串只出現(xiàn)在方法里面辫红,是作為方法的局部變量出現(xiàn)的凭涂,所以天然是線程安全的。
那么贴妻,如果我的代碼是這樣的呢切油?
這是一個初看起來會線程安全的代碼,實際上卻并沒有的代碼名惩。
先來分析staticC
澎胡。staticC
是在類初始化的時候完成計算的。JVM的類加載機制可以確保娩鹉,對于一個類加載器來說攻谁,staticC
那句代碼,只會被一個線程執(zhí)行弯予。在完成類初始化完成之前戚宦,staticA
和staticB
是無法被修改的。所以這個可以保證是線程安全的熙涤。
而變量c
就要復雜一點了阁苞,理論上,c
會在調(diào)用構(gòu)造初始化方法之前完成初始化祠挫。如果從字節(jié)碼的角度來解釋那槽,就是c
會在構(gòu)造方法里面的任何代碼執(zhí)行之前完成。
然而創(chuàng)建對象等舔,在JVM層面上并不是一個原子步驟骚灸,它大概是有兩步:
-
new
指令執(zhí)行,大體上可以理解為分配內(nèi)存慌植; - 調(diào)用初始化方法
<init>
甚牲;
所以在JIT的情況下,可能第一步執(zhí)行完之后蝶柿,引用就被外部獲取了丈钙,這個時候他們就可能并發(fā)修改變量a
或者b
的值:
-
new
指令執(zhí)行,大體上可以理解為分配內(nèi)存交汤; -
a
或者b
的值被修改---JIT情況下雏赦; - 調(diào)用初始化方法
<init>
;
所以我才說,這種優(yōu)化只保證和沒有優(yōu)化的語義一致星岗。和線程安全沒什么關(guān)系填大。