什么是单例模式?
通俗的讲,就是在应用程序中只需要某个类保留唯一一个实例对象,不希望有更多的实例。单例模式是 Java 设计模式中最简单的设计模式之一,在应用程序中经常被用到。
单例模式的应用场景有很多,比如线程池、日志对象、缓存、数据库连接池、计算机系统设备管理器等等。这些常常都设计成全局唯一的,方便集中管理,也节省系统的开销。
实现单例模式要注意以下三点:
1、单例类只能有一个实例,不能从其他对象中 new 出来, 即构造器用 private 修饰。
2、单例类必须自己创建自己的唯一实例,需要实现一个方法提供这个实例。
3、单例类必须能给其他对象提供这一实例。
饿汉式,顾名思义指的是在类加载的时候就初始化好对象,不管有没有用到。绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题。
Spring 中 IOC 容器 ApplicationContext 就是典型的饿汉式单例。
public class Singleton { private final static Singleton singleton = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return singleton; } public static void main(String[] args) { for (int i = 0; i < 5; i++) { Singleton singleton1 = Singleton.getInstance(); //获取都是同一个对象 System.out.println(singleton1.hashCode()); } } } 复制代码
还有另外一种写法,利用静态代码块的机制:
public class Singleton { // 1. 私有化构造器 private Singleton(){} // 2. 实例变量 private static final Singleton instance; // 3. 在静态代码块中实例化 static { instance = new Singleton(); } // 4. 提供获取实例方法 public static Singleton getInstance(){ return instance; } } 复制代码
懒汉式和饿汉式相对,指的在程序加载时不初始化对象,什么时候被引用什么时候才初始化对象,即在第一次使用的时候才去初始化对象,可以避免内存浪费。注意在获取实例的 getInstance()方法前加上了 synchronized 关键字,这是为了保证线程安全,避免多线程同一时刻获取对象时造成生成了多个实例。
public class Singleton { private static Singleton singleton = null; private Singleton(){} public synchronized static Singleton getInstance(){ if(singleton == null){ // 1 singleton = new Singleton(); // 2 } return singleton; } } 复制代码
双重检查锁是在懒汉式基础上演变过来的,当分析懒汉式代码时,你会发现只有在第一次调用获取实例方法时才需要同步。因为仅步骤2处的代码需要同步,但只有第一次调用才执行此行,后面的其他调用没有执行此行,但都付出了同步的代价。因此为避免在实例已经创建的情况下每次获取实例都加锁取,提高效率,双重检查锁应运而生。
为什么要二次检查?分析双重检查锁代码,多线程并发情况下, 第一个线程执行完 synchronized 的代码块后,后面的线程仍然需要对 singleton 进行第二次检查,即步骤3,避免重复实例化对象。所以需要对实例对象做两次检查。
既然 synchronized 能保证有序性,为什么还要加 volatile?多线程情况下,synchronized 关键字能够起到同步的作用,保证每次只有一个线程能够操作。这样一来对于其内部就相当于单线程操作,但是不会影响其内部的指令重排。我们知道 volatile 可以禁止指令重排,步骤4是属于复合操作指令,首先是实例化对象,然后才是写操作,关于实例化对象其实分为以下三个步骤:
(1)分配内存空间。
(2)初始化对象。
(3)将内存空间的地址赋值给对应的引用。
但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:
(1)分配内存空间。
(2)将内存空间的地址赋值给对应的引用。
(3)初始化对象
如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为 volatile 类型的变量。volatile 变量的写操作,会保证之前的所有指令一定会在 volatile 写操作之前完成,那么 instance = new SingleTon()
这个复合操作指令一定是对象创建完成再进行赋值。
public class Singleton { private volatile static Singleton singleton = null; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ // 步骤1 synchronized (Singleton.class){ // 步骤2 if(singleton == null){ // 步骤3 singleton = new Singleton(); // 步骤4 } } } return singleton; } } 复制代码
这种方式能达到双检锁方式一样的功效,但实现更为简单。这种和饿汉式比较,在类加载时,singleton实例并没有被初始化,需要显示调用getInstance()方法才会转载SingleHolder类,从而初始化singleton实例,所以达到了延时加载的效果。此方法在实际使用中用的最多,推荐此种写法。
public class Singleton { private static class SingleHolder{ private static Singleton singleton = new Singleton(); } private Singleton(){ } public static Singleton getInstance(){ return SingleHolder.singleton; } } 复制代码
这种方式巧妙的应用了枚举的特点,构造器本身私有,写法简单,自动支持序列化机制,防止多次实例化,获取实例可以通过 Singleton.INSTANCE 来访问。
public enum Singleton { INSTANCE; } 复制代码
java 在同步锁内外判断两次,有什么用处?
Java设计模式(一)—— 单例模式
设计模式 - 单例模式(详解)看看和你理解的是否一样?
设计模式 - 单例模式之多线程调试与破坏单例