最近去平安系面試時,遇到了個人技術領域認定的一大偶像吳大師(Cat作者),他隨口問了個單例的問題,要求基於Java技術棧,給出幾種單例的方案,並給出單元測試代碼,最后要求談談單例模式最需要注意的問題時什么?我想想挺簡單的,就是一個餓漢,一個懶漢模式,單元測試就一個判斷NULL和2個Instance的比較就好。結果被大師劈頭蓋臉一頓數落,比如我寫的懶漢單例(雙鎖),為什么使用volatile?還有別的更好的方式么?單元測試你不起多個線程,簡單的比較有任何意義么?最后被定位為寫代碼不懂腦筋,僅僅就是照抄別人的成熟方案,缺少對問題關鍵的把握,無法進行變通。
說實話當時還有些抵觸情緒,即使是被自己的偶像批評,不過回家之后好好回顧了相關的問題,發現自己對Java技術棧的理解仍然很淺薄,痛定思痛,決定好好來學習下單例模式和內部類,本文就是基於這兩個問題總結,此外,祝大家新的一周工作愉快。
單例模式
在詳細介紹單例模式前,首先來談談單例模式的目的和問題,尤其是問題部分,我們常常容易忽視(感謝身邊同事的提醒)。單例模式的目的非常明確,就是在當前應用中只保存指定對象的一個實例,主要目的是減少資源的消耗,各種提供服務的類會選用該模式。單例模式主要有資源消耗的取舍,線程安全和如何防止反射或序列化破壞單例等三個問題。主要單例的實現模式包括最簡單有效的餓漢式、最多變的懶漢式、最優的靜態內部類方式、奇特的枚舉類方式和綜合的登記式模式,本文主要介紹前3種,也是個人認為比較最有價值的3種。
餓漢式
對於一般的業務開發來說餓漢式已經足夠,而且Spring框架的單例默認就是餓漢模式,絕大部分的提供各類服務的類都不是很占有內存空間,以此在項目啟動時進行預加載對於系統影響不大,即使始終不被使用也沒有太大的關系,其代碼如下(解決了反射和序列化破壞單例的問題)。
public class HungrySingleton implements Serializable
{
private HungrySingleton(){
if(null != instance){//防止反射破壞單例,場景為二方庫調用者強行破壞單例
throw new IllegalOperationException();
}
}
public static final HungrySingleton instance = new HungrySingleton();
public static HungrySingleton getInstance(){
return instance;
}
// 防止反序列化獲取多個對象,Java這兒比較奇特,因為在Serializable
private Object readResolve() throws ObjectStreamException {
return instance;
}
}
public class HungrySingletonTest {
@Test
public void testGetInstance() throws Exception {
Class<HungrySingleton> clazz = HungrySingleton.class;
Constructor<?> ctor = clazz.getDeclaredConstructor(null);
ctor.setAccessible(true);
HungrySingleton first = HungrySingleton.getInstance();
HungrySingleton second = (HungrySingleton)ctor.newInstance(null);//破壞單例失敗
}
}
懶漢式
懶漢式單例是考點最多的一個,雖然現實中使用次數不是很多,但掌握它有利於了解Java並發編程,常見的實現方式包括加同步鎖的懶漢式和防止指令重排優化的懶漢式。
//加同步鎖的懶漢式,比較簡單,但每次獲取實例時都需要獲得鎖,對性能有一定影響
public static synchronized LazySingleton01 getInstance(){
if(instance == null){
instance = new LazySingleton01();
}
return instance;
}
//防止指令重排優化的懶漢式
//對常見的雙鎖進行了優化,對instance使用volatile修飾
//再JAVA中,同步塊外的判空操作有可能看到已存在,但不完整的實例.
//如果使用不完整的實例則會造成系統崩潰,造成該問題的原因是由於Java內存模型的重排序機制。
public static volatile LazySingleton02 instance = null;
public static LazySingleton02 getInstance(){
if(null == instance){
synchronized (LazySingleton02.class) {
if(null == instance){
instance = new LazySingleton02();
}
}
}
return instance;
}
//單測時一定要注意,需要使用多個線程進行測試,不然就失去了意義
public class LazySingleton02Test {
@Test
public void getInstance() throws InterruptedException, ExecutionException{
ExecutorService threadPool = Executors.newFixedThreadPool(2);
Future<LazySingleton02> futureA = threadPool.submit(
new Callable<LazySingleton02>() {
@Override
public LazySingleton02 call() {
return LazySingleton02.getInstance();
}
}
);
Future<LazySingleton02> futureB = threadPool.submit(
new Callable<LazySingleton02>() {
@Override
public LazySingleton02 call() {
return LazySingleton02.getInstance();
}
}
);
Assert.assertNotNull(futureA.get());
Assert.assertNotNull(futureB.get());
Assert.assertTrue(futureA.get().equals(futureB.get()));
}
}
靜態內部類方式
靜態內部類這種方式是個人最不熟悉的,之前又一次面試中還被問過一個如何擴充類的問題,即Java中不支持多繼承,如果想要復用多個類的屬性如何做到?相對於將屬性提取到接口中或通過自合模式復用,內部類的方式會更加優雅。對於單例同樣可以借助內部類的特性優雅的處理,代碼如下所示,注意看關於內部類加載的注釋。
public class LazySingleton03 {
private LazySingleton03(){}
private static class LazyHolder{
private static final LazySingleton03 INSTANCE = new LazySingleton03();
}
//借助了靜態內部類的特性,其要被引用后才會裝載到內存
//通常的理解是,只要是當前jar中的靜態屬性或方法都會被加載到內存,但靜態內部類卻不是,它只有在第一次調用getInstance方法,產生了LazyHolder的引用,才會被真正加載。
//實際上也是懶加載。
public static final LazySingleton03 getInstance(){
return LazyHolder.INSTANCE;
}
}
此外,枚舉類型的單例借助其特性默認就是線程安全和防止反射破壞等行為,但並不是很適合大范圍的使用。而登記器的方式實際上就是做一個Mapper其中存放所有的單例對象,需要時去獲取即可,因此最推薦的仍然是內部類方式。
Tip
重排序
對於instance = new LazySingleton02()
,其實際執行情況偽代碼可能如下,可以看到Java內存模型分配內存並創建對象的方式和我們預想的不太一樣,這部分會在Java並發編程系列文章中繼續加強學習。
memory = allocate();//分配內存空間,C語言中應該很熟悉
instance = memory;//這時instance會變成非空,但還未初始化
ctor(instance);//初始化對象
synchronized
關鍵字
通常基於不同的維度有如下幾種用法,鎖定范圍越來越小,盡可能選擇更小粒度的鎖定范圍可以獲得更好的性能。
給類加鎖:synchronized(XXXX.class)
給對象加鎖:synchronized(this)
, public synchronized void test(){}
給代碼塊加鎖:synchronized(lock){ ... }
Tip
單例模式的類和提供方法的靜態類有什么區別?提供方法的靜態類不是面向對象的思想的產物,相應的其沒有封裝、繼承、多態等特性,簡單來說你無法對提供方法的靜態類進行擴展。
內部類
之前通過靜態內部類方式實現單例引入了內部類這一重要概念,接下來將詳細介紹內部類的相關概念和用法。如果在一個類的內部定義一個新的類型,就將這個新的類型稱為內部類,其名稱無需和文件名一致。特別的,內部類是一個編譯時概念,一旦編譯成功,就會成為完全不同的兩個類,比如Outer.class
和Outer$Inner.class
。通常來說,內部類包括靜態內部類、匿名內部類、成員內部類和局部內部類,重要性依次遞減。
靜態內部類與成員內部類
個人認為靜態內部類最重要的一種內部類,比如常見的ReentrantLock
中的Sync
,NonfairSync
、FairSync
等一系列靜態內部類。其通過static
修飾,可以包含static數據和屬性,且其無需創建外部類和內部類即可被使用。
成員內部類是最基本的一種內部類類型,其可以訪問外部類的所有成員和方法,但不能含有static的變量和方法,因為成員內部類需要先創建外部類,之后才能創建自己,特別的,其可以通過外部類.this.屬性
的方式訪問外部類同名屬性,示例如下所示。
public class InnerClass {
private static String staticName = "xionger";
private String objectName = "xiongda";
public static final class InnerClass01 {//靜態內部類
private statißßc String name = InnerClassInterface.class.getName();
public static String getName() {
return InnerClass.staticName + "--" + name;
}
}
public class InnerClass02 {//成員內部類
public String getName() {
return InnerClass.this.objectName;//注意這兒獲取外部類屬性的形式
}
}
}
匿名內部類
匿名內部類經常會被使用,比如使用線程、事件等場景,示例代碼如下所示。
public class AnonymousInnerClass {
public void createThread(){
Thread thd = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(AnonymousInnerClass.class.getName());
}
});
}
//注意方法參數上的final關鍵字,由於內部類編譯時生成單獨的.class文件,內部類與外部類不在同一文件。
//內部類是通過將傳入的參數先通過構造器復制到自己內部再被使用的,因此為了保持數據的一致需要添加final。
public InnerClassInterface getInnerClass(final String prefixName){
return new InnerClassInterface(){
@Override
public String getName() {
return prefixName + "--suffixName";
}
};
}
}
interface InnerClassInterface {
String getName();
}
局部內部類的使用場景實在太少就不做介紹了。
類加載
類加載這部分知識可以通過一個簡單的問題給串起來,“用什么工具?在什么時機?通過什么樣的步驟?加載類”,接下來就是解決這3個問題的方法介紹。
1.用什么工具?類加載器
類加載器包括以下4種,加載順序按照序號從小到大。
a.Bootstrap ClassLoader啟動類加載器,負責加載jre/lib/rt.jar
。
b.Extension ClassLoader擴展類加載器,負責加載擴展功能Jar包,包括jre/lib/*.jar
或ext目錄
下的jar包。
c.App ClassLoader應用類加載器,負責加載classpath中指定的jar包.
d.Custom ClassLoader自定義類加載器,如tomcat根據j2ee規范自行實現ClassLoader。
2.在什么時機?類加載的時機
a.創建類的實例,new對象或者反射創建對象。
b.訪問類或接口的靜態變量時或靜態方法時。
c.初始化一個類的子類時會先初始化父類。
d.JVM啟動時明確指定的啟動類。
類加載器這部分的水很深,會在之后的文章專門用一篇文章進行解析,之后一段時間,將主要進行工作2年多來項目的回顧總結。
3.通過什么樣的步驟?類的加載過程
Java加載類的過程主要包含如下3步。
a.加載:查找並加載類的二進制文件。
b.鏈接(包含3個子步驟):驗證,確保加載類的正確性,防止惡意代碼;准備,為類的靜態變量分配內存空間並賦默認值;解析,將類的符號引用轉化為直接引用。
c.初始化:為類的靜態變量賦予初始值。
4.類加載的范疇
參考資料
推薦:海子大神的JAVA技術棧相關文章:http://www.cnblogs.com/dolphin0520/
單例模式,你知道的和你所不一定知道的一切
如何防止JAVA反射對單例類的攻擊?
Java設計模式(一):單例模式,防止反射和反序列化漏洞
java 單例模式通過內部靜態類的方式?
java 內部類如何訪問外部類的同名屬性
Java內部類的使用小結
Java類加載器總結
類加載原理分析&動態加載Jar/Dex
Java高新技術第一篇:類加載器詳解