简介
说明
本文用示例介绍Java的单例模式的写法。有如下六种写法:懒汉式,饿汉式,静态内部类,双重校验锁,枚举,非synchronized的加锁。
本文所述的单例模式都是线程安全的。线程不安全的单例模式,不是合格的单例模式。
在下边的单例模式中,我比较喜欢静态内部类。如果涉及到反序列化创建对象我会使用枚举的方式。我永远不会使用饿汉式,如果有其他特殊的需求,我可能会使用双重校验锁。
什么是单例模式?
单例模式(Singleton Pattern)用于确保某个类在整个应用程序中只有一个实例,并提供一个全局访问点来获取该实例。
单例模式适用于以下情况
- 一个类只需要一个实例,例如:工具类等。
- 控制资源的使用,避免多次创建对象造成资源浪费。
- 想把实例作为全局访问点,方便其他地方访问。
在 Java 中,实现单例模式的关键在于:
- 将类的构造方法私有化,防止外部直接实例化该类。
- 提供一个静态方法来获取该类的唯一实例,并确保在整个应用程序中只有一个实例存在。
- 要保证线程安全。
第一种 懒汉
简介
懒汉模式就是,它很懒,直到用到的时候才会去创建对象,而不是一开始就创建对象。
特点
- 支持多线程
- 支持懒加载
- 性能很低
- 因为是加锁同步。
实例
public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
第二种 饿汉
简介
饿汉模式就是,它很饿,想快点创建对象,此模式在类加载时就立即创建对象。
特点
- 支持多线程
- 这种方式基于classloder机制保证初始化instance时只有一个线程。
- 不完全支持懒加载
- instance在类装载时就实例化,大多数都是调用getInstance方法。但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
- 性能很高(因为使用时不需要加锁同步)。
法1
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; } }
法2
表面上看起来差别挺大,其实跟法1差不多,都是在类初始化即实例化instance。
public class Singleton { private static Singleton instance = null; static { instance = new Singleton(); } private Singleton() { } public static Singleton getInstance() { return instance; } }
第三种 静态内部类
简介
静态内部类的单例写法是:在类内部有一个静态内部类,它持有外部类的实例。
特点
- 支持多线程
- 同样基于classloder机制保证初始化instance时只有一个线程。
- 支持懒加载
- Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显式装载SingletonHolder类,从而实例化instance。
- 性能很高
- 因为使用时不需要加锁同步。
实例
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton() { } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
第四种 双重校验锁
简介
说明
此模式用了两次if判断。
特点
- 是懒汉模式的升级版。
- 在JDK1.5之后,双重检查锁定才能够正常达到单例效果。
- 原因: Java 5 以前的 JMM (Java 内存模型)存在缺陷,即使将变量声明成 volatile 也不能完全避免重排序。
实例
public class Singleton { private static volatile Singleton singleton; private Singleton() { } public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
为什么加volatile关键字?
volatile作用:保证有序性、可见性。
有序性
Singleton singleton = new Singleton() 这句话可以分为三步:
- 为 Singleton 分配内存空间(加载、链接);
- 初始化 singleton;
- 将 singleton 指向分配的内存空间。
但是由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。 指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得一个未初始化的实例。例如:线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。所以,使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行。
可见性
把变量声明为 volatile,就指示 JVM,修改的值立即被更新到主存。
普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
为什么有两次校验?
第一次校验:也就是第一个if(singleton == null)
这个是为了代码提高代码执行效率。由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。
第二次校验:也就是第二个if(singleton == null)
这个校验是防止二次创建实例。假如有一种情况,当singleton还未被创建时,线程t1调用getInstance方法,由于第一次判断singleton ==null,此时线程t1准备继续执行,但是由于资源被线程t2抢占了,此时t2页调用getInstance方法,同样的,由于singleton并没有实例化,t2同样可以通过第一个if,然后继续往下执行,同步代码块,第二个if也通过,然后t2线程创建了一个实例singleton。此时t2线程完成任务,资源又回到t1线程,t1此时也进入同步代码块,如果没有这个第二个if,那么,t1就也会创建一个singleton实例,那么,就会出现创建多个实例的情况,但是加上第二个if,就可以完全避免这个多线程导致多次创建实例的问题。
所以说:两次校验都必不可少。
第五种 枚举
简介
Effective Java作者Josh Bloch 提倡的方式。
- 支持多线程
- 可保证只有一个实例。这与枚举类的实现有关。
- 支持防止反序列化和防止反射破坏单例。见:Java单例模式–枚举类防止破坏单例 – 自学精灵
关于破坏单例,见:Java单例模式–破坏单例的方法 – 自学精灵
实例
public enum Singleton { INSTANCE; }
简单示例
package org.example.a; enum MyEnum{ FIRST("第一个"), SECOND("第二个"); private String desc; private MyEnum(String desc) { this.desc = desc; } public String getDesc() { return desc; } private String lastName; public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } } public class Demo { public static void main(String[] args) { MyEnum.FIRST.setLastName("Tony"); System.out.println(MyEnum.FIRST.getLastName()); System.out.println(MyEnum.FIRST.getDesc()); MyEnum myEnum = MyEnum.FIRST; MyEnum myEnum1 = MyEnum.FIRST; System.out.println(myEnum == myEnum1); } }
执行结果
Tony 第一个 true
第六种 非synchronized (不常用)
CAS
package org.example.a; import java.util.concurrent.atomic.AtomicReference; public class Singleton { private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>(); private Singleton() { } public static Singleton getInstance() { for (; ; ) { Singleton singleton = INSTANCE.get(); if (null != singleton) { return singleton; } singleton = new Singleton(); if (INSTANCE.compareAndSet(null, singleton)) { return singleton; } } } }
ThreadLocal
package org.example.a; public class Singleton { private static final ThreadLocal<Singleton> singleton = new ThreadLocal<Singleton>() { @Override protected Singleton initialValue() { return new Singleton(); } }; public static Singleton getInstance() { return singleton.get(); } private Singleton() { } }
请先
!