单例模式
基本概念介绍
所谓的单例模式,就是采取一定的方法保证在整个的软件系统中,某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
比如:Hibernate的SessionFactory,他充当数据存储源的代理,并负责创建Session对象。SessionFactory并不是轻量级的,一般情况下,一个项目通常只需要一个就够,这就会使用到单例模式。
单例模式一般实现步骤
- 构造器私有化(防止 new)
- 类的内部创建对象 向外暴露一个静态的公共方法。
- getInstance()拿到唯一的实例对象
单例模式实现方式
饿汉式
顾名思义,饥不择食,无论在什么情况下都会创建实例对象。所以并没有按需创建,没有实现Lazy Loading。这种实现方式优缺点总结如下:
- 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
- 缺点:在类装载的时候就完成实例化,没有达到lazy loading的效果。如果从始至终未用过,则会造成内存的浪费
- 这种方式基于classloader机制避免了多线程的同步问题。不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance()方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式导致类装载,这时候初始化instance就没有达到lazy loading的效果
饿汉式单例模式实现方式一般可以用静态常量或者静态代码块来实现
// 饿汉式(静态变量)
class Singleton {
// 1. 构造器私有化,外部不能new
private Singleton() {
}
// 2. 本类内部创建对象实例
private final static Singleton instance = new Singleton();
// 3. 提供一个共有的静态方法,返回实例对象
public static Singleton getInstance() {
return instance;
}
}
/******/
// 饿汉式(静态代码块)
class Singleton1 {
private static Singleton1 instance;
// 1. 构造器私有化,外部不能new
private Singleton1() {
}
// 2. 在静态代码块中创建单例对象
static {
instance = new Singleton1();
}
// 3. 提供一个共有的静态方法,返回实例对象
public static Singleton1 getInstance() {
return instance;
}
}
懒汉式
针对饿汉式存在的可能会导致内存浪费的问题,于是延伸出了懒汉式单例模式。顾名思义,只有在用到它的时候才会装载,且只实例化一次。
但这就又会引入线程安全问题。即在多线程下,一个线程进入到了if(instance == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程下不可用
class Singleton {
private static Singleton instance;
private Singleton(){}
// 提供一个静态的公有方法,当使用到该方法时,才去创建instance
// 即懒汉式
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
自然而然的,针对多线程问题,我们会想到加锁来使得线程安全,因此,衍生出懒汉式线程安全的版本。
但由于getInstance()在实际中必然会经常使用,而将整个方法进行同步,会使得效率异常底下。
因此,虽然该方法实现了单例,并且保证了线程安全,但实际开发中并不推荐。
class Singleton {
private static Singleton instance;
private Singleton(){}
// 提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
// 即懒汉式
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重检查
针对懒汉式、饿汉式各自的优势弊端,双重检查来实现单例模式是较为推荐的方法。Double-Check概念是多线程开发中常使用到的,如下面代码中所示,进行了两次if(instance == null) 检查,这样就能保证线程安全。
并且同时,外部调用getInstance()方法时,不会被方法本身锁住,如果已有实例,便会直接获取到instance对象。当没有实例时,即使有多个线程都进入到了第一层if(instance == null)里,也可以通过第二层的检查使其不创建多个对象。
该方式:
线程安全,延迟加载,效率较高
class Singleton {
private static volatile Singleton instance;
private Singleton(){}
// 提供一个静态的公有方法,加入双重检查,解决线程安全问题,同时解决懒加载问题
public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
静态内部类方式
这也是一种比较不错的方式
优缺点说明:
- 这种方式采用了类装载的机制来保证初始化实例时只有一个线程
- 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance()方法,才会装载SingletonInstance类,从而完成Singleton的实例化
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮我们保证了线程的安全性,在类进行初始化时,别的线程无法进入
- 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
- 结论:推荐使用
// 使用静态内部类完成单例模式,推荐使用
class Singleton {
private static volatile Singleton instance;
private Singleton(){}
// 写一个静态内部类,该类中有一个静态属性Singleton
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Singleton.getInstance().hashCode());
}).start();
}
}
}
枚举方式
这是Effective Java中所提倡的最优秀的方式。
不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象
// 使用枚举,实现单例模式
enum Singleton {
INSTANCE; // 属性
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(Singleton.INSTANCE.hashCode());
}).start();
}
}
}