多線(xiàn)程給我們帶來(lái)了更好的資源利用和更好的程序響應,不過(guò)也為我們帶來(lái)了必須要解決的麻煩!
線(xiàn)程安全:當多個(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn)一個(gè)對象時(shí),如果不用考慮這些線(xiàn)程在運行時(shí)環(huán)境下的調度和交替執行,也不需要進(jìn)行額外的同步,或者在調用方進(jìn)行任何其他的協(xié)調操作,調用這個(gè)對象的行為都可以獲得正確的結果,那就稱(chēng)這個(gè)對象是線(xiàn)程安全的。
簡(jiǎn)單點(diǎn)理解就是對象在不用額外同步的情況下也能支持多線(xiàn)程訪(fǎng)問(wèn)并且能獲得正確的結果,那么這個(gè)對象就是線(xiàn)程安全的。
那么對象要如何實(shí)現線(xiàn)程安全呢,最直接的方法就是對象沒(méi)有共享的數據,也就是沒(méi)有屬性,也就是無(wú)狀態(tài)對象,無(wú)狀態(tài)對象一定是線(xiàn)程安全的。
但是無(wú)狀態(tài)對象并不能支持所有程序,一個(gè)程序的運行必定會(huì )出現有狀態(tài)的對象用來(lái)存儲一些數據,那么要怎么解決呢?解決線(xiàn)程安全主要有三個(gè)方向:
1、訪(fǎng)問(wèn)共享數據時(shí)使用同步;
2、不再共享數據;
3、共享數據不可變;
那么我們不妨在無(wú)狀態(tài)對象的基礎上來(lái)進(jìn)行分析,現在我們在無(wú)狀態(tài)對象的基礎上加一個(gè)狀態(tài),這個(gè)對象就不是線(xiàn)程安全的了,那么要如何保證它繼續線(xiàn)程安全呢,只需要保證這個(gè)狀態(tài)的更新是原子性就行了,可以利用java.util.concurrent中的對象去替換,比如這個(gè)狀態(tài)時(shí)int,我們可以把他換成AtomicInteger。
但是如果繼續在給對象加狀態(tài),即時(shí)這些狀態(tài)都是原子性的,比如就算有兩個(gè)AtomicInteger屬性,雖然他們各自的操作都可以實(shí)現原子性,但是組合起來(lái)就不是原子性,也就不會(huì )是線(xiàn)程安全的,這種情況下就只能通過(guò)鎖來(lái)解決了,所有要訪(fǎng)問(wèn)這些狀態(tài)的線(xiàn)程就必須獲取鎖,并且這個(gè)鎖必須是同一個(gè)鎖,也就是必須是由同一個(gè)鎖來(lái)保護這幾個(gè)狀態(tài)的一致性。
之前文章中有專(zhuān)門(mén)講過(guò)Java的內存模型,其中最主要的模塊是主內存與工作內存,主內存存儲一些可以共享的變量比如實(shí)例字段、靜態(tài)字段和構成數組對象的元素,每個(gè)線(xiàn)程都擁有有屬于自己的工作內存,每次一個(gè)線(xiàn)程需要使用這些共享的變量時(shí),都是先加載到工作內存中在進(jìn)行計算,而在把共享變量加載到工作內存中時(shí)可能其他線(xiàn)程已經(jīng)修改了這個(gè)數據,也就是剛剛加載到工作內存的變量是過(guò)時(shí)、失效的數據,而我們大多數造成線(xiàn)程不安全的原因是程序基于一種可能失效的數據做出判斷或者執行某個(gè)計算。
而volatile關(guān)鍵字就是用來(lái)確保在變量更新后及時(shí)的通知到其他線(xiàn)程的,但是即使及時(shí)通知到其他線(xiàn)程,由于線(xiàn)程對變量的操作也有可能不是原子性的,比如i++,要讀取i、再計算、更新三個(gè)操作,如果在這個(gè)過(guò)程中i的值被其他線(xiàn)程改變,那么這個(gè)計算結果最終會(huì )覆蓋前面的值而造成錯誤的結果,所以這種操作還是要考鎖來(lái)控制,在i++的整個(gè)過(guò)程中都不允許其他線(xiàn)程對i進(jìn)行訪(fǎng)問(wèn),這樣就保證了線(xiàn)程的安全性,也就是volatile能保證變量的可見(jiàn)性,而加鎖能保證變量的可見(jiàn)性和原子性。
那么volatile什么時(shí)候可以使用呢?主要有以下情況:
1、對變量寫(xiě)操作不依賴(lài)變量的當前值,或者確保只有一個(gè)線(xiàn)程更新變量;
2、該變量不會(huì )與其他狀態(tài)變量一起納入不變性條件者;
3、在訪(fǎng)問(wèn)變量時(shí)不需要加鎖;
如果我們能保證數據只能在單線(xiàn)程內訪(fǎng)問(wèn),那么就不需要同步,這種技術(shù)叫線(xiàn)程封閉,線(xiàn)程封閉主要有以下幾種方法:
Ad-hoc線(xiàn)程封閉:完全由程序實(shí)現,一般的方法是某些特定的模塊實(shí)現為一個(gè)單線(xiàn)程操作。比如對一個(gè)volatile變量確保只有一個(gè)線(xiàn)程去修改,那么就保證了線(xiàn)程的安全。
棧封閉:變量全部是局部變量,局部變量全部都只能在方法內部使用,其他線(xiàn)程肯定無(wú)法訪(fǎng)問(wèn)。這種方式要小心對象逃逸。
ThreadLocal類(lèi):ThreadLocal對象通常用于防止對可變的單實(shí)例變量或全局變量進(jìn)行共享,比如JDBC的連接在多線(xiàn)程情況下就不是線(xiàn)程安全的,如果線(xiàn)程調用多個(gè)方法更新數據庫,如果JDBC的連接不是用一個(gè)對象就無(wú)法保證事務(wù),所以在ThreadLocal中設置一個(gè)自己的連接,不管這個(gè)線(xiàn)程調用了多少個(gè)更新數據庫的方法,他們都是用的同一個(gè)連接,就能夠保證事務(wù)。
保證線(xiàn)程安全的第二個(gè)方法是共享數據不可變,如果我們能保證獲取到的數據不可變,那么就不用擔心在獲取到數據后被其他線(xiàn)程修改的風(fēng)險。Java中有一個(gè)關(guān)鍵字final可以保證數據不可變。不過(guò)如果只有一個(gè)字段好控制,如果有多個(gè)呢?我們可以把多個(gè)放到一個(gè)對象里,然后這多個(gè)字段都用final修飾,最后這個(gè)對象也就是不可變的對象了,也可以安全的使用,下面是一個(gè)例子,如下圖:

一個(gè)對象可以變成不可變的對象,那么它肯定是線(xiàn)程安全的,不過(guò)有些雖然是可變的但是我們肯定不會(huì )去改變它,比如創(chuàng )建時(shí)間因為我們不會(huì )去改變他所以它事實(shí)上也是不可變的對象,也是線(xiàn)程安全的。
所以一個(gè)對象我們應該怎么保證它的線(xiàn)程安全呢?大概分4個(gè)方向:
1、線(xiàn)程封閉:對象只能被一個(gè)線(xiàn)程擁有和修改;
2、只讀共享:所有線(xiàn)程只能讀不能改,不可變對象和事實(shí)不可變對象;
3、線(xiàn)程安全共享:對象內部自己實(shí)現線(xiàn)程安全,所以線(xiàn)程通過(guò)指定方法調用;
4、保護對象:特定的鎖才能訪(fǎng)問(wèn)對象;
內容較多也有點(diǎn)亂,用導圖整理下重點(diǎn)內容如下圖:

Java程序員日常學(xué)習筆記,如理解有誤歡迎各位交流討論!
聯(lián)系客服