浅谈单例模式

什么是单例模式

保证一个类只创建一个实例,并提供一个访问它的全局访问点。

特点:

1、这个类只能有一个实例
2、必须自行创建这个实例
3、它必须自行向系统提供这个实例

饿汉式
public final class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
特点

优点: 线程安全 static修饰的成员变量会在类初始化的过程中被收集进入类构造器方法中, 在多线程的场景下,JVM会保证只有一个线程能执行该类的方法,其他线程将会被阻塞等待,等到唯一的方法执行完成,其他线程将不会再执行方法,转而执行自己的代码,因此static修饰的成员变量在多线程下能保证只实例化一次。
缺点:这种模式在类的初始化阶段就开辟出内存空间,在成员变量较多或者较大的情况下,当没有使用当前对象时会一直占用堆内存


懒汉式
public class Singleton2 {
    private static Singleton2 instance = null;
    private Singleton2() {
    }
    public static Singleton2 getInstance() {
        if (null == instance) {
                instance = new Singleton2();
        }
        return instance;
    }
}
特点

模式使用懒加载方式,由于对象被使用到的时候才会将实例加载到堆内存中,避免初始化时占用内存空间,但是它存在线程不安全的问题

public class TestDemo {
    public static void main(String[] args) {
        T1 t1 = new T1();
        T2 t2 = new T2();
        T3 t3 = new T3();
        T4 t4 = new T4();
        T5 t5 = new T5();
        T6 t6 = new T6();
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();
    }
}
class T1 extends Thread{
    @Override
    public void run() {
        System.out.println(Singleton2.getInstance());
    }
}
class T2 extends  Thread{
    @Override
    public void run() {
        System.out.println(Singleton2.getInstance());
    }
}
class T3 extends Thread{
    @Override
    public void run() {
        System.out.println(Singleton2.getInstance());
    }
}
class T4 extends Thread{
    @Override
    public void run() {
        System.out.println(Singleton2.getInstance());
    }
}
class T5 extends Thread{
    @Override
    public void run() {
        System.out.println(Singleton2.getInstance());
    }
}
class T6 extends Thread{
    @Override
    public void run() {
        System.out.println(Singleton2.getInstance());
    }
}

浅谈单例模式_第1张图片运行上述代码以后6个线程出现了两个不同地址的实例,这就违反了单例模式的初衷。
那么如何解决这问题呢,我们都知道多线程出现资源竞争都是通过加锁来解决,那么如何加锁,怎么样去加合适呢。

在getInstance方法上加锁,因为同步锁会带来锁竞争,带来系统性能开销,从而导致系统性能下降,并且每次请求获取类对象的时候,都会通过 getInstance() 方法获取,除了第一次为 null,其它每次请求基本都是不为 null 的。在没有加同步锁之前,是因为 if 判断条件为 null 时,才导致创建了多个实例。为了避免这一点我们可以在if判断中加锁,减小锁的范围

public class Singleton2 {
    private static Singleton2 instance = null;
    private Singleton2() {
    }
    public static Singleton2 getInstance() {
        if (null == instance) {
            synchronized (Singleton2.class) {
            //只有当instance为null时才加锁,减少系统的资源开销
                instance = new Singleton2();
            }
        }
        return instance;
    }
}

线程测试,这样做还是会存在线程安全的问题,并不能真正实现单例模式的初衷。因为在多线程的场景下,进入判断条件的线程,还是会获取锁创建实例,然后释放锁,在这个过程中有其他的线程进入判断以后,还是会创建实例。此时在进入判断以后再加入一个判断,判断当前实例是否为null,这样能有效避免创建多个实例了。
这种方式通常被称为Double-Check,但是这里还会有其他的问题,因为现在的虚拟机编译器为了尽可能的减少寄存器的读取、存储次数,会充分复用存储的的存储值,如下代码

  • 步骤1:int a = 1;//步骤1:加载a变量的内存地址到寄存器中,加载1到寄存器中,CPU通过mov指令把1写入到寄存器指定的内存中
  • 步骤2:int b = 2;//步骤2 加载b变量的内存地址到寄存器中,加载2到寄存器中,CPU通过mov指令把2写入到寄存器指定的内存中
  • 步骤3:a = a + 1;//步骤3 重新加载a变量的内存地址到寄存器中,加载1到寄存器中,CPU通过mov指令把1写入到寄存器指定的内存中

正常的执行顺序是1-2-3,可是jvm可以对它们进行任意排序以提高性能,这也是虚拟机的编译重排序(这个后面学习到在讲)这样一来Double-Check模式下,假设单例类中的存在其他的属性,并且其他的属性也需要实例化,这个时候除了要实例化单例本身,还要对其他属性进行实例化


//懒汉模式 + synchronized同步锁 + double-check
public final class Singleton2 {
    private static Singleton2 instance= null;//不实例化
    public List<String> list = null;//list属性
    private Singleton2(){
      list = new ArrayList<String>();
    }//构造函数
    public static Singleton2 getInstance(){//加同步锁,通过该函数向整个系统提供实例
        if(null == instance){//第一次判断,当instance为null时,则实例化对象,否则直接返回对象
          synchronized (Singleton2.class){//同步锁
             if(null == instance){//第二次判断
                instance = new Singleton2();//实例化对象
             }
          } 
        }
        return instance;//返回已存在的对象
    }
}

在执行 instance = new Singleton2(); 代码时,正常情况下,实例过程这样的:

  • 给 Singleton 分配内存;
  • 调用 Singleton 的构造函数来初始化成员变量;
  • 将 Singleton 对象指向分配的内存空间(执行完这步 singleton 就为非 null 了)。

如果虚拟机发生了重排序,这个时候步骤3可能在步骤2之前完成,此时刚好有一个线程到了第一次判断,此时instance为非null,并返回使用,而实际上给对象并未完成实例化,这样会导致系统出错。此时Happens-Before规则就发生了效果。

Happens-Before定义

前一个操作的结果可以被后续的操作获取

在java中volatile 关键字可以保证线程间变量的可见性,当线程A对变量X进行修改的后,线程A后面执行的线程就能看到X的变化,此外volatile 在jdk1.5后,还有一个作用是阻止局部重排序的发生,也就是说volatile变量操作指令是不会被重排序的,因此使用volatile来修饰instance之后,Double-Check懒汉式单例模式就不会存在其他问题了。代码如下:

//懒汉模式 + synchronized同步锁 + double-check
public final class Singleton2 {
    private volatile static Singleton2 instance= null;//不实例化
    public List<String> list = null;//list属性
    private Singleton2(){
      list = new ArrayList<String>();
    }//构造函数
    public static Singleton2 getInstance(){//加同步锁,通过该函数向整个系统提供实例
        if(null == instance){//第一次判断,当instance为null时,则实例化对象,否则直接返回对象
          synchronized (Singleton2.class){//同步锁
             if(null == instance){//第二次判断
                instance = new Singleton2();//实例化对象
             }
          } 
        }
        return instance;//返回已存在的对象
    }
}

你可能感兴趣的:(浅谈单例模式)