什么是单例模式
在应用中一个类仅有一个实例,这个实例由类本身提供,客户端无法自主创建实例
单例模式的角色
Singleton:单例类,仅有一个实例,由单例类自己返回自己的实例
首先提供一种简单的写法——饿汉模式:
Printer:
public class Printer {
private static Printer printer=new Printer();
private Printer(){}
public static Printer getPrinter(){
return printer;
}
public void getSomethingToPrint(String thing){
System.out.println("打印"+thing);
}
}
这种写法是线程安全的,static保证了对象的唯一性,因为static修饰的对象的实例化是在JVM加载类过程中完成的,一般情况下,JVM只会加载某个类一次,关于恶汉模式的巧妙性,可以参考我的一篇博客:java中的单体类
恶汉模式有一点问题,假设某个类的实例化需要耗费许多系统资源,并且占据的内存也比较大,但是这个对象并不急着使用,我们想先加载这个类,但并不想立刻使用这个类的实例(这种情况是存在的),此时恶汉模式就会出现问题,static保证加载这个类时JVM就会实例化这个类,如何解决?答案是按需加载(这种思想在计算机中非常常见)
按需加载使用的实现方式——懒汉模式:
public class ThreadSafeSingleton
{
private static ThreadSafeSingleton singObj = null;
private ThreadSafeSingleton(){
}
public static Synchronized ThreadSafeSingleton getSingleInstance(){
if(null == singObj ) singObj = new ThreadSafeSingleton();
return singObj;
}
}
这种实现在多线程下不会出现问题,但是效率非常的低,因为使用了锁,所以当多个线程同时请求这个实例时,就会出现排队使用的情况,有没有效率高,并且不会出现线程问题的写法呢?答案是有的,而且设计的非常精妙:
public class Singleton
{
private Singleton(){}
private static class SingletonHolder
{
public static Singleton instance = new Singleton();
}
public static Singleton getInstance()
{
return SingletonHolder.instance;
}
public void print(){
System.out.println("输出");
}
}
private修饰内部类,可以隐藏内部类,static修饰内部类,一方面是因为getInstance()为静态方法,仅能使用static修饰的类、属性、方法,另一方面是instance必须是static修饰,如果不是static修饰,则必须先实例化SingletonHolder(此时instance才会被初始化),为了保证Singleton的唯一性(每次返回都是同一个实例),SingletonHolder必须具有唯一性,这又是一个单例问题(将问题复杂化了),所以static修饰instance是必须的(可以简化问题),当我们第一次调用getInstance方法时,JVM才会去加载SingletonHolder,按需加载使用的目的达成,并且整个过程只会实例化Singleton类,内部类不会实例化。
还有一种写法叫做双重检查模式,这种写法在多线程下是会有问题的:
public class DoubleCheckedSingleton
{
private static DoubleCheckedSingletonsingObj = null;
private DoubleCheckedSingleton(){
}
public static DoubleCheckedSingleton getSingleInstance(){
if(null == singObj ) {
Synchronized(DoubleCheckedSingleton.class){
if(null == singObj)
singObj = new DoubleCheckedSingleton();
}
}
return singObj;
}
}
JVM实例化一个类时,会先在堆中申请一块内存,接着执行构造函数,本例中,当有多个线程执行时,线程A开始实例化singObj,此时singObj已经在堆中有地址了,但singObj的构造函数执行比较慢,此时线程B执行getSingleInstance()函数,由于singObj有了地址,所以线程B直接得到singObj对象,但singObj对象的构造函数并未执行完毕,此时部分值可能就为空
使用单例模式时,由于单例是一块共有资源,多线程下需要注意是否存在改值问题,使用单例模式更多是因为使用需求要求某个类仅有一个实例,所以单例模式的优点我看不太出来,但单例模式的几种写法的技巧性比较高,也非常精妙