确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
确保某个类只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有一个。例如,创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源。
//普通员工
public class Staff {
public void work(){
//干活
}
}
//CEO,饿汉单例模式
public class CEO extends Staff{
private static final CEO ceo = new CEO();
//构造函数私有
private CEO(){
}
//公有的静态方法,对外暴露获取单例对象的接口
public static CEO getCEO(){
return ceo;
}
public void work(){
//管理VP
}
}
//副总裁
public class VP extends Staff{
public void work(){
//管理下面的经理
}
}
//公司类
public class Company {
private List<Staff> allStaffs = new ArrayList<>();
public void addStaff(Staff staff){
allStaffs.add(staff);
}
public void showAllStaffs(){
for (Staff s :
allStaffs) {
System.out.println("Obj :"+s.toString());
}
}
}
public class Test {
public static void main(String[] args) {
Company company = new Company();
//CEO对象只能通过getCEO函数获取
Staff ceo1 = CEO.getCEO();
Staff ceo2 = CEO.getCEO();
company.addStaff(ceo1);
company.addStaff(ceo2);
//通过new创建VP对象
Staff vp1 = new VP();
Staff vp2 = new VP();
company.addStaff(vp1);
company.addStaff(vp2);
//通过new创建Staff对象
Staff staff1 = new Staff();
Staff staff2 = new Staff();
Staff staff3 = new Staff();
company.addStaff(staff1);
company.addStaff(staff2);
company.addStaff(staff3);
company.showAllStaffs();
}
}
从上述的代码可以看到,CEO类不能通过new的形式构造对象,只能通过CEO.getCEO()函数来获取,而这个CEO对象是静态对象,并且在声明的时候就已经初始化,这就保证了CEO对象的唯一性。
懒汉模式是声明一个静态对象,并在用户第一次调用getInstance()时进行初始化,而上述的饿汉模式(CEO类)是在声明静态对象时就已经初始化。实现如下:
public class Singleton {
private static final Singleton instance;
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
getInstance()方法添加了synchronized关键字,也就是getInstance是一个同步方法,这就是上面所说的在多线程情况下保证单例对象的唯一性的手段。但是这样就会有一个问题,即使instance已经被初始化,每次调用getInstance方法都会进行同步,这样会消耗不必要的资源,这也是懒汉单例模式存在的最大问题。
public class Singleton {
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
getInstance进行了两次判空,第一次判空是为了避免不必要的同步,第二次判空则是为了在null的情况下创建实例。
假设线程A执行到instance=new Singleton()语句,这里看起来是一句代码,但实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了三件事情:
注意:由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Model,即Java内存模型)中Cache,寄存器到主内存回写顺序的规定,上面的第二和第三的顺序是无法保证的。也就是说,执行顺序可能是1-2-3或者1-3-2.如果是后者,并且在3执行完毕,2未执行之前,被切换到B线程上,这时候instance因为已经在线程A内执行了第三步,instance已经是非空了,所以,线程B直接取走instance,再使用时就会出错,这就是DCL失效问题,而且这种难以跟踪难以重视的错误很可能会隐藏很久。
DCL的优点:资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。缺点:第一次加载反应稍慢,也由于Java内存模型的原因偶尔会失败。在高并发环境下也有一定的缺陷,虽然发生概率很小。DCL模式是使用最多的单例实现方式,它能够在需要时才实例化单例对象,并且能够在绝大多数场景下保证单例对象的唯一性。
DCL虽然在一定程度上解决了资源消耗,多余的同步,线程安全问题,但是,它还是在某些情况下会出现失效的问题。这个问题被称为双重检查锁定失效,建议使用如下代码代替。
public class Singleton{
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolder{
private static final Singleton sInstance = new Singleton();
}
}
当第一次加载SIngleton类时并不会初始化sInstance,只有在第一次调用Singleton的getInstance方法时才会导致sInstance被初始化。因此,第一次调用getInstance方法会导致虚拟机加载SingletonHolder类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化。