java设计模式之单例模式详解
单例模式定义及应用场景
使用单例模式,规定全局只能有一个实例,并且提供一个全局访问站点。单例模式在java中的应用场景很多,比如说JAVAEE的servletContext,spring的ApplicationContext,数据库连接池。
饿汉式单例模式
饿汉式单例模式也是提供一个全局实例,只是全局实例在初始化时就加载了,不管有没有用到。
代码示例
public class HungryStaticSingleton {
//先静态后动态
//先上,后下
//先属性后方法
private static final HungryStaticSingleton hungrySingleton;
//装个B
static {
hungrySingleton = new HungryStaticSingleton();
}
private HungryStaticSingleton(){}
public static HungryStaticSingleton getInstance(){
return hungrySingleton;
}
}
饿汉式单例模式定死了初始化时就做创建对象,适用于系统中单例对象较少的场景,由于定死了一个对象,所以在多线程的环境下是安全的。但是在单例对象较多的情况下会占用较大的内存空间。
懒汉式单例模式
因为懒汉式单例模式在类一创建时就会创建对象,当系统中的单例对象过多时,就会占用大量的内存空间。所以这边采用懒汉式单例模式来解决,懒汉式单例模式和饿汉式单例模式一样,构造方法私有化,在类中提供静态公有构造方法提供唯一实例。与饿汉式单例模式不同的是,懒汉式单例模式的对象要在使用的时候才创建。
代码示例
public class LazySimpleSingletion {
private static LazySimpleSingletion instance;
private LazySimpleSingletion(){}
public static LazySimpleSingletion getInstance(){
if(instance == null){
instance = new LazySimpleSingletion();
}
return instance;
}
}
这种情况解决了系统运行就初始化对象的问题,但是存在线程安全问题。我们来开启两个线程来同时访问该实例。
public class LazySimpleSingletionExcutor implements Runnable{
@Override
public void run() {
LazySimpleSingletion instance = LazySimpleSingletion.getInstance();
System.out.println(Thread.currentThread().getName()+": "+instance);
}
}
public class TestLazySimpleSingletion {
public static void main(String[] args) {
Thread thread1 = new Thread(new LazySimpleSingletionExcutor());
Thread thread2 = new Thread(new LazySimpleSingletionExcutor());
thread1.start();
thread2.start();
System.out.println("end");
}
}
在线程跑的位置打上线程断点,然后以debug形式运行。
可以发现线程1和线程0同时运行,势必会出现多线程安全问题问题。
测试运行结果:
这里只有两个线程,在线程量过多的情况下,是有可能出现多个不同的实例的。为了解决这种问题,我们可以在方法上加上synchronized关键字。
public class LazySimpleSingletion {
private static LazySimpleSingletion instance;
private LazySimpleSingletion(){}
public static synchronized LazySimpleSingletion getInstance(){
if(instance == null){
instance = new LazySimpleSingletion();
}
return instance;
}
}
再次运行代码,进入断点调试,可以发现一个进程进入getInstance方法时,另一个线程变成了monitor状态,发生了线程阻塞,这样就可以保证线程安全。可以发现线程安全问题得到解决。
但是当访问线程数量过多时,就会产生一个个的线程阻塞,导致我们程序的运行效率越来越慢,为了解决这个问题,我们采用双重锁机制。
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
//检查是否要阻塞
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
//检查是否要重新创建实例
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
//指令重排序的问题
}
}
}
return instance;
}
}
使用双重锁机制可以保证一个线程在在运行时,另一个线程也可以进入方法内部,在方法内部加锁也可以做到线程安全性。这种情况对性能的影响是肉眼观测不到的。
但是这种方式终归还是要上锁,对性能终归还是有一点影响,有没有什么方式是不要上锁也能保证线程安全类,答案就是采用静态类部类初始化的方式。
public class LazyStaticInnerClassSingleton {
private LazyStaticInnerClassSingleton(){
}
private static LazyStaticInnerClassSingleton getInstance(){
return LazyHolder.INSTANCE;
}
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
用这种方式,既保证了实例不会在创建时就加载,同时静态类部类会在方法调用之前初始化,所以也保证了线程安全问题,同时没有上锁,保证了程序运行的性能。
上面的方式看似是完美,但是金无足赤,人无完人,在牛逼的程序也会有漏洞,我们来用暴力反射获取上面静态类部类实现的构造方法,然后在通过newInstance方法获取实例,可以发现获取到的实例还是两个。
public class LazyStaticInnerClassSingletonTest {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<LazyStaticInnerClassSingleton> lazyStaticInnerClassSingletonClass = LazyStaticInnerClassSingleton.class;
Constructor<LazyStaticInnerClassSingleton> declaredConstructor = lazyStaticInnerClassSingletonClass.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyStaticInnerClassSingleton lazyStaticInnerClassSingleton = declaredConstructor.newInstance();
LazyStaticInnerClassSingleton lazyStaticInnerClassSingleton1 = declaredConstructor.newInstance();
System.out.println(lazyStaticInnerClassSingleton1 == lazyStaticInnerClassSingleton);
}
}
运行代码,发现:
这里显然是创建了两个实例,于是我我们在它的构造方法上添加异常处理。
public class LazyStaticInnerClassSingleton {
private LazyStaticInnerClassSingleton(){
if(LazyHolder.INSTANCE != null){
throw new RuntimeException("不允许非法访问");
}
}
private static LazyStaticInnerClassSingleton getInstance(){
return LazyHolder.INSTANCE;
}
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
在次运行代码,发现:
经过一步步的演进,看似最完美的版本总算诞生了。但是只要我们发散场景,上面这种方式的单例模式还是有可能被破坏。
对象的创建还有另外一种方式即序列化和反序列化,序列化就是将一个对象以文件的形式存储在磁盘中,等到对象要使用时将磁盘文件反序列化成对象来使用,但是两次创建的对象肯定不是同一个,这显然违背了单例模式的定义。
public class SeriableSingletonTest {
public static void main(String[] args) {
LazyStaticInnerClassSingleton singleton1 = null;
LazyStaticInnerClassSingleton singleton2 = LazyStaticInnerClassSingleton.getInstance();
String filePath = "D:\\code\\test\\testSingleton\\SeriableSingleton.obj";
FileOutputStream fos = null;
try {
fos = new FileOutputStream(filePath);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("D:\\code\\test\\testSingleton\\SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
singleton1 = (LazyStaticInnerClassSingleton) ois.readObject();
ois.close();
System.out.println(singleton1);
System.out.println(singleton2);
System.out.println(singleton1 == singleton2);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
代码运行结果:
可以发现此时创建的对象不是同一对象,违反单例模式的原则。解决方式就是给单例对象加上readResolve方法
private Object readResolve() {
return LazyHolder.INSTANCE;
}
在看运行结果,结果如下:
注册式单例模式
注册式单例模式又称登记式单例模式,是将实例登记注册到某个地方,然后统一从发布处获取实例的方式,注册式单例模式有枚举式单例模式和容器式单例模式。
枚举式单例模式
枚举式单例模式是指将实例定义在枚举内部,通过枚举的一个实例方法进行获取的方式。
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){return INSTANCE;}
}
利用io序列化和反序列化的方式创建对象和使用对象测试。
public class EnumSingleTonTest {
public static void main(String[] args) {
try {
EnumSingleton instance1 = null;
EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(new Object());
FileOutputStream fos = new FileOutputStream("D:\\code\\test\\testSingleton\\Enumsingleton ,obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("D:\\code\\test\\testSingleton\\Enumsingleton ,obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println(instance1.getData());
System.out.println(instance2.getData());
System.out.println(instance1.getData() == instance2.getData());
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
由此可见,序列化和反序列化创建对象不能破坏枚举单例。那么反射能不能破坏枚举单例嘞,我们来试一试。
public class EnumSingleTonReflectTest {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<EnumSingleton> enumSingletonClass = EnumSingleton.class;
Constructor<EnumSingleton> constructor = enumSingletonClass.getDeclaredConstructor();
EnumSingleton enumSingleton = constructor.newInstance();
System.out.println(enumSingleton);
}
}
运行结果如下:
这个异常很明显,没有找到空参构造方法,枚举是没有构造方法吗,我们来看看枚举的源码。
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
可以发现,枚举只有一个两个参数的构造方法,那么我们用它这个构造方法来创建一个对象试试。
public class EnumSingleTonReflectTest {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<EnumSingleton> enumSingletonClass = EnumSingleton.class;
Constructor<EnumSingleton> constructor = enumSingletonClass.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingleton enumSingleton = constructor.newInstance("tom",666);
System.out.println(enumSingleton);
}
}
运行结果还是有异常,异常是不能用反射来创建反射对象。
我们习惯性的点开Constructor的newInstance方法,可以发现很明显的对于枚举构造反射对象创建对象,直接就抛出异常了。
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
由此可见,枚举式单例不存在线程安全问题,序列化创建对象和反射创建对象都不能够支持,是目前最稳的单例模式实现。
容器式单例
枚举式单例虽然优雅,但是和饿汉式单例模式一样,在系统初始化的时候就已经将所有对象加载到内存中了。这种在单例对象过多的情况下并不适合使用。我们来看一下一种适合系统中存在多个单例对象的注册单例模式-容器单例模式。
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className){
Object instance = null;
if(!ioc.containsKey(className)){
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
}catch (Exception e){
e.printStackTrace();
}
return instance;
}else{
return ioc.get(className);
}
}
}
可以发现,容器式单例是用一个currentHashMap容器来进行管理的,它是非线程安全的。
ThreadLocal线程单例
ThreadLocal不能保证全局的单例,但是能保证一个线程只有一个实例。
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocaLInstance =
new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton(){}
public static ThreadLocalSingleton getInstance(){
return threadLocaLInstance.get();
}
}
public class ThreadLocalSingletonExecutorThread implements Runnable {
@Override
public void run() {
ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ": " + instance);
}
}