设计模式之单例模式
什么是单例模式
单例模式是一种创建设计模式,确保类只有一个实例,同时为此实例提供全局访问点,换句话说就是在JVM中,某个类只允许被创建一次(唯一实例),之后所有的操作都是基于同一个实例。 单例模式同时解决了两个问题(保证类只有一个实例、提供全局访问点),所以违反了单一职责原则。
应用场景
单例模式主要用来确保某个类的实例只能有一个。
- 工厂类
- 配置类
- 日志类
- 资源管理类
- 工具类
各种Mgr和各种Factrory都可以使用单例模式。
优缺点
优点
- 保证了类只有一个实例,节省内存空间。
- 通过全局访问点获取对实例的访问。
- 避免重复创建销毁对象,减少GC,提供性能。
缺点
- 违反了单一职责原则,该模式同时解决了两个问题。
- 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。
- 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
- 单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。
单例模式的实现方式
单例实现需要满足以下两点:
- 构造器私有化,防止外部实例化该对象。
- 提供一个静态方法获取实例,返回相同的实例 。
饿汉式(推荐)
/**
* 饿汉式
* 优点:类加载到内存后,就会实例化对象,JVM保证线程安全。
* 缺点:没有达到懒加载的目的。
* 总结:推荐使用,简单易用,至于懒加载,项目中你不使用它为什么要装载它。
*/
public class Singleton {
/**
* 初始化对象,也可以在static代码块初始化
*/
private static final Singleton INSTANCE = new Singleton();
/**
* 私有化构造器,防止new
*/
private Singleton() {}
/**
* 获取实例对象
*
* @return 实例对象
*/
public static Singleton getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
//测试
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
懒汉式(一)
/**
* 懒汉式(非线程安全)
* 优点:达到了懒加载,按需加载。
* 缺点:多线程下不安全。
* 总结:不推荐,多线程下不安全。
*/
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton() {}
public static Singleton getInstance() {
if (null == INSTANCE) {
try {
//模拟延迟,多线程下同时进入此代码块。
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
//通过hashCode打印,可以发现多线程下是不安全的
System.out.println(Singleton.getInstance().hashCode());
}).start();
}
}
}
懒汉式(二)
/**
* 懒汉式(线程安全)
* 优点:达到了懒加载,按需加载,解决了多线程下安全问题。
* 缺点:通过synchronized解决,效率下降。
* 总结:不推荐。
*/
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (null == INSTANCE) {
try {
//模拟延迟,多线程下同时进入此代码块。
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
//通过hashCode打印,可以发现多线程下是安全的
System.out.println(Singleton.getInstance().hashCode());
}).start();
}
}
}
双重检查
/**
* 双重检查
* 优点:达到了懒加载,按需加载,使用双重检查能够减小锁机制带来的开销,解决了多线程下安全问题。
* 缺点:实现略微复杂,使用volatile保证安全。
* 总结:不推荐。
*/
public class Singleton {
/**
* 使用volatile通过内存屏障禁止指令重排序从而达到线程安全。
*/
private static volatile Singleton INSTANCE = null;
private Singleton() {
}
public static Singleton getInstance() {
if (null == INSTANCE) {
synchronized (Singleton.class) {
if (null == INSTANCE) {
try {
//模拟延迟,多线程下同时进入此代码块。
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
//通过hashCode打印,可以发现多线程下是安全的
System.out.println(Singleton.getInstance().hashCode());
}).start();
}
}
}
为什么使用volatile?
new 对象并不是一个原子操作,new 对象时会有三个步骤:
- 内存申请。
- 调用构造器初始化对象。
- 将对象的引用赋值给变量。
其中1永远是第一步因为2,3都依赖于1,而2,3可能发生指令重排。 在多线程下,线程A进入,调用了INSTANCE = new Singleton(),假设率先执行的是步骤3,此时其他线程进来,发现INSTANCE不为NULL,就会直接返回,产生错误。
静态内部类
/**
* 饿汉式
* 优点:类加载到内存后,就会实例化对象,JVM保证线程安全。
* 缺点:没有达到懒加载的目的。
* 总结:推荐使用,简单易用,至于懒加载,项目中你不使用它为什么要装载它。
*/
public class Singleton {
/**
* 初始化对象
*/
private static final Singleton INSTANCE = new Singleton();
/**
* 私有化构造器,防止new
*/
private Singleton() {
}
/**
* 获取实例对象
*
* @return 实例对象
*/
public static Singleton getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
//测试
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
枚举
/**
* 枚举方式
* 优点:不仅保证了线程安全,还防止了序列化。
* 缺点:没啥缺点。
* 总结:最优。
*/
public class Singleton {
private Singleton() {
}
private enum SingletonHelper{
INSTANCE;
SingletonHelper() {
singleton = new Singleton();
}
private final Singleton singleton;
private Singleton getInstance() {
return singleton;
}
}
public static Singleton getInstance() {
return SingletonHelper.INSTANCE.singleton;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
//通过hashCode打印,可以发现多线程下是安全的
System.out.println(Singleton.getInstance().hashCode());
}).start();
}
}
}
总结
以上几种单例模式实现方式中,除了枚举方式外,其他几种方式都可以通过序列化和反序列化绕过类的private构造方法从而创建出多个实例(实际开发中也不会有人去这么做,费力不讨好)。