关于解决并发问题,99%的程序员都会忽略的一个重要方案!
△Hollis, 一個(gè)對Coding有著獨(dú)特追求的人△
這是Hollis的第?370?篇原創(chuàng)分享
作者 l zyz1992
來源 l Hollis(ID:hollischuang)
在并發(fā)編程的世界里,共享變量的線程安全問題永遠(yuǎn)是一個(gè)無法避免且不得不面對的問題,如果只有讀的情況,那么永遠(yuǎn)也不會(huì)出現(xiàn)線程安全的問題,因?yàn)槎嗑€程讀永遠(yuǎn)是線程安全的,但是多線程讀寫一定會(huì)存在線程安全的問題。
那既然這么說是不是通過只讀就能解決并發(fā)問題呢?其實(shí)最簡單的辦法就是讓共享變量只有讀操作,而沒有寫操作。這個(gè)辦法如此重要,以至于被上升到了一種解決并發(fā)問題的設(shè)計(jì)模式:不變性(Immutability)模式。
所謂不變性,簡單來講,就是對象一旦被創(chuàng)建之后,狀態(tài)就不再發(fā)生變化。換句話說,就是變量一旦被賦值,就不允許修改了(沒有寫操作);沒有修改操作,也就是保持了不變性。
1、不可變性的類
在 java 中,如果要實(shí)現(xiàn)一個(gè)不可變的對象是很簡單的,將其定義為 final 即可,同樣類也是如此,只需要通過 final 來修飾某個(gè)類即可。同時(shí)將一個(gè)類所有的屬性都設(shè)置成 final 的,并且只允許存在只讀方法,那么這個(gè)類基本上就具備不可變性了。
更嚴(yán)格的做法是這個(gè)類本身也是 final 的,也就是不允許繼承。因?yàn)樽宇惪梢愿采w父類的方法,有可能改變不可變性,所以推薦你在實(shí)際工作中,使用這種更嚴(yán)格的做法。
我們在日常開發(fā)中,已經(jīng)在不知不覺中享受不可變模式帶來的好處,例如經(jīng)常用到的 String、Long、Integer、Double 等基礎(chǔ)類型的包裝類都具備不可變性,這些對象的線程安全性都是靠不可變性來保證的。
仔細(xì)翻看這些類的聲明、屬性和方法,你會(huì)發(fā)現(xiàn)它們都嚴(yán)格遵守不可變類的三點(diǎn)要求:類是 final 的,屬性也是 final 的。同樣的一旦某個(gè)類被 final 修飾,其本身就不能被繼承了,也就無法重寫其方法,即方法是只讀的。
既然說方法是只讀的,但是 Java 的 String 方法也有類似字符替換操作,這個(gè)不就已經(jīng)改變了value[] 變量了嗎?因?yàn)?value[] 是這么定義的。
我們結(jié)合 String 的源代碼(jdk8)來看一下 jdk 是如何處理這個(gè)問題的,下面是源碼的截圖
它實(shí)際上是重新定義了一個(gè)新的 buf[] 來保存數(shù)據(jù),這樣在最后返回?cái)?shù)據(jù)的時(shí)候確實(shí)沒有修改 原始的value[],而是將替換后的字符串作為返回值返回了。
通過分析 String 的實(shí)現(xiàn),你可能已經(jīng)發(fā)現(xiàn)了,如果具備不可變性的類,需要提供類似修改的功能,具體該怎么操作呢?做法很簡單,那就是創(chuàng)建一個(gè)新的不可變對象,這是與可變對象的一個(gè)重要區(qū)別,可變對象往往是修改自己的屬性。
所有的修改操作都創(chuàng)建一個(gè)新的不可變對象。但是一個(gè)問題的解決必然會(huì)帶來的新的問題,那就是這樣勢必在每次使用的時(shí)候都會(huì)創(chuàng)建新的對象,那豈不是無端的降低了系統(tǒng)的性能了浪費(fèi)了系統(tǒng)的資源?這個(gè)時(shí)候享元模式就可以大顯神通了。
2、享元模式避免創(chuàng)建重復(fù)對象
享元模式你可能實(shí)際開發(fā)中使用的很少,它是這么定義的:
享元模式(Flyweight Pattern):是一種軟件設(shè)計(jì)模式。它使用共享物件,用來盡可能減少內(nèi)存使用量以及分享資訊給盡可能多的相似物件;它適合用于只是因重復(fù)而導(dǎo)致使用無法令人接受的大量內(nèi)存的大量物件。
通常物件中的部分狀態(tài)是可以分享。常見做法是把它們放在外部數(shù)據(jù)結(jié)構(gòu),當(dāng)需要使用時(shí)再將它們傳遞給享元
看不懂沒關(guān)系,用一句直白話來概括就是:通過對象池的技術(shù)來避免重復(fù)的創(chuàng)建對象。這就好比是 Spring 中的容器(單例模式下),我們的對象都交給 Spring 容器來管理,這樣我們再使用的時(shí)候只需要到容器中去拿即可,而不是每次都去創(chuàng)建新的對象。
利用享元模式可以減少創(chuàng)建對象的數(shù)量,從而減少內(nèi)存占用。Java 語言里面 Long、Integer、Short、Byte等這些基本數(shù)據(jù)類型的包裝類都用到了享元模式。
享元模式本質(zhì)上其實(shí)就是一個(gè)對象池,利用享元模式創(chuàng)建對象的邏輯也很簡單:創(chuàng)建之前,首先去對象池里看看是不是存在;如果已經(jīng)存在,就利用對象池里的對象;如果不存在,就會(huì)新創(chuàng)建一個(gè)對象,并且把這個(gè)新創(chuàng)建出來的對象放進(jìn)對象池里。
jdk 源碼中是如何使用享元模式的呢?我們以 Long 這個(gè)類為例來解釋說明下。
Long 這個(gè)類并沒有照搬享元模式,Long 內(nèi)部維護(hù)了一個(gè)靜態(tài)的對象池,僅緩存了[-128,127]之間的數(shù)字,這個(gè)對象池在 JVM 啟動(dòng)的時(shí)候就創(chuàng)建好了,而且這個(gè)對象池一直都不會(huì)變化,也就是說它是靜態(tài)的。之所以采用這樣的設(shè)計(jì),是因?yàn)?Long 這個(gè)對象的狀態(tài)共有 264 種,實(shí)在太多,不宜全部緩存,而[-128,127]之間的數(shù)字利用率最高。
下面的示例代碼出自 Java 1.8,valueOf() 方法就用到了 LongCache 這個(gè)緩存,你可以結(jié)合著來加深理解。
在看下 LongCache 中的 cache 方法(關(guān)鍵地方都在圖片的注釋中了)
3、基本類型包裝類作為鎖對象
正是由于這些包裝類內(nèi)部用了享元模式,所以基本上所有的基礎(chǔ)類型的包裝類都不適合做鎖,因?yàn)樗鼈儍?nèi)部用到了享元模式,這會(huì)導(dǎo)致看上去私有的鎖,其實(shí)是共有的??聪孪旅娴拇a,我們假設(shè)以 Long 對象作為鎖,
class?A?{//定義一個(gè)?A?對象名字叫?aObj?,值為?1private?Long?aObj?=?Long.valueOf(1);//定義一個(gè)?B?對象名字叫?bObj,值為?1private?Long?bObj?=?Long.valueOf(1);private?void?a()?{//鎖對象是?aObjsynchronized?(aObj)?{System.out.println("正在執(zhí)行A方法,5秒以后退出");try?{TimeUnit.SECONDS.sleep(5);System.out.println("A執(zhí)行結(jié)束......");}?catch?(InterruptedException?e)?{e.printStackTrace();}}}private?void?b()?{//鎖對象是?bObjsynchronized?(bObj)?{System.out.println("正在執(zhí)行B方法,2秒以后退出");try?{TimeUnit.SECONDS.sleep(2);System.out.println("B執(zhí)行結(jié)束......");}?catch?(InterruptedException?e)?{e.printStackTrace();}}}public?static?void?main(String[]?args)?throws?InterruptedException?{A?a?=?new?A();//開通兩個(gè)線程來執(zhí)行,因?yàn)閍Obj?和?bObj?是不同的對象,所以理論上應(yīng)該是互不干擾的new?Thread(a::a).start();TimeUnit.SECONDS.sleep(1);new?Thread(a::b).start();}}?
但是卻出現(xiàn)了上面這樣的結(jié)果?為什么會(huì)是同步的執(zhí)行呢?就是因?yàn)橄碓J綄?dǎo)致的,因?yàn)?1 是在 [-128~127] 的,所以定義再多的對象都是直接從緩存池中拿的,并不會(huì)創(chuàng)建新的對象,即鎖的是同一個(gè)對象?,F(xiàn)在改成一個(gè)不在 [-128~127] 范圍之內(nèi)的,假設(shè)是128
結(jié)果為:
這個(gè)時(shí)候發(fā)現(xiàn)兩個(gè)是互不干擾的,也就是兩個(gè)鎖并不是同一個(gè)對象。
4、 使用 Immutability 模式的注意事項(xiàng)
在使用 Immutability 模式的時(shí)候,需要注意以下兩點(diǎn):
對象的所有屬性都是 final 的,并不能保證不可變性;
不可變對象也需要正確發(fā)布。
在 Java 語言中,final 修飾的屬性一旦被賦值,就不可以再修改,但是如果屬性的類型是普通對象,那么這個(gè)普通對象的屬性是可以被修改的。什么鬼?亂七八糟的。別急,我們來看個(gè)例子(畢竟光說含義就是等于在耍流氓)。
class?D?{final?C?c;public?D(C?c)?{this.c?=?c;}private?void?changeValue(int?salary)?{c.setSalary(salary);}public?static?void?main(String[]?args)?{C?c?=?new?C();c.setSalary(1);System.out.println("c.getSalary()?=?"?+?c.getSalary());D?d?=?new?D(c);d.changeValue(3);System.out.println("c.getSalary()?=?"?+?c.getSalary());}}在使用 Immutability 模式的時(shí)候一定要確認(rèn)保持不變性的邊界在哪里,是否要求屬性對象也具備不可變性。這里的C對象是不可變的,但是里面的屬性卻是可以修改的。如果想要屬性也不可以被修改,那么屬性也必須要定義為 final 的。像這樣的臨界問題在處理的時(shí)候一定要加倍小心。
5、本文小結(jié)
利用 Immutability 模式解決并發(fā)問題,也許你覺得有點(diǎn)陌生,其實(shí)你天天都在享受它的戰(zhàn)果。Java 語言里面的 String 和 Long、Integer、Double 等基礎(chǔ)類型的包裝類都具備不可變性,這些對象的線程安全性都是靠不可變性來保證的。Immutability 模式是最簡單的解決并發(fā)問題的方法,建議當(dāng)你試圖解決一個(gè)并發(fā)問題時(shí),可以首先嘗試一下 Immutability 模式,看是否能夠快速解決。
具備不變性的對象,只有一種狀態(tài),這個(gè)狀態(tài)由對象內(nèi)部所有的不變屬性共同決定。其實(shí)還有一種更簡單的不變性對象,那就是無狀態(tài)。無狀態(tài)對象內(nèi)部沒有屬性,只有方法。除了無狀態(tài)的對象,你可能還聽說過無狀態(tài)的服務(wù)、無狀態(tài)的協(xié)議等等。無狀態(tài)有很多好處,最核心的一點(diǎn)就是性能。在多線程領(lǐng)域,無狀態(tài)對象沒有線程安全問題,無需同步處理,自然性能很好;在分布式領(lǐng)域,無狀態(tài)意味著可以無限地水平擴(kuò)展,所以分布式領(lǐng)域里面性能的瓶頸一定不是出在無狀態(tài)的服務(wù)節(jié)點(diǎn)上。
?
技術(shù)交流群
最近有很多人問,有沒有讀者交流群,想知道怎么加入。
最近我創(chuàng)建了一些群,大家可以加入。交流群都是免費(fèi)的,只需要大家加入之后不要隨便發(fā)廣告,多多交流技術(shù)就好了。
目前創(chuàng)建了多個(gè)交流群,全國交流群、北上廣杭深等各地區(qū)交流群、面試交流群、資源共享群等。
有興趣入群的同學(xué),可長按掃描下方二維碼,一定要備注:全國 Or 城市 Or 面試 Or 資源,根據(jù)格式備注,可更快被通過且邀請進(jìn)群。
▲長按掃描
往期推薦居然有人提問“國家何時(shí)整治程序員的高薪現(xiàn)象”?
再見了,谷歌
Windows 11 再惹“眾怒”!網(wǎng)友:微軟就是逼我去買新電腦!
如果你喜歡本文,
請長按二維碼,關(guān)注?Hollis.
轉(zhuǎn)發(fā)至朋友圈,是對我最大的支持。
點(diǎn)個(gè)?在看?
喜歡是一種感覺
在看是一種支持
↘↘↘
總結(jié)
以上是生活随笔為你收集整理的关于解决并发问题,99%的程序员都会忽略的一个重要方案!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 编译原理_1
- 下一篇: 去掉我的电脑中WPS,百度云,360,爱