Java创建型单例设计模式:全局唯一对象

文章类别 in java, 设计模式

回收站的例子说明单例设计模式

当你打开你桌面上的 「回收站」 时,你可以看到里面有你删除的文件,当你再次或者多次打开桌面的「回收站」时,你会发现无论你打开多少次,系统都只会有一个「回收站」ze的窗口存在,而不会出现多个,这是为什么呢?

试想一下,如果每次打开都会有一个新的「回收站」窗口,那么当你在某一个窗口中清除了垃圾文件,那么另一个窗口依然存在这些文件,这样子的话:

  • 一:用户体验不好,有一种删除了还存在文件的错觉,有歧义。
  • 二:打开多个窗口,创建了多个对象,浪费了资源。

单例设计模式

所以这个时候整个系统就只允许一个「回收站」对象存在,让它具有唯一性,从而统一对它进行管理操作。

那么如何做到确保整个系统只有一个对象,而且能让这个唯一的对象能够提供公共的访问方法呢?那么这时候就可以用到单例设计模式了。

实现思路是这样的:

  1. 让该类的构造函数定义为私有(private),这样其它代码没办法去实例化(new)这个对象
  2. 让该类提供一个静态的方法,且让这个静态方法可以得到这个类的实例。

我们来模拟回收站这个对象:

public class RecycleBin {

    private final static RecycleBin instance = new RecycleBin();

    //让该类的构造函数定义为私有(private),这样其它代码没办法去实例化(new)这个对象
    private RecycleBin(){}

    //让该类提供一个静态的方法,且让这个静态方法可以得到这个类的实例。
    public static RecycleBin getInstance()
    {
        return instance;
    }

    public void showDeletedFile()
    {
        System.out.println("显示被删除的文件");
    }

    public void restoreFile()
    {
        System.out.println("还原被删除的文件");
    }

    public void deletFile()
    {
        System.out.println("彻底删除文件");
    }


}

在这里我们模拟创建了「回收站」这个对象,它拥有显示被删除的文件、还原被删除的文件、彻底删除文件这三个方法,当然这里只是模拟打印出一句话来,真正的程序是很复杂的。而这里以通俗的言语来描述,根据我们刚刚的思路,这里让该类的构造函数定义为私有(private),提供一个静态的方法,让这个静态方法可以得到这个类的实例。

这时候我们调用的时候就是这样的:

public class Test {

    public static void main(String args[])
    {
        RecycleBin recycleBin = RecycleBin.getInstance();
        RecycleBin recycleBin1 = RecycleBin.getInstance();
        RecycleBin recycleBin2 = RecycleBin.getInstance();

        System.out.println("是否用一个实例:" + (recycleBin == recycleBin1 && recycleBin1== recycleBin2));

        recycleBin.showDeletedFile();
        recycleBin.restoreFile();
        recycleBin.deletFile();
    }
}

输出:

是否用一个实例:true
显示被删除的文件
还原被删除的文件
彻底删除文件

我们通过判断可以知道每次getInstance()出来的实例都是同一个,也就是说这个recycleBin具有唯一性,接着便是调用recycleBin的方法了。

这就是一个单例的简单具体实现了,可以说RecycleBin这个类是单例类。

我们可以看到这个类的对象引用永远是同一个,而且这个类提供一个获得该实例的静态方法,通常情况下用的是getInstance()这个名称。

刚刚我们的RecycleBin类是在类加载的时候便去初始化:

 private final static RecycleBin instance = new RecycleBin();

有人也许会觉得这样类一加载就要去实例化对象,会耗费时间且一直占用资源,能不能在使用的时候才初始化呢?

答案是肯定的,我们将我们的RecycleBin修改一下:

public class RecyleBin {

    private volatile static RecyleBin instance;

    //让该类的构造函数定义为私有(private),这样其它代码没办法去实例化(new)这个对象
    private RecyleBin(){}

    //需要的时候才去加载实例
    public static RecyleBin getInstance()
    {
        if(instance==null)
        {
            instance = new RecyleBin();
        }

        return instance;
    }

    public void showDeletedFile()
    {
        System.out.println("显示被删除的文件");
    }

    public void restoreFile()
    {
        System.out.println("还原被删除的文件");
    }

    public void deletFile()
    {
        System.out.println("彻底删除文件");
    }
}

可以看到,这一次我们是先判断instance是否为空,空的话就实例对象再返回,不为空就直接返回对象了,类加载的时候不进行实例化,其实这种方式称为:延迟加载或者懒加载(lazy load),也就是我们在需要的时候才去加载实例。

但是这样会存在一个隐患,就是当多个线程去执行调用这个实例的时候,可能会产生多个实例,例如我这里创建一个MyThread,主要是调用RecyleBin的获取实例方法,然后打印出这个实例的哈希值:

public class MyThread implements Runnable {
    @Override
    public void run() {
        RecyleBin instance = RecyleBin.getInstance();
        System.out.println(instance.hashCode());
    }
}

