Java中有一些或常用蚯瞧,或不常用锅睛,但卻不得不知關(guān)鍵字煌茴,本篇文章將討論這些關(guān)鍵字的作用忽舟。
transient
transient關(guān)鍵字可能用的不是那么頻繁双妨,但卻是一個(gè)很重要的關(guān)鍵字,它的作用是在對(duì)象序列化過程中體現(xiàn)的叮阅。如果一個(gè)類的變量被transient修飾刁品,那么這個(gè)對(duì)象在序列化過程中,不會(huì)序列化這個(gè)變量浩姥,同時(shí)哑诊,在反序列化過程中,也不會(huì)去反序列這個(gè)變量及刻。
筆者有時(shí)候會(huì)遇到這樣一種情況镀裤,可能是因?yàn)楣P者經(jīng)驗(yàn)不足竞阐。在使用JPA進(jìn)行數(shù)據(jù)庫操作的時(shí)候,某些對(duì)象在序列化到數(shù)據(jù)庫的時(shí)候暑劝,會(huì)有一些變量并不想要去存到數(shù)據(jù)庫中骆莹,這個(gè)時(shí)候就可以用transient來修飾變量,解決這個(gè)問題担猛。
instanceof
這個(gè)關(guān)鍵字就比較常用了幕垦,尤其是在一些大量采用反射的框架中,在需要判斷某一個(gè)對(duì)象是否是某一類型(可以是接口傅联,父類先改,父類的父類等)的時(shí)候,可以采用這個(gè)關(guān)鍵字蒸走。比如說仇奶,判斷UserServiceImpl是否是UserService接口的類型,可以這樣做:
if(userServiceImpl instanceof UserService){
//do some thing
}
筆者在自己的IOC框架中就用到了這個(gè)關(guān)鍵字比驻,在將進(jìn)行接口依賴注入的時(shí)候该溯,使用該關(guān)鍵字判斷容器中是否有相應(yīng)接口的實(shí)例,然后將實(shí)例注入别惦。
final
final關(guān)鍵字可以用來修飾變量狈茉、類、方法掸掸。
final修飾的變量氯庆,一旦在被賦值之后,將不能再次賦值扰付,也就是說這個(gè)變量在后續(xù)的使用過程中堤撵,只能采取讀的方式,變量具有不可變性悯周。這里需要做一下區(qū)別粒督,就是基本數(shù)據(jù)類型和引用數(shù)據(jù)類型在被final修飾后的情況∏菀恚基本數(shù)據(jù)類型的不可變性往往體現(xiàn)在變量的值永遠(yuǎn)不會(huì)再變化屠橄,而引用類型則不是,引用類型的不變性是體現(xiàn)在引用的不變性闰挡。引用類型的變量一旦為一個(gè)引用數(shù)據(jù)類型賦值锐墙,那么變量就會(huì)指向?qū)ο笤趦?nèi)存中的地址,final關(guān)鍵字修飾過后长酗,變量所指向的地址就不能再被改變溪北,但對(duì)象本身的狀態(tài)還是可以改變的,可以看一下下面這段代碼:
final int i = 0;
i = 1; //error
final int[] j = new int[10];
j = new int[20]; //error
j[0] = 1; //right
final修飾過的變量有一個(gè)好處,即它會(huì)是線程安全的之拨。在Java中茉继,final變量會(huì)進(jìn)行指令重排序,確保所有線程在訪問該變量的時(shí)候蚀乔,變量已經(jīng)被初始化過了烁竭。雖然在舊的版本中,會(huì)出現(xiàn)對(duì)象引用在構(gòu)造函數(shù)中“逸出”的情況吉挣,但自從jsr133增強(qiáng)了final的內(nèi)存語義之后派撕,所有線程在看到final變量時(shí),看到的都是已經(jīng)初始化之后的值睬魂。final變量初始化之后又不會(huì)再改變终吼,所以它是線程安全的。
final修飾的類氯哮,將具有不可繼承性际跪,即不能有子類。
final修飾的方法蛙粘,將不可被重寫垫卤,但可以重載威彰。
static
static關(guān)鍵字可以用來修飾變量出牧,方法。static修飾的變量和方法歇盼,將只屬于類舔痕,可以通過類名.變量名(或方法名)的方式來引用,使用方式如下:
public class Demo{
public static int i = 0;
public static void hello(){
System.out.println(i);
}
}
//可以這么訪問
int j = Demo.i;
Demo.hello();
其中static變量將會(huì)常駐內(nèi)存中豹缀,不會(huì)在垃圾回收的過程中被回收掉伯复,甚至?xí)蔀槔厥罩械腉C Root。static變量的初始化只能在static代碼塊中執(zhí)行邢笙,它先于構(gòu)造函數(shù)執(zhí)行啸如,使用方式如下:
public class Demo{
public static int i;
static{
i=0;
}
}
volatile
這個(gè)關(guān)鍵字是一個(gè)很重要的關(guān)鍵字,可以修飾變量氮惯。如果要進(jìn)行多線程編程叮雳,那么這個(gè)關(guān)鍵字將會(huì)是一個(gè)重點(diǎn)。因?yàn)樗袃蓚€(gè)特性:可見性和原子性妇汗。
可見性是指帘不,對(duì)一個(gè)volatile變量的讀,總是能看到(任意線程)對(duì)這個(gè)volatile變量最后的寫入杨箭。volatile變量再被修改的時(shí)候寞焙,如果有其他線程讀取了該變量,它會(huì)通知其他線程變量已經(jīng)實(shí)現(xiàn),重新讀取最新的值捣郊。
原子性是指辽狈,對(duì)任意單個(gè)volatile變量的讀/寫具有原子性,但類似于volatile++這種符合操作不具有原子性呛牲。其原理是因?yàn)関olatile具有一定程度上的鎖的語義稻艰,但并沒有像synchronized那么重量級(jí)。
它的使用方式如下侈净,以最常見的單例模式為示例尊勿。一般來說,我們比較喜歡使用double-check的方式實(shí)現(xiàn)單例模式畜侦,因?yàn)樗瓤梢宰鲅舆t初始化又可以保證線程的安全元扔,代碼如下:
public class Demo{
private static Demo instance;
private Demo(){
}
public static Demo getInstance(){
if(instance == null){ //第一步,判斷instance是否為null
synchronized(Demo.class){ //第二步旋膳,加鎖
if(instance == null){ //第三步澎语,再判斷instance是否為null
instance = new Demo(); //第四步,實(shí)例化
}
}
}
return instance;
}
}
這段代碼看似線程安全验懊,但卻存在一個(gè)很大的缺陷擅羞。如果有兩個(gè)線程,兩個(gè)線程都執(zhí)行到了第二步义图,一個(gè)線程在拿到鎖并進(jìn)行實(shí)例化之后减俏,另一個(gè)線程繼續(xù)進(jìn)入同步代碼塊,這個(gè)線程讀到的instance可能還是null碱工,之后就會(huì)導(dǎo)致線程安全問題娃承。為什么會(huì)這樣呢?其實(shí)在Java中怕篷,對(duì)象的實(shí)例化可以分為以下三個(gè)子階段:
memory = allocate(); // 1:分配對(duì)象的內(nèi)存空間
ctorInstance(memory); // 2:初始化對(duì)象
instance = memory; // 3:設(shè)置instance指向剛分配的內(nèi)存地址
由于Java會(huì)對(duì)指令進(jìn)行重排序历筝,可能會(huì)導(dǎo)致3和2的指令順序相反,即
memory = allocate(); // 1:分配對(duì)象的內(nèi)存空間
instance = memory; // 3:設(shè)置instance指向剛分配的內(nèi)存地址
ctorInstance(memory); // 2:初始化對(duì)象
問題就出現(xiàn)在這里廊谓,如果線程一的還沒有執(zhí)行完初始化對(duì)象這個(gè)子階段梳猪,另一個(gè)線程將會(huì)認(rèn)為這個(gè)實(shí)例為空,將會(huì)導(dǎo)致線程安全蒸痹。要解決這個(gè)問題春弥,可以加上volatile關(guān)鍵字,代碼如下:
public class Demo{
private volatile static Demo instance;
private Demo(){
}
public static Demo getInstance(){
if(instance == null){ //第一步电抚,判斷instance是否為null
synchronized(Demo.class){ //第二步惕稻,加鎖
if(instance == null){ //第三步,再判斷instance是否為null
instance = new Demo(); //第四步蝙叛,實(shí)例化
}
}
}
return instance;
}
}
volatile會(huì)禁止指令重排序俺祠,使得這個(gè)操作變得具有原子性,這樣線程就可以讀取到變量最新的狀態(tài),保證了線程的安全蜘渣。
synchronized
synchronized可以說是一個(gè)非常常見的關(guān)鍵字了淌铐,在jdk1.6之前,它被稱為重量級(jí)鎖蔫缸,不過jdk1.6之后經(jīng)過優(yōu)化和升級(jí)腿准,已經(jīng)沒有那么夸張了。通過使用synchronized可以使代碼同步拾碌,解決多線程環(huán)境下的一些問題吐葱。一般來說,它可以使用下面三種形式的鎖:
- 對(duì)于普通同步方法校翔,鎖是當(dāng)前實(shí)例對(duì)象弟跑。
- 對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類的Class對(duì)象防症。
- 對(duì)于同步方法塊孟辑,鎖是synchronized括號(hào)里配置的對(duì)象。
當(dāng)一個(gè)線程試圖去訪問同步代碼塊時(shí)蔫敲,它必須先得到鎖饲嗽,退出或拋出異常時(shí)必須釋放鎖。其底層原理是通過Monitor對(duì)象來實(shí)現(xiàn)方法同步和代碼塊同步奈嘿。代碼塊同步是使用monitorenter和monitorexit指令實(shí)現(xiàn)的貌虾,方法同步是使用另一種方法實(shí)現(xiàn)的。
monitorenter方法是在編譯后插入到同步代碼塊的開始位置指么,而monitorexit是插入到方法結(jié)束處和異常處酝惧,JVM要保證每個(gè)monitorenter都有一個(gè)monitorexit與之配對(duì)榴鼎。任何對(duì)象都有一個(gè)monitor與之關(guān)聯(lián)伯诬,當(dāng)且一個(gè)monitor被持有后,它將處于鎖定狀態(tài)巫财。線程執(zhí)行到monitorenter指令時(shí)盗似,將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的monitor所有權(quán),即嘗試獲得對(duì)象的鎖平项。