單例模式:在一個應用中一個類對應的對象只有一個。
常見的單例應用:spring中默認bean爲單例,JavaWeb中Application對象。在程序中有些類只需要一個對象,比如全局配置信息,公共服務對象。 單例模式能減少資源的浪費,減少程序配置的複雜度。學習了下各個實現單例的思想。
理想的單例默認應有的特性:線程安全,多線程調用效率高,能延遲加載。
一、單例模式實現方式
1.惡漢式
package singleton;
/**
* 惡漢式單例模式
*/
public class Singleton01 {
//01 私有化構造器
private Singleton01(){}
//02 靜態初始化需要單例的類
private static Singleton01 instance = new Singleton01();
/**
* 返回當前單例
* 因爲單例對象在對象初始化時就生成,不存在多線程競爭
* 優點:不需要synchronize同步,併發調用效率高
* 缺點:不能延遲加載,當此單例未使用時 造成了資源浪費
*/
public static Singleton01 getInstance(){
return instance;
}
}
2.懶漢式
package singleton;
/**
* 懶漢模式
*/
public class Singleton02 {
//01 私有化構造器
private Singleton02(){}
//02 靜態需要單例的類
private static Singleton02 instance ;
/**
* 返回當前單例 增加 synchronize同步 解決多線程調用線程安全問題
* 優點:需要synchronize同步,併發調用效率低
* 缺點:延遲加載,當此單例未使用時 節省資源
*/
public static synchronized Singleton02 getInstance(){
if(instance == null){
instance = new Singleton02();
}
return instance;
}
}
3.雙重檢驗鎖(在實際使用時有一定概率出現線程安全問題返回的對象未初始化完成)
package singleton;
/**
* 雙重檢測鎖
*/
public class Singleton03 {
//01 私有化構造器
private Singleton03(){}
//02 靜態需要單例的類
private static Singleton03 instance ;
/**
* 解決 資源浪費和同步調用效率低問題
*
* 編譯器優化問題和jvm底層模型 有時會出問題:
* 在於JVM 是先分配空間引用 給instance 再實例化對象 還是先實例化完之後 再給引用給instance
* 僅供參考一般不使用
*/
public static Singleton03 getInstance(){
if(instance == null){
//鎖對象
synchronized (Singleton03.class){
if(instance == null){
synchronized (Singleton03.class){
instance = new Singleton03();
}
}
}
}
return instance;
}
}
4.內部類(推薦)--符合 線程安全,多線程調用效率高,延遲加載
package singleton;
/**
* 靜態內部類(懶加載方式)
*/
public class Singleton04 {
//01 私有化構造器
private Singleton04(){}
/**
*單例模式要求: 1.線程安全 2.調用效率高 3.懶加載
*當外部調用getInstance()時纔會加載內部類,同時實例化單例。類加載時 線程安全。
* 此方式兼顧了 調用效率和延遲加載
*
*/
public static Singleton04 getInstance(){
//加載內部類時jvm會自動加鎖 直到類初始化完
return SingletonInner.instance;
}
//靜態內部類
private static class SingletonInner{
private static final Singleton04 instance= new Singleton04();
}
}
5.枚舉
package singleton;
/**
* 枚舉 枚舉本身就是單例模式 。 jvm提供保證
* 無延遲加載
*/
public enum Singleton05 {
/**
* 定義枚舉元素,就代表singleton
* 本身就是單例 ,調用效率高,但是沒延遲加載
*/
instance("tim");
Singleton05(String name){
this.name = name;
}
String name = "tom";
public void getName(){
System.out.println(name);
}
}
二、單例模式注意事項
除枚舉單例模式是JVM來維護,其他方式實現的單例模式不做安全限制,都可以通過反射和序列化方式破解。下面以內部類單例模式爲例,測試破解。
1.反射方式破解單例模式
import singleton.Singleton06Reflect;
import java.lang.reflect.Constructor;
public class client1 {
public static void main(String[] args) throws NoSuchMethodException {
//通過反射打破單例
Singleton06Reflect s6reflect = Singleton06Reflect.getInstance();
Singleton06Reflect ss6reflect = Singleton06Reflect.getInstance();
System.out.println(s6reflect);
System.out.println(ss6reflect);
//獲取class對象
Class clazz6 = Singleton06Reflect.class;
//獲取無參構造函數
Constructor constructor = clazz6.getDeclaredConstructor(null);
//打破權限
constructor.setAccessible(true);
try{
//反射代用構造函數,實例化對象
Singleton06Reflect s = (Singleton06Reflect) constructor.newInstance(null);
Singleton06Reflect ss = (Singleton06Reflect) constructor.newInstance(null);
System.out.println(s);
System.out.println(ss);
}catch (Exception e){
e.printStackTrace();
}
}
}
輸出
可見反射生成的爲新對象,打破了我們使用單例模式的初衷
防範措施及思路:反射調用構造函數 生成新的對象,那麼可以在構造函數裏面做單例的判斷,阻止生成新的對象。
private Singleton06Reflect() throws Exception {
if(SingletonInner.instance != null){
throw new Exception("此類爲單例,禁止生成新對象,你可以通過getInstance()獲取單例對象");
}
}
測試
2.序列化方式打破單例模式
要實現對象的序列化那麼對象要實現Serializable接口 否則序列化時會報java.io.NotSerializableException。
import singleton.Singleton06Reflect;
import java.io.*;
public class client2 {
public static void main(String[] args) throws IOException {
//通過反序列化 打破單例
Singleton06Reflect s6reflect = Singleton06Reflect.getInstance();
//字節數字輸出流
ByteArrayOutputStream baos =null ;
//對象輸出留言
ObjectOutputStream oos =null ;
//字節數組輸入流
ByteArrayInputStream bais =null;
//字節數組輸出流
ObjectInputStream ois =null;
try {
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
//序列化對象s6reflect對象到字節數組流中
oos.writeObject(s6reflect);
//從輸出字節數組流中讀取字節
bais = new ByteArrayInputStream(baos.toByteArray());
ois = new ObjectInputStream(bais);
//反序列化爲新對象
Singleton06Reflect sarry = (Singleton06Reflect) ois.readObject();
System.out.println(s6reflect);
System.out.println(sarry);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}finally {
ois.close();
bais.close();
oos.close();
baos.close();
}
}
}
結果反序列化後爲一個新對象
防範措施及思路。 通過序列化和反序列化的api查閱,當被序列化的對象有public Object readResolve()方法時,直接返回此方法返回的對象。
到此比較完整的單例模式類如下使用內部類的方式:
package singleton;
import java.io.Serializable;
/**
* 靜態內部類(懶加載方式)
*/
public class Singleton06Reflect implements Serializable {
//01 私有化構造器
private Singleton06Reflect() throws Exception {
if(SingletonInner.instance != null){
throw new Exception("此類爲單例,禁止生成新對象,你可以通過getInstance()獲取單例對象");
}
}
/**
*單例模式要求: 1.線程安全 2.調用效率高 3.懶加載
*當外部調用getInstance()時纔會加載內部類,同時實例化單例。類加載時 線程安全。
* 此方式兼顧了 調用效率和延遲加載
*
*/
public static Singleton06Reflect getInstance(){
//加載內部類時jvm會自動加鎖 直到類初始化完
return SingletonInner.instance;
}
//靜態內部類
private static class SingletonInner{
private static Singleton06Reflect instance = null;
static {
try {
instance = new Singleton06Reflect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
//反序列化時直接返回此對象,解決反序列化破解單例模式
public Object readResolve(){
return SingletonInner.instance;
}
}
三、幾種單例模式的性能測試
經過測試性能:惡漢式>枚舉>內部類>雙重檢驗鎖>>>懶漢式
懶漢式因爲存在鎖的競爭,所以性能最低。其他幾種模式相差不大,結合消耗資源的大小,推薦 枚舉和內部類 模式
性能測試代碼:
import singleton.*;
import java.util.concurrent.CountDownLatch;
public class PerformanceTest {
public static void main(String[] args) throws InterruptedException {
//性能測試
Long currentTime = System.currentTimeMillis();
//線程計數器
final CountDownLatch countDownLatch = new CountDownLatch(1000);
for(int i =0;i<1000;i++){
new Thread(new Runnable() {
public void run() {
for(int i=0 ;i<1000000;i++){
Object o = Singleton02.getInstance();
}
//此線程執行結束計數器減一
countDownLatch.countDown();
}
}).start();
}
//等待所有線程執行完
countDownLatch.await();
Long end = System.currentTimeMillis();
System.out.println(end-currentTime);
}
}