单例模式

涉及的JVM 与 CPU

Java虚拟机在执行该对象创建语句时具体做了哪些事情,我们简单概括为3步:

  • 1 在栈内存中创建singleton 变量,在堆内存中开辟一个空间用来存放Singleton实例对象,该空间会得到一个随机地址,假设为0x0001。

  • 2 对Singleton实例对象初始化。

  • 3 将singleton变量指向该对象,也就是将该对象的地址0x0001赋值给singleton变量,此时singleton就不为null了。

CPU在执行指令时并不是无节操随意打乱顺序,而是有一定的原则可寻的,这个原则也叫先行发生原则(happens-before)

先行发生原则(happens-before):

  • 1 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作

  • 2 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作

  • 3 volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

  • 4 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

  • 5 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作

  • 6 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

  • 7 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

  • 8 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

单例模式介绍

所谓单例,就是整个程序有且仅有一个实例。该类负责创建自己的对象,同时确保只有一个对象被创建。在Java,一般常用在工具类的实现或创建对象需要消耗资源。

特点

  • 类构造器私有

  • 持有自己类型的属性

  • 对外提供获取实例的静态方法

饿汉模式

线程安全,比较常用,但容易产生垃圾,因为一开始就初始化

public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
       return instance;  
    }  
}

直接调用 Singleton.getInstance() 来创建对象

只要这个类一加载完,就会创建实例对象,显得很着急,像一个恶汉一样,所以称之为恶汉式单例模式。

由于饿汉模式会带来一个问题,类加载的时候静态变量便申请了空间,如果没有用到的话,而且复杂的话,会造成大量的资源浪费,所以用一种懒加载的方式来创建对象,等需要用到这个对象的时候再创建。

懒汉模式

线程不安全,延迟初始化,严格意义上不是不是单例模式

public class Singleton {  
    private static Singleton instance; // 静态类型
    private Singleton (){}  
  
    public static Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}

直接调用 Singleton.getInstance() 来创建对象

由于只有当用到时才创建对象,比较懒,我们称之为懒汉式单例模式。

双重锁模式

线程安全,延迟初始化。这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

public class Singleton {

    private volatile static Singleton singleton;

    private Singleton() {
        System.out.print("实例化对象\n");
    }

    public static Singleton getInsatnce() {
        if (singleton == null) {//-----------------------------1
            synchronized (Singleton.class) {//------------2
                if (singleton == null) {   
                    singleton = new Singleton();  
                }
            }
        }
        return singleton;
    }
}

这里synchronized 的用法和上面的有所不同,上面我们用synchronized 来修饰方法,表示给整个方法上了把锁,我们称之为同步方法。这里我们只给语句1和语句2加了把锁,我们称这种结构为同步代码块,同步代码块synchronized 后面的括号中需要一个对象,可以任意,这里我们用了Singleton的类对象Singleton.class。可以看到我们在方法中进行了两次对象是否为空的判断,一次在同步代码块外面,一次在里面。因此称之为双重检验锁模式(Double Checked Locking Pattern)。

双重检查模式,进行了两次的判断,第一次是为了避免不要的实例,第二次是为了进行同步,避免多线程问题。由于singleton=new Singleton()对象的创建在JVM中可能会进行重排序,在多线程访问下存在风险,使用volatile修饰signleton实例变量有效,解决该问题。

静态内部类单例模式

public class Singleton { 
    private Singleton(){}
    public static Singleton getInstance(){  
        return Inner.instance;  
    }  
    private static class Inner {  
        private static final Singleton instance = new Singleton();  
    }  
} 

只有第一次调用getInstance方法时,虚拟机才加载 Inner 并初始化instance ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。目前此方式是所有单例模式中最推荐的模式,但具体还是根据项目选择。

我们在Singleton 类内部定义一个Inner 静态内部类,里面有一个对象实例,当然由于Java虚拟机自身的特性,只有调用该静态内部类时才会创建该对象,从而实现了单例的延迟加载,同样由于虚拟机的特性,该模式是线程安全的,并且后续读取该单例对象时不会进行同步加锁的操作,提高了性能。

枚举单例模式

  • 枚举类隐藏了私有的构造器。

  • 枚举类的域 是相应类型的一个实例对象

public enum Singleton  {
    INSTANCE 
 
    // doSomething 该实例支持的行为
      
    // 可以省略此方法,通过Singleton.INSTANCE进行操作
    public static Singleton get Instance() {
        return Singleton.INSTANCE;
    }
}

参考如下:

Java 大白话讲解设计模式之 -- 单例模式