接着我们在测试类中:

	public static void main(String args[])
    {
       while(true)
       {
           new Thread(new MyThread()).start();
       }
    }

从打印输出中可以看到:

...
1780742980
1780742980
603764719
1780742980
1780742980
1780742980
...

发现哈希值有不一样的,这也是刚刚说的存在这样的隐患,假设当A、B两个线程进入:

	 if(instance==null)
     {
          instance = new RecyleBin();
     }

A线程判断instance不为空的时候,开始进入实例化,不过在这个时候,A还没来得及实例化,B线程就进来判断了,这时候B线程发现instance是空的,于是它也进入实例化。那么这样就会出现多个实例了,违背了我们之前说的单例原则的唯一性了。

解决方法:

如果你看过Java惹人爱的多线程,那么应该会想到用锁来解决这一问题,没错!就是这样:

 	//需要的时候才去加载实例
    public static RecyleBin getInstance()
    {
        if(instance==null)
        {
            synchronized (RecyleBin.class)
            {
                if(instance==null){

                    instance = new RecyleBin();
                }
            }
        }

        return instance;
    }

这里对instance进行了双重判断且加了一把锁,这样解决了线程安全问题:

如果如果A、B线程同时进入getInstance(),当两者都判断为null的时候,这时候它们遇到了synchronized锁,如果A先进入锁里面的代码,那么B只能先在外面等待,当A执行完之后,instance也被实例化了,这时候锁被释放,B进去的时候判断instance不为null,于是不实例化对象,直接返回。这样就实现了单例原则了!

单例设计模式的实现方式

从上面的例子中,以两种方式实现,一种是在类加载的时候就对对象进行实例化,一种是在需要的时候才创建对象,其实这两种都有各自的名字,第一种叫做:饿汉式,第二种叫做懒汉式

以代码提现就是这样的:

java单例模式中的饿汉式

/**
 * Created by wistbean on 2017/9/27.
 * 单例模式:饿汉式方式实现
 */
public class Singleton {

    private static final Singleton INSTANCE = new Singleton();

    private Singleton(){}

    public static Singleton getInstance()
    {
        return INSTANCE;
    }

}

java单例模式中的懒汉式

/**
 * Created by wistbean on 2017/9/27.
 * 单例模式:懒汉式方式实现
 */
public class LazySingleton {

    private volatile static LazySingleton instance = null;

    private LazySingleton(){}

    public static LazySingleton getInstance()
    {
        if(instance == null)
        {
            synchronized (LazySingleton.class)
            {
                if(instance==null)
                {
                    instance = new LazySingleton();
                }
            }
        }

        return instance;
    }

}

java单例模式中的饿汉式与懒汉式的比较

饿汉式在类一加载就实例化了对象,这样就一直存在,比较占用资源,浪费时间,但是它不需要考虑线程安全问题就可以确保对象的唯一性,相对于懒汉式的方式来说获取速度稍微快一些。

懒汉式是在需要的时候才创建对象,不会一直存在占用着资源,不过它对线程方面需要添加线程锁来保证对象的唯一性,相对于饿汉式的方式来说影响了性能。

更好的单例实现方式

饿汉式速度快,懒汉式不会一直占用资源,但是它们又都有缺点,能不能对他们“取其精华去其糟粕”呢?

能!

那就是以静态内部类的方式,他有个名称叫:Initialization-on-demand holder idiom,它的实现方式是这样的:

/**
 * Created by wistbean on 2017/9/27.
 * 单例模式 以内部类的形式实现
 */
public class IodhiSigleton {

    private IodhiSigleton(){}

    private static class LazyHolder
    {
        private final static IodhiSigleton INSTANCE = new IodhiSigleton();
    }

    public static IodhiSigleton getInstance()
    {
        return LazyHolder.INSTANCE;
    }

}

当JVM加载IodhiSigleton的时候,内部类一开始不会被初始化,只有当java虚拟机(JVM)执行才会初始化,也就是当执行getInstance()的时候,jvm才会去执行LazyHolder并且初始化,初始化的时候会实例化一个外部的IodhiSigleton类。

因为这样的初始化阶段是依赖于java虚拟机(JVM)的语言规范 Java Language Specification (JLS),它确保的是类的初始化阶段是串行的,不是并发的。所以不需要线程锁synchronized。

这样的话它就不需要像饿汉式那样一开始就去实例对象占用资源,又做到了懒加载的方式,并且不需要添加线程锁就能确保线程安全,提高了性能。可以算是有了饿汉式和懒汉式的精华,但是去除了它们的缺点,so cool!

单例模式应用场景

  1. 对共享资源进行统一管理的时候,如系统的配置文件,系统的日志管理等。

  2. 当我们只允许一个对象实例来使用的时候,例如系统需要唯一的对象来管理资源,系统需要唯一的对象来生成序列单号。

相关文章

相关资料