單例模式
Java內存模型的抽象示意圖:
所有單例模式都有一個共性,那就是這個類沒有自己的狀態。也就是說無論這個類有多少個實例,都是一樣的;然后除此者外更重要的是,這個類如果有兩個或兩個以上的實例的話程序會產生錯誤。
非線程安全的模式
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance() {
if (instance == null) //1:A線程執行
instance = new Singleton(); //2:B線程執行
return instance;
}
}
普通加鎖
public class SafeLazyInitialization {
private static Singleton instance;
public synchronized static Singleton getInstance() {
if (instance == null)
instance = new Singleton();
return instance;
}
}
出於性能考慮,采用雙重檢查加鎖的模式
雙重檢查加鎖模式
public class Singleton{
private static Singleton singleton;
private Singleton(){
}
public static Singleton getInstance(){
if(null == singleton){ //第一次檢查
synchronized(Singleton.class){ //加鎖
if(null == singleton){ //第二次檢查
singleton = new Singleton();//問題的根源出在這里
}
}
}
return singleton;
}
}
雙重檢查加鎖模式
相對於普通的單例和加鎖模式而言,從性能和線程安全上來說都有很大的提升和保障。然而雙重檢查加鎖模式
也存在一些隱蔽不易被發現的問題。首先我們要明白在JVM創建新的對象時,主要要經過三個步驟。
- 分配內存
- 初始化構造器
- 將對象指向分配的內存地址
這樣的順序在雙重加鎖模式下是么有問題的,對象在初始化完成之后再把內存地址指向對象。
問題的根源
但是現代的JVM為了追求執行效率會針對字節碼(編譯器級別
)以及指令和內存系統重排序(處理器重排序
)進行調優,這樣的話就有可能
(注意是有可能
)導致2和3的順序是相反的,一旦出現這樣的情況問題就來了。
java源代碼到最終實際執行的指令序列:
前面的雙重檢查鎖定示例代碼的(instance = new Singleton();)創建一個對象。這一行代碼可以分解為如下的三行偽代碼:
memory = allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設置instance指向剛分配的內存地址
上面三行偽代碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的,詳情見參考文獻1的“Out-of-order writes”部分)。2和3之間重排序之后的執行時序如下:
memory = allocate(); //1:分配對象的內存空間
instance = memory; //3:設置instance指向剛分配的內存地址
//注意,此時對象還沒有被初始化!
ctorInstance(memory); //2:初始化對象
多線程並發執行的時候的情況:
解決方案
基於Volatile的解決方案
先來說說Volatile
這個關鍵字的含義:
- 可以很好地解決可見性問題
- 但不能確保原子性問題(通過
synchronized
進行解決)- 禁止指令的重排序(單例主要用到此JVM規范)
Volatile 雙重檢查加鎖模式
public class Singleton{
private volatile static Singleton singleton;
private Singleton(){
}
public static Singleton getInstance(){
if(null == singleton){
synchronized(Singleton.class){
if(null == singleton){
singleton = new Singleton();
}
}
}
return singleton;
}
}
基於類初始化的解決方案
利用靜態內部類的方式來創建,因為靜態屬性由JVM確保第一次初始化時創建,因此也不用擔心並發的問題出現。當初始化進行到一半的時候,別的線程是無法使用的,因為JVM會幫我們強行同步這個過程。另外由於靜態變量只初始化一次,所以singleton仍然是單例的。
這個方案的實質是:允許“問題的根源”的三行偽代碼中的2和3重排序,但不允許非構造線程(這里指線程B)“看到”這個重排序。
靜態內部類
的方式
public class Singleton{
private Singleton(){}
public static Singleton getInstance(){
return InnerClassSingleton.singleton;
}
private class InnerClassSingleton{
protected static Singleton singleton = new Singleton();
}
}
然而,雖然靜態內部類
模式可以很好地避免並發創建出多個實例的問題,但這種方式仍然有其存在的隱患。
存在的隱患
- 一旦一個實例被持久化后重新生成的實例仍然有可能是不唯一的。
- 由於java提供了反射機制,通過反射機制仍然有可能生成多個實例。
序列化和反序列化帶來的問題
:反序列化后兩個實例不一致了。
private static void singleSerializable() {
try (FileOutputStream fileOutputStream=new FileOutputStream(new File("myObjectFilee.txt"));
ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);) {
// SingletonObject singletonObject = SingletonObject.getInstance();
// InnerClassSingleton singletonObject = InnerClassSingleton.getInstance();
EnumSingleton singletonObject = EnumSingleton.INSTANCE;
objectOutputStream.writeObject(singletonObject);
objectOutputStream.close();
fileOutputStream.close();
System.out.println(singletonObject.hashCode());
} catch (IOException e) {
e.printStackTrace();
}
try (FileInputStream fileInputStream=new FileInputStream(new File("myObjectFilee.txt"));
ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);) {
// SingletonObject singleTest=(SingletonObject) objectInputStream.readObject();
// InnerClassSingleton singleTest=(InnerClassSingleton) objectInputStream.readObject();
EnumSingleton singleTest=(EnumSingleton) objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
System.out.println(singleTest.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
問題點及解決辦法
ObjectInputStream中的readOrdinaryObject
。
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
調用自定義的readResolve
方法
protected Object readResolve(){
System.out.println("調用了readResolve方法!");
return InnerClassSingleton.getInstance();
}
通過反射機制獲取到兩個不同的實例
private static void attack() {
try {
Class<?> classType = InnerClassSingleton.class;
Constructor<?> constructor = classType.getDeclaredConstructor(null);
constructor.setAccessible(true);
InnerClassSingleton singleton = (InnerClassSingleton) constructor.newInstance();
InnerClassSingleton singleton2 = InnerClassSingleton.getInstance();
System.out.println(singleton == singleton2); //false
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
解決方案
: 私有構造方法中進行添加標志判斷。
private InnerClassSingleton() {
synchronized (InnerClassSingleton.class) {
if (false == flag) {
flag = !flag;
} else {
throw new RuntimeException("單例模式正在被攻擊");
}
}
}
單例最優方案,枚舉
的方式
枚舉實現單例的優勢
- 自由序列化;
- 保證只有一個實例(即使使用反射機制也無法多次實例化一個枚舉量);
- 線程安全;
public enum Singleton {
INSTANCE;
private Singleton(){}
}
Hibernate的解決方案
通過ThreadLocal
的方式
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.cfg.Configuration;
public class HibernateSessionFactory {
private static String CONFIG_FILE_LOCATION = "/hibernate.cfg.xml";
private static final ThreadLocal threadLocal = new ThreadLocal();
private static Configuration configuration = new Configuration();
private static org.hibernate.SessionFactory sessionFactory;
private static String configFile = CONFIG_FILE_LOCATION;
static {
try {
configuration.configure(configFile);
sessionFactory = configuration.buildSessionFactory();
} catch (Exception e) {
System.err.println("%%%% Error Creating SessionFactory %%%%");
e.printStackTrace();
}
}
private HibernateSessionFactory() {
}
public static Session getSession() throws HibernateException {
Session session = (Session) threadLocal.get();
if (session == null || !session.isOpen()) {
if (sessionFactory == null) {
rebuildSessionFactory();
}
session = (sessionFactory != null) ? essionFactory.openSession() : null;
threadLocal.set(session);
}
return session;
}
// Other methods...
}
參考文檔: