单例模式的常见写法有两种,懒汉模式和饥饿模式。
懒汉模式的写法如下:
public class LazySingleton {
private static LazySingleton lazy = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (null == lazy) {
lazy = new LazySingleton();
}
return lazy;
}
}
懒汉模式的优点:加载类的时候不实例化对象,节省内存开销,等真正获取实例使用的时候进行实例化。
缺点:因为是在多线程的情况下会出现得到的对象不是同一个的情况,当然也可以在getInstance方法上加锁解决问题。
饥饿模式写法:
public class HungrySingleton {
private static HungrySingleton hs = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hs;
}
}
优点:因为是加载的时候直接初始化成员变量,所以不存在懒汉模式多线程情况下的问题。
缺点:貌似没什么缺点,除了会消耗一点堆内存,加入没有用到这个对象会有点浪费。
不过瑕不掩瑜,总的来说要比懒汉模式要安全得多。Spring容器加载bean的时候直默认采用饥饿模式。通过成员变量来控制org.springframework.beans.factory.support.AbstractBeanDefinition#lazyInit
破坏单例特性的方式:1、反射;2、序列化
@Test
public void testLazySingleton() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException {
LazySingleton l1 = LazySingleton.getInstance();
LazySingleton l2 = LazySingleton.getInstance();
Assert.assertTrue(l1 == l2);
// 通过反射获取单例对象,破坏单例特性
Constructor c = LazySingleton.class.getDeclaredConstructor();
c.setAccessible(true);
LazySingleton reflect1 = (LazySingleton) c.newInstance();
LazySingleton reflect2 = (LazySingleton) c.newInstance();
Assert.assertTrue(reflect1 != reflect2);
// 通过序列化获取单例对象,破坏单例特性
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
oos = new ObjectOutputStream(bao);
oos.writeObject(l1);
oos.writeObject(null);
ByteArrayInputStream bai = new ByteArrayInputStream(bao.toByteArray());
ois = new ObjectInputStream(bai);
LazySingleton seri1 = (LazySingleton) ois.readObject();
LazySingleton seri2 = (LazySingleton) ois.readObject();
Assert.assertTrue(seri1 != seri2);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (null != oos) oos.close();
if (null != ois) ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在破坏单例特性的过程中踩过的坑:
1、反射的时候本来想通过class.forName的方式获取类,然后直接newInstance的方式来获取单例对象。但是后来测试发现这种方式也无法访问私有的构造器。没办法,只有先通过反射拿到构造器,然后打开权限,在new对象。
2、序列化的时候一开始报了EOF异常,一脸懵逼,流的操作太生疏,网上查了下,写对象的最后写个null好像可以避免这个异常,具体为什么还不知道。
除了常见的懒汉和饥饿单例,还有注册式单例。
注册式单例,简单的说就是把对象注册到容器当中,通过容器来管理单例对象,获取单例实例的方式是通过唯一的key值获取对应单例对象。从这可以很容易联想到spring的容器,没错,spring的容器就是注册式单例最典型的的应用。
以下是一个简单的容器单例实现:
public class IocSingleton implements Serializable {
private static final Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
private IocSingleton(){}
public static Object getBean( String className){
synchronized (ioc){
if (!ioc.containsKey(className)){
try {
Class clazz = Class.forName(className);
ioc.put(className,clazz.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
}
return ioc.get(className);
}
}
容器单例与普通单例最大的区别就是普通单例是在类本身存放自己的单例对象,而容器单例是把所有的单例对象放到一个容器当中,由此看出容器单例适用于实例比较多的情况。