最近在一个项目中遇到问题了,设计中使用到了单例模式,但是因为多线程使用,出了一个bug,最后通过优化单例模式的写法将问题解决。
这里就来回顾下单例模式。
目录
一.什么是单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。单例模式就是: 在程序运行期间, 某些类有且最多只有一个实例对象。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
二.单例模式的实现思路
① 静态化实例对象, 让实例对象与Class对象互相绑定, 通过Class类对象就可以直接访问;
② 私有化构造方法, 禁止通过构造方法创建多个实例;
③ 提供一个公共的静态方法, 用来返回这个类的唯一实例。
三.单例模式的优缺点
优点:
①、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
②、避免对资源的多重占用。
缺点:
①没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
四.单例模式的不同写法
1.懒汉式
package com.yx.singleton;
/**
* Singleton
* <p>初级原型-懒汉式</>
* <p>优点:用到时再初始化</>
* <p>缺点:并发下会产生多个实例</>
* <p>不推荐</>
*
* @author yx
* @date 2019/11/24 12:18
*/
public class Singleton {
/**
* 静态化实例对象
*/
private static Singleton sInstance = null;
/**
* 私有构造方法
*/
private Singleton() {
}
/**
* 提供一个公共的静态方法, 用来返回这个类的唯一实例.
*
* @return 单例对象
*/
public static Singleton getInstance() {
if (sInstance == null) {
sInstance = new Singleton();
}
return sInstance;
}
}
上面这种写法,在并发环境下,会出现多个实例,线程不安全。
2.同步的懒汉式
package com.yx.singleton;
/**
* LazySingleton
* <p>懒汉式优化,使用synchronized</>
* <p>优点:节省空间, 用到的时候再创建实例对象,线程安全</>
* <p>缺点:所有线程的访问都会进行同步操作,影响性能</>
*
* @author yx
* @date 2019/11/24 12:30
*/
public class LazySingleton {
/**
* 静态化实例对象
*/
private static LazySingleton sInstance = null;
/**
* 私有构造方法
*/
private LazySingleton() {
}
/**
* 提供一个公共的静态方法, 用来返回这个类的唯一实例.
*
* @return 本类的实例
*/
public static synchronized LazySingleton getInstance() {
if (sInstance == null) {
sInstance = new LazySingleton();
}
return sInstance;
}
}
优点:节省空间, 用到的时候再创建实例对象,线程安全
缺点:所有线程的访问都会进行同步操作,影响性能
3.饿汉模式
package com.yx.singleton;
/**
* HungrySingleton
* 饿汉模式,实例化对象在对象声明的时候就进行实例化
* <p>优点:JVM层面的线程安全</>
* <p>缺点:创建之后如果不使用这个实例, 就造成了空间的浪费</>
*
* @author yx
* @date 2019/11/24 12:21
*/
public class HungrySingleton {
/**
* 实例对象
*/
private static HungrySingleton sInstance = new HungrySingleton();
/**
* 禁用构造方法
*/
private HungrySingleton() {
}
/**
* 获取单例对象, 直接返回已创建的实例
*
* @return 本类的实例
*/
public static HungrySingleton getInstance() {
return sInstance;
}
}
优点:线程安全,代码简单
缺点:创建之后如果不使用这个实例, 就造成了空间的浪费
4.双锁检验
package com.yx.singleton;
/**
* DoubleLockSingleton
* 双锁检验
*
* @author yx
* @date 2019/11/24 12:45
*/
public class DoubleLockSingleton {
/**
* 静态化实例对象 volatile防止指令重排
*/
private static volatile DoubleLockSingleton sInstance;
/**
* 私有构造方法
*/
private DoubleLockSingleton() {
}
/**
* 获取单例对象, 直接返回已创建的实例
*
* @return 单例对象
*/
public static DoubleLockSingleton getInstance() {
if (sInstance == null) {
synchronized (DoubleLockSingleton.class) {
if (sInstance == null) {
sInstance = new DoubleLockSingleton();
}
}
}
return sInstance;
}
}
优点:节省空间, 用到的时候再创建实例对象,线程安全,为null时同步创建,相对于2介绍的方式是一个优化
缺点:同步,影响性能
5.静态内部类模式
package com.yx.singleton;
/**
* StaticInnerSingleton
* 静态内部类模式, 也称作Singleton Holder(单持有者)模式
* 线程安全, 懒惰模式的一种, 用到时再加载
*
* @author yx
* @date 2019/11/24 12:51
*/
public class StaticInnerSingleton {
/**
* 禁用构造方法
*/
private StaticInnerSingleton() {
}
/**
* 通过静态内部类获取单例对象, 没有加锁, 线程安全, 并发性能高
*
* @return SingletonHolder.sInstance 内部类的实例
*/
public static StaticInnerSingleton getInstance() {
return SingletonHolder.sInstance;
}
/**
* 静态内部类创建单例对象
*/
private static class SingletonHolder {
private static StaticInnerSingleton sInstance = new StaticInnerSingleton();
}
}
优点:线程安全, 懒惰模式的一种, 用到时再加载,没有加锁, 线程安全, 用到时再加载, 并发行能高。
6.枚举方式
/**
* EnumSingleton
* 枚举类单例模式
*
* <p>优点:不需要考虑序列化的问题;不需要考虑反射的问题</>
* <p>缺点:所有的属性都必须在创建时指定, 也就意味着不能延迟加载; 并且使用枚举时占用的内存比静态变量的2倍还多</>
*
* @author yx
* @date 2019/11/24 15:09
*/
public enum EnumSingleton {
INSTANCE
}
优点:不需要考虑序列化的问题;不需要考虑反射的问题。
缺点:所有的属性都必须在创建时指定, 也就意味着不能延迟加载; 并且使用枚举时占用的内存比静态变量的2倍还多
7.通过ThreadLocal实现单例模式
package com.yx.singleton;
/**
* ThreadLocalSingleton
* 通过ThreadLocal实现单例模式
* <p>不常见,性能也较低</>
*
* @author yx
* @date 2019/11/24 15:19
*/
public class ThreadLocalSingleton {
/**
* 如果 perThreadInstance.get() 返回一个非空值, 说明当前线程已经被同步了: 它要看到instance变量的初始化
*/
private static ThreadLocal sPerThreadInstance = new ThreadLocal();
/**
* 静态实例
*/
private static ThreadLocalSingleton sInstance = null;
/**
* 提供一个公共的静态方法, 用来返回这个类的唯一实例.
*
* @return 静态实例
*/
public static ThreadLocalSingleton getInstance() {
if (sPerThreadInstance.get() == null) {
createInstance();
}
return sInstance;
}
/**
* 创建实例
*/
private static final void createInstance() {
synchronized (ThreadLocalSingleton.class) {
if (sInstance == null) {
sInstance = new ThreadLocalSingleton();
}
}
sPerThreadInstance.set(sPerThreadInstance);
}
public static void remove() {
sPerThreadInstance.remove();
}
}
线程安全,但实现代码复杂,性能偏低
五.JDK种单例的例子
(1) java.lang.Runtime类中的getRuntime()方法;
(2) java.awt.Toolkit类中的getDefaultToolkit()方法;
(3) java.awt.Desktop类中的getDesktop()方法;
(4) java.awt.GraphicsEnvironment#getLocalGraphicsEnvironment()方法。
六.小结
一般情况下,使用单例,建议使用饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用静态内部类模式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式,如果有其他特殊的需求,可以考虑使用第 4 种双锁检验方式。