简单概念

单例模式:确保这个类,只有一个实例,并且自动实例化向系统提供这个对象。

public class Singleton {

    private static Singleton singleton = null;
    private static int count = 0;
    private Singleton(){
        System.out.println(count++);
    }

    public static Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }

    public static void main(String[] args) {

        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        Singleton singleton3 = Singleton.getInstance();
        System.out.println(singleton1);
        System.out.println(singleton2);
        System.out.println(singleton3);
        //0    表名构造函数只执行了一次,返回的是三个对象是同一个。
        //com.origin.designmodel.Singleton@29453f44
        //com.origin.designmodel.Singleton@29453f44
        //com.origin.designmodel.Singleton@29453f44
    }
}

以上的Singleton称为单例类,上面的代码也就是所谓的懒汉式加载:只有到使用该对象的时候才来创建,意味着:饿了才来做饭吃,太懒了。

懒汉式加载

见上一节点:简单概念

饿汉式加载

顾名思义,饿汉式加载,即:并不是需要的时候才去创建,一开始就创建一个,当需要的时候,直接返回即可。意味着:始终认为是饥饿的,得先创建一个,以供享用。

public class Singleton {

    private static Singleton singleton = new Singleton();//程序启动、项目启动的时候,就创建一个,放那儿。
    private Singleton(){}

    public static Singleton getInstance(){
        return singleton;//当有需要的时候,直接返回即可。
    }

    public static void main(String[] args) {
        
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        Singleton singleton3 = Singleton.getInstance();
        System.out.println(singleton1);
        System.out.println(singleton2);
        System.out.println(singleton3);
        //com.origin.designmodel.Singleton@29453f44
        //com.origin.designmodel.Singleton@29453f44
        //com.origin.designmodel.Singleton@29453f44
    }
}

枚举实现单例模式

单元素的枚举类型已经成为实现Singleton的最佳方法。
点击可见详情

public class Singleton {

    public static void main(String[] args) {
        Resource resource1= Connect.INSTANCE.getResource();
        Resource resource2= Connect.INSTANCE.getResource();
        System.out.println(resource1);
        System.out.println(resource2);
        //connect构造方法!
        //resource构造方法!
        //com.origin.designmodel.Resource@29453f44
        //com.origin.designmodel.Resource@29453f44
    }
}

//通过枚举来创建Resource唯一实例
enum Connect{
    INSTANCE;
    private Resource resource;
    Connect(){
        System.out.println("connect构造方法!");
        this.resource = new Resource();
    }
    public Resource getResource(){
        return resource;
    }
}

//该类才是真正的需要唯一实例对象的类
class Resource{
    public Resource(){
        System.out.println("resource构造方法!");
    }
    //.......
}

线程安全问题

通过以上三种方式可见:

  • 懒汉式是线程不安全的,比如当线程A进入了if(singleton == null)判断里面,还没有进行实例的创建,此时线程B也进入了判断,导致两个线程都在创建实例对象,从而不在具有单例的特性:唯一的实例。
  • 饿汉式枚举实现是线程安全的,因为在线程访问实例之前,实例就是创建好了的,所有的线程过来都是拿到的这个实例。

如何避免懒汉式所带来的安全问题呢?

同步控制方式

将懒汉式中的获取实例的方法加上synchronized关键字,使其成为同步方法:

    public static synchronized Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }

双重检查

public class Singleton {
    // 使用静态变量记录唯一实例
    // volatile可以确保当singleton被初始化后,多线程才可以正确处理
    // 被volatile修饰的变量的值,将不会被本地线程缓存
    // 对该变量读写都是直接操作共享内存,确保多个线程能正确的处理该变量。
    private static volatile Singleton singleton = null ;
    private Singleton (){}
    public static Singleton getInstance (){
        // 如果实例不存在,则进入同步区
        if (singleton == null){
            // 只有第一次才会彻底执行这里面的代码
            synchronized (Singleton.class) {
                if (singleton == null){
                    singleton = new Singleton() ;
                }
            }
        }
        return singleton ;
    }
    public static void main(String[] args) {

        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        Singleton singleton3 = Singleton.getInstance();
        System.out.println(singleton1);
        System.out.println(singleton2);
        System.out.println(singleton3);
        //com.origin.designmodel.Singleton@29453f44
        //com.origin.designmodel.Singleton@29453f44
        //com.origin.designmodel.Singleton@29453f44
    }
}

延迟类初始化

要想很简单地实现线程安全,可以采用静态初始化器的方式,它可以由JVM来保证线程的安全性。比如前面的饿汉式实现方式,在类装载的时候就初始化对象,不管是否需要,存在一定的空间浪费。
一种可行的方式就是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,那就不会创建对象实例,从而同时实现延迟加载和线程安全。


首先要明白两个基础的概念:

  • 类级内部类
    简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。
      类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。
      类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。
      类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。

  • 多线程缺省同步锁
    在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:

  1.由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
  2.访问final字段时
  3.在创建线程之前创建对象时
	  4.线程可以看见它将要处理的对象时
	```

具体的实现方式如下:

```java
public class Singleton {

    //内部类,用于延迟创建实例对象
    private static class SingletonHolder{
        private static Singleton singleton = new  Singleton();
    }
    
    public static Singleton getInstance(){
        return SingletonHolder.singleton;
    }

    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1);
        System.out.println(singleton2);
        //com.origin.designmodel.Singleton@29453f44
        //com.origin.designmodel.Singleton@29453f44
    }
}

JDK中的应用

Runtime 类中使用了饿汉式的单例模式:

应用:

public class Singleton {

    public static void main(String[] args) {
        Runtime runtime1 = Runtime.getRuntime();
        Runtime runtime2 = Runtime.getRuntime();
        System.out.println(runtime1);
        System.out.println(runtime2);
        //java.lang.Runtime@29453f44
        //java.lang.Runtime@29453f44
    }
}

Runtime 源码分析:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }
    private Runtime() {}
}

Spring中的应用

  • 我们存在一个Bean

    public class UserBean {
    }
    
  • 在spring配置文件中配置

    <!-- 单例Bean,此处在bean中应配置scope属性,不配置的话,默认是单例: singleton -->
    <bean id="user" 
    class="com.origin.designmodel.model.UserBean" />
    
    
  • 测试

    @Test
        public void test01 (){
            ApplicationContext context01 = new ClassPathXmlApplicationContext("spring.xml");
            ApplicationContext context02 = new ClassPathXmlApplicationContext("spring.xml");
            UserBean user01 = (UserBean)context01.getBean("user") ;
            UserBean user02 = (UserBean)context01.getBean("user") ;
            UserBean user03 = (UserBean)context02.getBean("user") ;
            // com.origin.designmodel.model.UserBean@365345
            System.out.println(user01);
            // com.origin.designmodel.model.UserBean@365345
            System.out.println(user02);
            // com.origin.designmodel.model.UserBean@c2559k
            System.out.println(user03);
        }
    
  • 结论

    Spring单例模式与纯粹的单例设计模式的主要区别:
    尽管使用相同的类加载器来加载两个应用程序上下文,但是UserBean的实例是不一样的。也就是Spring框架中的单例对象是基于应用程序中,并不是整个系统层面,我们平常写的单例模式中,启动多个main线程中的所有实例都是相同的。

总结

注意

  • 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
  • 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new Object() 的方式。
  • 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象。

优缺点

优点
  • 单例模式只会创建一个对象实例,减少内存的消耗。
  • 可以设置全局访问点,优化资源的访问。
缺点
  • 没有接口,难以扩展。
  • 不利于测试。
  • 与单一职责原则冲突。