在項目中,經常會使用到多線程,例如android本身封裝了handler來進行多線程通信,平時會用到eventbus,rx這樣的框架來處理,自己用鎖的時候已經很少了,但還是無法完全避免。多線程的概念非常好理解,是為了提高工作效率的一種方法,尤其在現代多cpu多核的計算機下,利用好各個資源是非常有必要的。在android等交互上,為了給用戶良好體驗,不能阻塞用戶交互請求也必須在非ui線程上更新ui。
線程之間如何正確的通信是我們所關注的最關鍵的點。
首先問題在于線程為什么存在同步和通信的問題,這個可以看些java線程內存模型的文章,簡單來說原因在于,jvm為每個線程維護了一個私有內存,線程間是不能直接通信的,他們通過與主存通信完成同步。大家知道,所謂線程,其實就是運行在cpu的指令流,就是方法的順序執行。比如說,方法 f(),在線程1上被調用的,則這個方法是在線程1上執行的,咋一看像句廢話,其實很多人會陷入某個變量是哪個線程的,比如說handler的理解上,經常有說是哪個線程的handler,實際上只是他們的loop方法在那個線程調用了而已,那只是一種易于記憶的說法,希望大家仔細思考。這里也同樣說明了另外一個問題,即局部變量是不存在線程同步問題的,因為它的生命周期就是一個方法,即一個局部變量對象只存在于一個線程上。而相應的全局變量是存在線程安全問題的。每個線程內存維護了一份該變量的拷貝,或者說clone更便于理解,拿mips作為模型舉例來理解,就是比如全局變量A a;會有一個寄存器存儲其的地址,對應地址的內存上存有A的信息,成員變量值什么的,每個線程用到時都會從主存拷貝一份以上的信息。但是并不是立刻同步的,想要立刻寫回可用volatile修飾。(非常不建議的做法)
這里,比如A里的成員變量 int mTv = 1;在線程1上做了mTv++運算,變成2,這時還未寫回,但是線程2又做了一遍,則出現了同步問題,與我們預期是不一樣的。這就是我們所要注意和需要解決的問題。
多線程不是java特有的,甚至java是沒有自己的線程模型的,而是直接采用了操作系統的線程模型。很多維護機制都是從操作系統來的。下面介紹一下大家最常見到的volatile,(就是大家經常覺得反正加了保險的那個),這里已經無數工程師勸過,這個要盡量避免使用,因為至今其使用場景都是非常之低的,反而會帶來一些其他的問題。那么它到底什么意思呢。volatile修飾變量,有強制從主存讀和回寫的作用,理解起來很簡單,就是用的時候從主存讀,改變了就往主存回寫。請注意,它完全不能解決我們的多線程問題!!這里就不得不提到另一個人氣更高的關鍵字:synchronized,這也是jdk5唯一實現鎖的方式(也就是說volatile根本不能算鎖),理解更加簡單,比如舉個例子,被其修飾的變量是不會被兩個線程同時調用改變的,是真真正正的鎖,當然這是最重的鎖了,大家想一下就知道這種方式是非常保險的,但也是非常不高效的(就是那個最笨的方法)。synchronized可以保證操作是原子性的,volatile是不能保證的。
什么叫原子性呢,大家知道物理學原子某種程度上算是物體組成基本物質了,可以理解為不可分割的,這里只是便于大家理解關鍵字概念,不涉及物理學知識。強制讀寫就不是一個原子性操作,比如i++,看起來是一步,實際上,要從主存讀,然后把值拷貝給臨時變量,臨時變量加一賦回i,然后寫回主存,也就是說,這整個是可以分割的,是可以進行一半暫停的,這里我們完成了加法運算還沒寫回主存,另一個線程調用i,從主存讀值,顯然出現了同步問題。線程工作內存可以說是主存的一份緩存,為了避免緩存不一致,volatile需要廢掉此緩存。除了內存緩存之外,在CPU硬件級別也是有緩存的,即寄存器。假如線程A將變量X由0修改為1的時候,CPU是在其緩存內操作,沒有及時回寫到內存,那么JVM是無法X=1是能及時被之后執行的線程B看到的,JVM在處理volatile變量的時候,也同樣用了硬件級別的緩存一致性原則。詳細請查閱硬件級別cpu緩存相關的博客。
volatile是可以防止指令重排的,我覺得這個沒什么特別大意義,大家感興趣可以自己了解一下,其實就是匯編里那個流水線,可以算是cpu對代碼運行的優化,總之建議大家不要隨意使用。使用可以參閱正確使用 Volatile 變量。
synchronized這個用法,從對方法的修飾上來講,其實可以理解為對對象的修飾,即是每個普通方法加了一個鎖,這個鎖便是對象本身。所以直接以synchronized (a){}為例,這里是給代碼塊上了一把對象a的鎖。鎖在Java內存模型里在不同機制下對應不同的數據結構。每個對象都有個長度2個字寬的對象頭(在32位虛擬機里,1字寬是4個字節,64位虛擬機里,1字寬是8個字節。如果是數組對象,則對象頭是3個字寬,其中第三個字存儲數組的長度),這里面存儲了對象的hashcode或鎖信息,官方稱它為“Mark Word”,如下圖:
對象頭的最后兩位存儲了鎖的標志位,01是初始狀態,未加鎖,其對象頭里存儲的是對象本身的哈希碼,隨著鎖級別的不同,對象頭里存儲不同的內容。偏向鎖存儲的是當前占用此對象的線程ID;而輕量級則存儲指向線程棧中鎖記錄的指針。以上參考Java的多線程機制系列:(三)synchronized的同步原理。一把鎖被一個線程持有,走完鎖住得代碼快鎖會被自動釋放,這里也可以在運行一半調用a.wait()方法釋放鎖,阻塞當前線程。另一個線程執行到某段代碼后希望剛才那個線程繼續運行,則可以調用a.notify()。當等待在obj上線程收到obj.notify()時,它就能重新獲得obj的獨占鎖,并繼續運行。注意了,notify()方法是隨機喚起等待在當前對象的某一個線程。
以上可以作為多線程入門的參考,有時間寫一下高級并發包的使用。