单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其(唯一)的对象的方式,可以直接访问,不需要实例化该类的对象(不需要每次new)。

温馨提示:

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己唯一的实例。
  3. 单例类必须给所有其他对象提供这一实例。
  4. 保证一个类只能有一个实例,并提供它的全局访问点。

主要来解决一个全局使用的类频繁的创建和销毁,当你想控制实例数目,节约系统资源的时候。

判断系统是否有了这个单例,如果有则返回,没有则创建。在实现单例的共同特点就是构造函数是私有的。

个人不太恰当的理解(如果错了请指正):

把普通模式和单例模式理解为一根火柴(普通)和一个打火机(单例)的区别,火柴的燃烧要燃尽自己为代价,而打火机可以持续的供给,重复的销毁和创建,如果类占用内存空间较大,耗时也长,如果频繁的创建会产生不必要的消耗的时候,我们就可以建一个单例类供我们使用就好了。

举例:

一个班级只能有一个班主任,只有当这个班主任走了,才能有新的班主任过来。

一个电脑有两台打印机设备,在输出的时候就不能两台打印机同时打印一个文件。

优点:

在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁(比如管理学院首页页面缓存)。

避免对资源的多重占用

缺点:

没有接口,不能继承,与单一职责原则冲突,只关心内部逻辑,而不管外面怎么实例化。

注意事项:

getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

单例模式又分为四种(也可以叫五种,枚举用的相对来说比较少)

  1. 饿汉式
  2. 懒汉式
  3. 双检锁/双重校验锁(DCL,即 double-checked locking)
  4. 登记式/静态内部类
  5. 枚举

接下来就来一个个介绍........

单例模式创建要求:

  1. 只能有一个实例化(构造方法私有化)
  2. 它必须自己创建这个实例(含有一个该类的静态变量来保存这个唯一的实例)
  3. 自己向整个系统提供这个实例(直接暴露或使用静态变量的get方法)

饿汉式

直接创建对象,不存在线程安全问题。比较容易实现,类似的思想的很多。

举例说明更加清晰:

就好比女人最喜欢的双十一,你一下把你家里不管(需要)还是(不需要)的都买了,认为以后会用的到的也买了。这样就会导致占用内存了。

代码展示:

也可以使用枚举式(最简洁)和 静态代码块饿汉式(适合复杂实例化)

//饿汉式 不管需不需要这个对象都创建
public class Singleton
{
    private static Singleton singleton=new Singleton();

    private Singleton(){}

    public static Singleton getInstance()
    {
        return singleton;
    }
}

懒汉式

从上面我们可以了解到饿汉式早早的就把对象创建好了,在执行一些逻辑的时候不存在线程安全问题,但是懒汉式会延迟创建对象可能会导致线程安全问题

线程不安全的源码展示(适合单线程):

//不支持多线程,适用单线程。
//懒汉式:构造器私有化
//静态变量保存实例
//提供一个静态方法,获取这个实例对象
public class Singleton
{
  private static Singleton singleton;

  private Singleton(){}

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

对于这种单线程没问题,但是如果如果两个或多个线程来同时访问instance如果都为null同时申请的时候会对系统数据造成错误

线程安全的源码展示(适合多线程):

既然上面那种不能实现多线称,那要这个单例干嘛?所以既然不允许多个线程同时访问,我们就给他加个锁(synchronized)不就可以了。

//支持多线程。
//懒汉式:构造器私有化
//静态变量保存实例
//提供一个静态方法,获取这个实例对象
public class Singleton
{
  private static Singleton singleton;

  private Singleton(){}

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

但是这样有啥问题呢?就是我获取这个单例对象的时候,被上锁了,你气不气?我们就是在创建这个单例时候多线程可能出现点问题,咱么获取的时候不存在线程安全问题啊。。你为啥获取也要锁??  就好比我们去食堂吃饭,吃饭是不是要一个一个的排队去打饭,但是不妨碍你看着里面的菜流口水《干饭人》


双检锁/双重校验锁:

相对来说实现比较复杂,这种方式采用了双锁(volatile 轻量级锁)(synchronized 重量级锁)机制,安全且在多线程情况下能保持高性能。后面我们就顺便把这两种锁扩展了。

getInstance() 的性能对应用程序很关键。

public class Singleton
{
    private volatile static Singleton singleton;

    private Singleton(){}

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

对于上述存在的问题,我们只要双重判定就行了

  • 首先判断instance是不是为null。如果不是null说明被实例化过直接返回即可!nice!
  • 如果instance为null?上锁之后再判空实例化

或许有人会问了两个null,为什么要两个?

  • 第一个:用来判断是否为空需要上锁new 实例。
  • 第二个:上锁后虽然只有一个会执行当前方法,但是很可能都为null的时候两个或多个都为null上锁的想构造。然后后面线程在等待同时前面线程构造好了,那么它就不需要再去构造这个singleton啦!直接不操作就行了。

这样就相对完美了


登记式/静态内部类(适用于多线程):

这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。利用了 classloader 机制来保证初始化 singleton 时只有一个线程

静态内部类是个好方式。主要是静态内部类和外部类不是一起加载的,并且你去调用它的时候他就会初始化,并且类加载是线程安全的,这个不需要考虑线程安全问题。当加载完之后你就可以直接拿啦。这样也能达到延迟加载的作用。

public class Singleton
{
    private static class SingletonHolder
    {
        private  static  final Singleton INSTANCE=new Singleton();
    }

    private Singleton(){}

    public static final Singleton getInstance()
    {
        return SingletonHolder.INSTANCE;
    }
}

枚举:

种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。

public enum Singleton 
{
    INSTANCE;
    public void whateverMethod() {}
}

懒汉式和饿汉式的区别:

这里举个例子加深一下印象:大家平时都玩过游戏和下过游戏吧,就拿下游戏来举例,拿(我的世界)来举例吧。饿汉式就是一次性把所有的模块全部下完,不管你后续玩不玩都下完了,这样子等待和成本太大,懒汉式就是你要玩什么模块就下什么模块,避免过多的消耗。


volatile和synchronized的区别:

看完上面是不是对这两个锁有了一些的认识,接下来我们就来扩展一下他们的区别。

首先需要理解线程安全的两个方面:

  1. 执行控制。(执行控制:的目的是控制代码执行(顺序)及是否可以并发执行。)
  2. 内存可见。(内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。)

区别在于:

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

如果哪里写的不对的地方,还望指教,感谢支持。

上一篇:1.5.1. The Singleton Scope (单例作用域)


下一篇:MySQL Workbench和phpMyadmin