本文主要記錄自己閱讀《Java并發(fā)編程實(shí)戰(zhàn)》后,對(duì)并發(fā)編碼的淺薄認(rèn)識(shí),為原創(chuàng)內(nèi)容,如有文中有書寫或其他問題,請(qǐng)留言指導(dǎo)修正,互相交流,共同進(jìn)步,本人QQ:417213902。
常見的并發(fā)概念
-
原子性
符合原子操作的那么就說具有原子性,那么原子操作指不會(huì)被線程調(diào)度 機(jī)制打斷的操作;這種操作一旦開始,就一直運(yùn)行到結(jié)束,中間不會(huì)有 任何上下文切換。比如我們常見的++a,它的操作是原子的,因?yàn)樗⒉粫?huì)作為一個(gè)不可
分割的操作來執(zhí)行,這是一個(gè)“讀取-修改-寫入”的操作序列,并且其結(jié)果
狀態(tài)依賴于之前的狀態(tài)。
我們可以通過采用java.util.concurrent.atomic包中包含的原子變量類,用于實(shí)現(xiàn)在數(shù)值和對(duì)象引用上的原子狀態(tài)轉(zhuǎn)換。原子變量類采用了CAS(compare and swap)無鎖算法,屬于樂觀鎖。CAS的原理是有3個(gè)操作數(shù),內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的新值B,當(dāng)且僅當(dāng)A和V相等時(shí),將V改為B,否則什么也不做。
還有一種方式可以采用加鎖機(jī)制,即內(nèi)置鎖及重入機(jī)制,用關(guān)鍵字synchronized同步代碼塊能達(dá)到原子操作。 -
競(jìng)態(tài)條件
在并發(fā)編程中,當(dāng)某個(gè)計(jì)算的正確性取決于多個(gè)線程的交替執(zhí)行時(shí)序 時(shí),那么就會(huì)發(fā)生競(jìng)態(tài)條件。比如說“先檢查后執(zhí)行”操作,即可能通過一個(gè)可能失效的觀測(cè)結(jié)果來決定
下一步的動(dòng)作。常見的有單例模式。那么此時(shí)我們采用可以原子操作解決此問題,即保證在單例模式中初始化變量的方法增加鎖synchronized,或者定義原子性變量。 -
指令重排序
在沒有充分同步的程序中,如果調(diào)度器采用不恰當(dāng)?shù)姆绞絹斫惶鎴?zhí)行 不同線程的操作,那么將導(dǎo)致不正確的結(jié)果,更糟的是,JVM還使得不 同線程看到的操作執(zhí)行順序是不同,從而導(dǎo)致在缺乏同步的情況下,要 推斷操作的執(zhí)行順序?qū)⒆兊酶訌?fù)雜,此些都可以歸為重排序。在編譯器中生成的指令順序,可以與源代碼中的順序不同,此外編譯器還會(huì)把變量保存在寄存器而不是內(nèi)存中;處理器可以采用亂序或并行等方式執(zhí)行指令;緩存可能會(huì)改變將寫入變量提交到主內(nèi)存的次序;而且,保存在處理器本地緩存中值,對(duì)于其他處理器是不可見的。當(dāng)然對(duì)于亂序,我覺得應(yīng)該是沒有依賴或者關(guān)聯(lián)的指令可以亂序。
-
可見性
當(dāng)多個(gè)線程同時(shí)訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其 他線程能夠立即看得到修改的值。首先我們得要理解下,在實(shí)際內(nèi)存中,每個(gè)線程內(nèi)都會(huì)有個(gè)工作內(nèi)存,僅每個(gè)線程自己可見,且還有共享內(nèi)存,工作內(nèi)存中的值由共享內(nèi)存復(fù)制過來;當(dāng)把變量聲明為volatile類型后,線程每次操作都是修改主內(nèi)存中的變量,讀取volatile類型變量時(shí)也總是返回最新寫入的值。這就說明volatile能解決 線程對(duì)變量的可見性,注意volatile不可以對(duì)非原子操作的變量具有可見性。加鎖機(jī)制既可以確??梢娦杂挚梢源_保原子性,而volatile變量只能確??梢娦?。synchronized同樣可以保證可見性。
-
不可變
如果某個(gè)對(duì)象在被創(chuàng)建后其狀態(tài)就不能被修改,那么這個(gè)對(duì)象就稱為不可變對(duì)象。主要關(guān)鍵字為final。
內(nèi)置鎖及重入性
java本身提供了同步代碼塊,由關(guān)鍵字synchronized實(shí)現(xiàn),每個(gè)java對(duì)象都可以用做一個(gè)實(shí)現(xiàn)同步的鎖,這些鎖我們稱為內(nèi)置鎖。java的內(nèi)置鎖相當(dāng)于一個(gè)互斥體,這意味著最多只有一個(gè)線程能持有這種鎖,當(dāng)線程A嘗試獲取由線程B持有的鎖時(shí),線程A必須等待或阻塞。
在java內(nèi)部,同一線程在調(diào)用自己類中其他synchronized方法/塊或調(diào)用父類的synchronized方法/塊都不會(huì)阻礙該線程的執(zhí)行,就是說同一線程對(duì)同一個(gè)對(duì)象鎖是可重入的,而且同一個(gè)線程可以獲取同一把鎖多次,也就是可以多次重入。重入意味著獲取鎖的操作的粒度是“線程”,而不是“調(diào)用”。重入的一種實(shí)現(xiàn)方法是,為每個(gè)鎖關(guān)聯(lián)一個(gè)獲取計(jì)數(shù)值和一個(gè)所有者線程,當(dāng)計(jì)數(shù)值為0時(shí),這個(gè)鎖就被認(rèn)為是沒有被任何線程持有,當(dāng)線程請(qǐng)求一個(gè)未被持有的鎖時(shí),JVM將記下鎖的持有者,并且將獲取計(jì)數(shù)值置為1。如果同一個(gè)線程再次獲取這個(gè)鎖,計(jì)數(shù)值將遞增,而當(dāng)線程退出 同步代碼塊時(shí),計(jì)數(shù)器會(huì)相應(yīng)地遞減,當(dāng)計(jì)數(shù)值為0時(shí),這個(gè)鎖將被釋放。并發(fā)中常見關(guān)鍵字synchronized、volatile、final
synchronized :
修飾一個(gè)方法,被修飾的方法稱為同步方法,其作用的范圍是整個(gè)方法,作用的對(duì)象是調(diào)用這個(gè)方法的對(duì)象;
synchronized(this)同步代碼塊時(shí),代碼塊為原子操作,多線程將被阻塞。
synchronized(某個(gè)對(duì)象)同步代碼塊時(shí),這個(gè)對(duì)象將只能被擁有鎖的線程修改
volatile:
volatile是Java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制。關(guān)于volatile的可見性作用,我們必須意識(shí)到被volatile修飾的變量對(duì)所有線程總是立即可見的,對(duì)volatile變量的所有寫操作總是能立刻反應(yīng)到其他線程中,但是對(duì)于volatile變量運(yùn)算操作(如a++)在多線程環(huán)境并不保證安全性,這是因?yàn)閍++操作是非原子操作,可以將該方法用synchronized修飾。
/**
* Created by zejian on 2017/6/11.
* Blog : http://blog.csdn.net/javazejian [原文地址,請(qǐng)尊重原創(chuàng)]
*/
public class DoubleCheckLock {
private static DoubleCheckLock instance;
private DoubleCheckLock(){}
public static DoubleCheckLock getInstance(){
//第一次檢測(cè)
if (instance==null){
//同步
synchronized (DoubleCheckLock.class){
if (instance == null){
//多線程環(huán)境下可能會(huì)出現(xiàn)問題的地方
instance = new DoubleCheckLock();
}
}
}
return instance;
}
}
上述代碼一個(gè)經(jīng)典的單例的雙重檢測(cè)的代碼,這段代碼在單線程環(huán)境下并沒有什么問題,但如果在多線程環(huán)境下就可以出現(xiàn)線程安全問題。原因在于某一個(gè)線程執(zhí)行到第一次檢測(cè),讀取到的instance不為null時(shí),instance的引用對(duì)象可能沒有完成初始化。因?yàn)閕nstance = new DoubleCheckLock();可以分為以下3步完成(偽代碼)
memory = allocate(); //1.分配對(duì)象內(nèi)存空間
instance(memory); //2.初始化對(duì)象
instance = memory; //3.設(shè)置instance指向剛分配的內(nèi)存地址,此時(shí)instance!=null
由于步驟1和步驟2間可能會(huì)重排序,如下:
memory = allocate(); //1.分配對(duì)象內(nèi)存空間
instance = memory; //3.設(shè)置instance指向剛分配的內(nèi)存地址,此時(shí)instance!=null,但是對(duì)象還沒有初始化完成!
instance(memory); //2.初始化對(duì)象
由于步驟2和步驟3不存在數(shù)據(jù)依賴關(guān)系,而且無論重排前還是重排后程序的執(zhí)行結(jié)果在單線程中并沒有改變,因此這種重排優(yōu)化是允許的。但是指令重排只會(huì)保證串行語義的執(zhí)行的一致性(單線程),但并不會(huì)關(guān)心多線程間的語義一致性。所以當(dāng)一條線程訪問instance不為null時(shí),由于instance實(shí)例未必已初始化完成,也就造成了線程安全問題。那么該如何解決呢,很簡(jiǎn)單,我們使用volatile禁止instance變量被執(zhí)行指令重排優(yōu)化即可。
final:
final修飾的對(duì)象可以在定義時(shí)或構(gòu)造器中初始化
static final修飾的對(duì)象表示常量,只有在定義時(shí)賦值
除非需要某個(gè)域是可變的,否則應(yīng)將其聲明為final域,也是一個(gè)良好的編程習(xí)慣。摘自《Java并發(fā)編程實(shí)戰(zhàn)》
2018-05-08 23:31:00