在項目中哼转,經(jīng)常會使用到多線程城舞,例如android本身封裝了handler來進行多線程通信,平時會用到eventbus忙灼,rx這樣的框架來處理匠襟,自己用鎖的時候已經(jīng)很少了,但還是無法完全避免该园。多線程的概念非常好理解酸舍,是為了提高工作效率的一種方法,尤其在現(xiàn)代多cpu多核的計算機下里初,利用好各個資源是非常有必要的啃勉。在android等交互上,為了給用戶良好體驗双妨,不能阻塞用戶交互請求也必須在非ui線程上更新ui淮阐。
線程之間如何正確的通信是我們所關(guān)注的最關(guān)鍵的點。
首先問題在于線程為什么存在同步和通信的問題刁品,這個可以看些java線程內(nèi)存模型的文章泣特,簡單來說原因在于,jvm為每個線程維護了一個私有內(nèi)存挑随,線程間是不能直接通信的状您,他們通過與主存通信完成同步。大家知道兜挨,所謂線程膏孟,其實就是運行在cpu的指令流,就是方法的順序執(zhí)行拌汇。比如說柒桑,方法 f(),在線程1上被調(diào)用的,則這個方法是在線程1上執(zhí)行的担猛,咋一看像句廢話幕垦,其實很多人會陷入某個變量是哪個線程的,比如說handler的理解上傅联,經(jīng)常有說是哪個線程的handler先改,實際上只是他們的loop方法在那個線程調(diào)用了而已,那只是一種易于記憶的說法蒸走,希望大家仔細思考仇奶。這里也同樣說明了另外一個問題,即局部變量是不存在線程同步問題的比驻,因為它的生命周期就是一個方法该溯,即一個局部變量對象只存在于一個線程上岛抄。而相應(yīng)的全局變量是存在線程安全問題的。每個線程內(nèi)存維護了一份該變量的拷貝狈茉,或者說clone更便于理解夫椭,拿mips作為模型舉例來理解,就是比如全局變量A a;會有一個寄存器存儲其的地址氯庆,對應(yīng)地址的內(nèi)存上存有A的信息蹭秋,成員變量值什么的,每個線程用到時都會從主存拷貝一份以上的信息堤撵。但是并不是立刻同步的仁讨,想要立刻寫回可用volatile修飾。(非常不建議的做法)
這里实昨,比如A里的成員變量 int mTv = 1洞豁;在線程1上做了mTv++運算,變成2荒给,這時還未寫回丈挟,但是線程2又做了一遍,則出現(xiàn)了同步問題锐墙,與我們預(yù)期是不一樣的礁哄。這就是我們所要注意和需要解決的問題。
多線程不是java特有的溪北,甚至java是沒有自己的線程模型的,而是直接采用了操作系統(tǒng)的線程模型夺脾。很多維護機制都是從操作系統(tǒng)來的之拨。下面介紹一下大家最常見到的volatile,(就是大家經(jīng)常覺得反正加了保險的那個)咧叭,這里已經(jīng)無數(shù)工程師勸過蚀乔,這個要盡量避免使用,因為至今其使用場景都是非常之低的菲茬,反而會帶來一些其他的問題吉挣。那么它到底什么意思呢。volatile修飾變量婉弹,有強制從主存讀和回寫的作用睬魂,理解起來很簡單,就是用的時候從主存讀镀赌,改變了就往主存回寫氯哮。請注意,它完全不能解決我們的多線程問題I谭稹喉钢!這里就不得不提到另一個人氣更高的關(guān)鍵字:synchronized姆打,這也是jdk5唯一實現(xiàn)鎖的方式(也就是說volatile根本不能算鎖),理解更加簡單肠虽,比如舉個例子幔戏,被其修飾的變量是不會被兩個線程同時調(diào)用改變的,是真真正正的鎖税课,當(dāng)然這是最重的鎖了闲延,大家想一下就知道這種方式是非常保險的,但也是非常不高效的(就是那個最笨的方法)伯复。synchronized可以保證操作是原子性的慨代,volatile是不能保證的。
什么叫原子性呢啸如,大家知道物理學(xué)原子某種程度上算是物體組成基本物質(zhì)了侍匙,可以理解為不可分割的,這里只是便于大家理解關(guān)鍵字概念叮雳,不涉及物理學(xué)知識想暗。強制讀寫就不是一個原子性操作,比如i++帘不,看起來是一步说莫,實際上,要從主存讀寞焙,然后把值拷貝給臨時變量储狭,臨時變量加一賦回i,然后寫回主存捣郊,也就是說辽狈,這整個是可以分割的,是可以進行一半暫停的呛牲,這里我們完成了加法運算還沒寫回主存刮萌,另一個線程調(diào)用i,從主存讀值娘扩,顯然出現(xiàn)了同步問題着茸。線程工作內(nèi)存可以說是主存的一份緩存,為了避免緩存不一致琐旁,volatile需要廢掉此緩存涮阔。除了內(nèi)存緩存之外,在CPU硬件級別也是有緩存的旋膳,即寄存器澎语。假如線程A將變量X由0修改為1的時候,CPU是在其緩存內(nèi)操作,沒有及時回寫到內(nèi)存擅羞,那么JVM是無法X=1是能及時被之后執(zhí)行的線程B看到的尸变,JVM在處理volatile變量的時候,也同樣用了硬件級別的緩存一致性原則减俏。詳細請查閱硬件級別cpu緩存相關(guān)的博客召烂。
volatile是可以防止指令重排的,我覺得這個沒什么特別大意義娃承,大家感興趣可以自己了解一下奏夫,其實就是匯編里那個流水線,可以算是cpu對代碼運行的優(yōu)化历筝,總之建議大家不要隨意使用酗昼。使用可以參閱正確使用 Volatile 變量。
synchronized這個用法梳猪,從對方法的修飾上來講麻削,其實可以理解為對對象的修飾,即是每個普通方法加了一個鎖春弥,這個鎖便是對象本身呛哟。所以直接以synchronized (a){}為例,這里是給代碼塊上了一把對象a的鎖匿沛。鎖在Java內(nèi)存模型里在不同機制下對應(yīng)不同的數(shù)據(jù)結(jié)構(gòu)扫责。每個對象都有個長度2個字寬的對象頭(在32位虛擬機里,1字寬是4個字節(jié)逃呼,64位虛擬機里鳖孤,1字寬是8個字節(jié)。如果是數(shù)組對象抡笼,則對象頭是3個字寬淌铐,其中第三個字存儲數(shù)組的長度),這里面存儲了對象的hashcode或鎖信息蔫缸,官方稱它為“Mark Word”,如下圖:
對象頭的最后兩位存儲了鎖的標(biāo)志位际起,01是初始狀態(tài)拾碌,未加鎖,其對象頭里存儲的是對象本身的哈希碼街望,隨著鎖級別的不同校翔,對象頭里存儲不同的內(nèi)容。偏向鎖存儲的是當(dāng)前占用此對象的線程ID灾前;而輕量級則存儲指向線程棧中鎖記錄的指針防症。以上參考Java的多線程機制系列:(三)synchronized的同步原理。一把鎖被一個線程持有,走完鎖住得代碼快鎖會被自動釋放蔫敲,這里也可以在運行一半調(diào)用a.wait()方法釋放鎖饲嗽,阻塞當(dāng)前線程。另一個線程執(zhí)行到某段代碼后希望剛才那個線程繼續(xù)運行奈嘿,則可以調(diào)用a.notify()。當(dāng)?shù)却趏bj上線程收到obj.notify()時,它就能重新獲得obj的獨占鎖疗杉,并繼續(xù)運行象对。注意了,notify()方法是隨機喚起等待在當(dāng)前對象的某一個線程叶圃。
以上可以作為多線程入門的參考袄膏,有時間寫一下高級并發(fā)包的使用。