首先需要把問題限制在同一個jvm程序中。因為現在包含了很多共享記憶體映射,分布式緩存,緩存伺服器等相關的技術,會給跨程序的線程安全或者是說資料同步帶來問題。
按照java的記憶體模型,線程将會私有pc計數器,虛拟機棧。 公用jvm堆記憶體。
1. 當有線程并發的時候,因為多個線程可能操作同一塊堆記憶體位址這樣就會造成資源競争引發線程安全問題。
2. jvm server模式下對程式進行的優化:主要是重排序和執行時為了性能會把主存中的資料拷貝一份到線程自己的寄存器中,操作該對象并回寫主記憶體。 這兩種方式都會導緻目前線程操作的資料對于其他線程來講可見性方面出現問題,是以我統一稱為可見性問題。
3. jvm可能将long,double等64位數的操作轉化為兩個32位的操作。
1. 拷貝:
上代碼:
這段代碼在-server模式下,在某些硬體配置下的某些jvm實作下有可能永遠不會退出。 也就是readthread讀不到novisibily線程寫入的ready值。也就是novisibily對ready的修改,對于readthread來講不可見。
解決辦法是聲明ready為volatile的。如下:
這裡利用了volatile 的一個語義,是關閉寄存器拷貝優化,每一次都直接讀寫主記憶體。
另外一個辦法使使用synchronize關鍵字修飾change方法:
這兩種辦法稍有不同, volatile是直接讀取主存,而synchronized是在解鎖的時候保證寄存器回寫主存,還是會利用到寄存器的
2. 重排序
這個比較好的例子是double check的單例模式:
這個本來是一個線程安全的單例模式, 為了性能上的優化,又加了一個instance==null的判斷。本來是極好的,但是因為有重排序的存在,就會存在問題。
重排序:
jvm在編譯的時候會保證單線程模式下的結果是正确的,但是其中代碼的順序可能會進行重排序,或者亂序,主要是為了更好的利用多cpu資源(亂序), 以及更好的利用寄存器, 比如1 a = 1; b = 2; a=3;三個語句,如果b執行的時候可能會占用a的寄存器位置,jvm可能會把a=3語句提到b=2前面,減少寄存器置換次數。
比如上面的 instance = new doublechecksingleton()這部分代碼的僞位元組碼為:
1. memory = allocate() // 配置設定記憶體
2. init(memory) // 初始化對象
3. instance = memory // 執行個體指向剛才初始化的記憶體位址。
4. 第一次通路instance
在jvm的時候有可能2.3的位置進行了重新排序,因為jvm隻保證構造器執行完之後的結果是正确的,但是執行順序可能會有變化。 這個時候并發調用getinstance的時候就有可能出現如下的情況:
時間
線程a
線程b
t1
a1:配置設定對象的記憶體空間
t2
a3:設定instance指向記憶體空間
t3
b1:判斷instance是否為空
t4
b2:由于instance不為null,線程b将通路instance引用的對象
t5
a2:初始化對象
t6
a4:通路instance引用的對象
這樣就傳回了一個未初始化的對象,這樣就出現了問題。解決的辦法可以這樣:
惡漢模式, 壞處就是在不需要的時候也會建立執行個體
這種辦法允許重排序但是重排序對b線程不可見
還可以這樣:
這種辦法沒有加鎖,但是利用了jvm靜态方法的特性,保證其是線程安全的,也是允許重排序但是重排序對b線程不可見。
下面還有一種不允許重排序,就是利用volatile關鍵字:
volatile關鍵字可以讓jvm不進行重排序。就不會出現上訴的問題了。
總結下volatile關鍵字的用法:
其對于線程安全方面的控制很少,一般僅僅用來保證一個對象狀态變化的可見性, 比如使用一個屬性進行關鍵判斷的時候,這個屬性就應該使用volatile進行修飾,保證其他線程對該屬性的修改能夠及時可見。
原子操作表示對于兩個操作a,b,對于a來講b要麼都執行了,要麼都沒執行。那麼b對于a來講就是原子操作。
原子操作是線程安全的,排除使用synchronized來使得一個操作是原子操作外,java本身很多語句可以認為是原子的,比如volatile int i=1.
複合操作由多個操作構成,因為多個操作執行的時候會有時間差,這個時候就會産生線程安全的問題。 除了很明顯的多個語句外,java很多單條語句可能會變為多個操作,就是上面說的這幾種情況了:
1. i=1的時候可以分解為 從主存read i 到寄存器,修改i, 回寫主存這三步。
3. long等64的運算,可能分解為多個運算。
對于這三個操作,使用volatile就可以是他們變成原子操作。
但是對于多語句的情況就隻能使用鎖的機制來進行同步了。 這個後面再說。