在文章之前,首先要理解一個jvm運行時內(nèi)存分配機制以及一些定義。
(一)定義:
** 原子性**:
對于一個操作或者多個操作,要不全部執(zhí)行且在執(zhí)行過程中不可以被中斷,要不直接不執(zhí)行,這種行為成為原子性。
x = 10; //能保證原子性,10直接寫入x
y = x; //不能保證原子性,x讀取出來,寫入y
x++; //不能保證原子性,x讀取出來,自增后,寫入x
x = x + 1; //不能保證原子性,同上
** 可見性**:
指的是當存在多個線程訪問一個變量的時候,如果其中一個線程改變這個變量值,對于其他線程是可見的,即其他線程可以立即看到這個改變的變量值。
有序性:
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行
(二)運行時內(nèi)存分配機制
在其中一個內(nèi)存區(qū)域中有一塊叫jvm虛擬機棧,每個線程都獨立擁有一個線程棧。這里先說兩個定義名詞,但某一對象變量值存儲在某一內(nèi)存空間中,我們這里叫做“主內(nèi)存”,當某一線程訪問該對象變量值,用過對象引用找到該變量值的內(nèi)存空間,每個線程會先復制這個“主內(nèi)存”的變量到自己的線程棧中,等待該線程棧的值操作完成后再更新到主內(nèi)存中,貼下別人整理的圖。
所以,這里會衍生出一個問題。如果有多個線程讀取這個“主內(nèi)存”的變量,由于每次都是單獨的線程拷貝這個“主內(nèi)存”的變量到自己的線程棧,處理完成后才會更新到“主內(nèi)存”中。所以并發(fā)的情況下,不能保證當前拷貝的值是同步進行的。
volatile
volatile可以解決在線程并發(fā)時候的可見性,即在某一線程棧改變一個主內(nèi)存變量的時候,其他線程棧可以馬上看到這個變化。但是volatile并不能完全保證原子性。下面再舉例說明下為什么不能完全保證原子性。
volatile 的可見性
首先貼下代碼,看下一則比較常見的例子
/**
* 測試Volatile的可見性demo
* @author siven
*
*/
public class VolatileVisible{
private static int siven = 1;
public static int getSiven(){
return siven;
}
public static void work(){
siven = 2;
}
}
public static void main(String[] args) {
volatileVisibleAction();
}
private static void volatileVisibleAction(){
new Thread(new Runnable() {
public void run() {
VolatileVisible.work();
}
}).start();
new Thread(new Runnable() {
public void run() {
System.out.println("siven : " + VolatileVisible.getSiven());
}
}).start();
}
從實際想要的效果,第一個線程將變量siven更新為2,第二個線程再讀取siven變量并且打印出來。這樣的邏輯是沒有什么問題。但是有一定的可能性存在并發(fā)問題。線程1改變siven變量值的時候,還沒來得及更新到主內(nèi)存,就被線程2
讀取,所以這里有可能讀到的是1。讀者可以多運行幾次,幾率是比較小,截圖如下。
因此,假如某一線程棧改變了值,其他線程棧可以立即更新的話就可以避免這種問題。
修改成以下代碼:
private volatile static int siven = 1;
所以有volatile的存在,保證線程之間的可見性,當然只能處理多線程并發(fā)讀取問題,為什么這么說呢?看看下面代碼:
VolatileVisible中添加work2方法
public static void work2(){
siven ++;
}
測試方法
private static void threadAction(){
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
public void run() {
VolatileVisible.work2();
}
}).start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println("siven : " + VolatileVisible.getSiven());
}
從實際效果中,一萬個線程執(zhí)行完成后,siven會自加一萬次,所以最終想要的結(jié)果應該回事10001,但是經(jīng)過運行觀察,很難可以準確得到10001這個結(jié)果,如圖所示:
這里讀者會質(zhì)疑,siven已經(jīng)用volatile修飾符修飾了,siven變量改變應該對于所有線程棧是可見才對啊。首先這里我們應該了解siven++;自加操作,其實這個操作并不是原子操作,這里面包括內(nèi)存中取出siven,然后自加1,接著再寫入siven,所以在三個操作中有可能是會被中斷的。雖然用了volatile修飾保證線程棧中的可見性,但是只限制于讀取的時候可見性,也只有線程棧寫入完成之后才會立即更新到主內(nèi)存并且其他線程棧會馬上知道
舉個例子:A線程、B線程都共同訪問siven變量,這時候用得是volatile修飾,因此siven變量對于A、B線程都是可見的。當A對siven變量進行非原子操作(例如自加到2)是有可能出現(xiàn)還沒執(zhí)行到最后,B線程已經(jīng)讀取了舊值1,并且自己也自加到2(原來實際效果是想拿到A線程自加后的結(jié)果,在自加成3)。因此volatile只是保證線程之間的可見性,但是不能保證線程中操作的原子性。所以也引出了synchronized
synchronized
首先在理解synchronized中,要了解對象鎖和類鎖兩個定義。每個java對象都可以用做一個實現(xiàn)同步的鎖,這些鎖成為內(nèi)置鎖。其實對象鎖與類鎖的的存在價值與內(nèi)置鎖一致。只是對象鎖與類鎖的應用場景不一樣,即類鎖應用在靜態(tài)方法,對象鎖應用在實例方法。我們都知道,每個類都存在多個實例化對象,但是每個類只有一個class對象,所以實例化對象中的對象鎖并不會互相干預。
首先貼下代碼:
/**
* Synchronized 測試方法
* @author siven
*/
public class SynchronizedWorker {
public static void work(String tag){
for (int i = 0; i < 5; i++) {
System.out.println(tag + " work : " + i);
}
}
}
private static void synchronizedTest(){
Thread threadA = new Thread(new Runnable() {
public void run() {
SynchronizedWorker.work("A");
}
});
Thread threadB = new Thread(new Runnable() {
public void run() {
SynchronizedWorker.work("B");
}
});
threadA.start();
threadB.start();
}
輸出結(jié)果:
首先這里只是輸出了log語句,如果當前不是輸出而是改變某一對象里面的變量的時候,很容易出現(xiàn)因為線程并發(fā)問題導致對象成員變量被改變或者被重新實例化,特別是android中的回調(diào),很多時候是發(fā)生在多線程的,所以很容易發(fā)生這種并發(fā)問題。如果我們要實際的同步效果,我們可以直接使用synchronized對方法或者代碼塊進行加鎖。例如下面的優(yōu)化改造:
(一)直接修飾方法
public synchronized static void work(String tag){
for (int i = 0; i < 5; i++) {
System.out.println(tag + " work : " + i);
}
}
(二)修飾代碼塊
public static void work(String tag){
synchronized (SynchronizedWorker.class) {
for (int i = 0; i < 5; i++) {
System.out.println(tag + " work : " + i);
}
}
}
在synchronized 修飾的方法或者代碼塊中,會將這個區(qū)域進行加鎖,當?shù)谝粋€線程進行訪問的時候,該線程會獲取到該鎖,如果第二個線程對這個區(qū)域進行訪問的時候,如果有其他線程占有的鎖還沒釋放的時候,將會暫時性阻塞,等待其他線程鎖釋放后自己獲取后才可以進行訪問。
當然對于前面volatile可以解決可見性,但是不能完全保證原子性的代碼案例中也可以通過synchronized 解決,我們只需要該成以下代碼即可:
public static void work2(){
synchronized (VolatileVisible.class) {
siven ++;
}
}
輸出結(jié)果:
注意問題:
前面也說每個類有多個實例化對象,說明有多個對象鎖。如果是synchronized (this)進行代碼塊加鎖,此時只能對當前對象方法進行加鎖,與其他對象鎖沒有任何干預。當然如果是類鎖,針對靜態(tài)方法的,我們沒辦法通過synchronized (this)進行代碼塊加鎖,可以使用synchronized (**.class)進行加鎖
by siven(qq:708854877 email:sy.wu@foxmail.com)
2017.5.26