单例模式的几种写法
-
什么是单例模式?
单例模式其实是23中经典设计模式中的其中一种,通常解决的是一个类中实例化多个对象的问题。下面我们一起来深入了解一下单例模式吧。 -
定义
一个类中有且仅有一个实例,且由它自身创建出来。对外提供全局统一的访问点,协调系统同一整体的行为 -
特点
- 构造方法私有化,保证对外不能通过
new
进行创建对象 - 提供全局统一的访问点(要想获取这个对象的实例只能通过这一个方法来进行获取)
- 统一协调系统中该实例的整体行为
- 构造方法私有化,保证对外不能通过
-
优点
- 提供了对唯一实例的受控访问
- 由于内存中只有一个实例,节约系统资源浪费问题,对于一些频繁创建销毁对象来说,无疑单例模式可以提高系统的性能。
- 单例模式允许可以创建指定数量的单例,使用单例进行扩展,使控制单例对象的方法可以获取指定个数的实例,用来解决共享实例过多,导致性能低问题。
-
缺点
- 不支持抽象,所以导致扩展性比较差
- 使用场景比较单一,如果是多个使用场景不断变换的实例,不建议使用单例模式,容易产生数据问题。
- 最好不要使用在连接池这一块,N个对象共享一个连接池的情况,很容易会导致连接溢出。
-
单例模式的结构
单例模式是设计模式中最简单设计模式之一,通常一个类的构造方式是public
(公开的),所以我们对外可以通过new
来进行创建该实例。但是,如果将类中的构造方法使用private
(私有的)修饰,使构造方法私有化,这个时候对外在通过new
来进行创建显然是不可以的。所以,该类就必须提供一个私有的静态实例,并对外提供一个公开的静态的方法用来获取本类中私有的静态实例。 -
单例模式的实现
- 单例模式的实现方式有很多种下面我会一一给大家列举单例模式的几种写法。
- 第一种:饿汉式
饿汉式单例是最简单的一种单例写法,也是最常用的一种。
/**
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全
* 简单实用,推荐使用
* 唯一缺点:不管使用与否,类装载时就完成实例化
*
*/
public class Mgr01 implements Serializable{
//实例化Mgr01
private static final Mgr01 INSTANCE = new Mgr01();
//构造私有化
private Mgr01(){
if(null != INSTANCE){
throw new RuntimeException("不允许实例化多个对象");
}
}
//重写readResovle方法防止单例被序列化反序列化破环
private Object readResolve(){
return INSTANCE;
}
/**
* 全局同意访问点
* @return
*/
public static Mgr01 getInstance(){
return INSTANCE;
}
}
测试案例:
以下是我针对饿汉式单例模式进行编写的测试一些案例
其中包含了判断获取到的实例是不是同一个实例
多线程情况下会不会产生多个实例的情况
反射能不能破坏掉该实例
序列化反序列化创建后还是不是同一个实例 当然要想实现序列换必须要实现Serializable接口
/**
* 测试案例
* @param args
*/
public static void main(String[] args) {
//测试获取到的instance1和instance2是不是同一个实例
Mgr01 instance1 = Mgr01.getInstance();
Mgr01 instance2 = Mgr01.getInstance();
System.out.println(instance1 == instance2);
//多线程测试案例
for (int i = 0; i < 100; i++) {
new Thread(()-> System.out.println(Mgr07.getInstance().hashCode())).start();
}
//反射测试案例
try {
Constructor<Mgr01> declaredConstructor = Mgr01.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
Mgr01 mgr01 = declaredConstructor.newInstance();
Mgr01 mgr011 = declaredConstructor.newInstance();
System.out.println(mgr01 == mgr011);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//序列化反序列化测试案例
Mgr01 instance1 = Mgr01.getInstance();
Mgr01 instance2 = null;
try {
//序列化
//创建一个文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("Mgr01.java");
//创建一个对象输出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
//将对象输出到一个文件中
objectOutputStream.writeObject(instance1);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
//反序列化
FileInputStream fileInputStream = new FileInputStream("Mgr01.java");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
instance2 = (Mgr01)objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
System.out.println("比较序列化之前和之后是不是同一个对象:");
System.out.println(instance1==instance2);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
- 懒汉式单例
说到懒汉式单例,在这里我不得不说一下懒汉式单例的演变历史
最开始的懒汉式单例设计的初衷就是为了解决饿汉式单例所带来的资源浪费问题,当然这也是有些人追求完美,过于吹毛求疵,才出现了懒汉式单例这种写法,最开始的懒汉式单例,它是线程不安全的也就是以下这种写法。
- 懒汉式单例-原始版本有线程安全问题(不推荐使用)
/**
* 懒汉式
* 解决了资源浪费问题 但是同时又带来线程不安全的问题
*
*/
public class Mgr02 {
//实例化Mgr02
private static volatile Mgr02 INSTANCE;//volatile 保证原子的可见性
//构造私有化
private Mgr02(){}
/**
* 全局同意访问点
* @return
*/
public static final Mgr02 getInstance(){
if(null == INSTANCE){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr02();
}
return INSTANCE;
}
}
测试案例
这里我只测试了并发的情况,原因很简单,它既然都是线程不安全的,所以在实际的开发过程中我们基本上不用该种写法,除非你的程序是在单线程环境下运行的。
/**
* 测试案例
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()-> System.out.println(Mgr02.getInstance().hashCode())).start();
}
}
懒汉式单例演变的第二个版本
这个版本当然为了解决上一个版本所带来的线程安全问题,那么是怎么解决的呢,说到线程安全,大脑条件反射第一反应就是加锁,没错第二个版本就是对上一个版本进行加锁,当然加锁是可以解决多线程并发访问的问题,但是随着又带来了另外一个严重的问题,那就是性能问题,所以这个版本也是不推荐使用的。
废话不多说,下面请看一下这个版本的懒汉式单例写法:
- 懒汉式单例-第二版本对方法进行加锁(不推荐使用)
/**
* 懒汉式
* 解决了资源浪费问题 但是同时又带来线程不安全的问题
* 为了解决懒汉式单例上一个版本所带来的线程不安全问题 这个时候对线程进行加锁
* 但是同时又随之带来了另外的一个问题 就是性能问题 加锁必然 会导致性能上效率比较低
*/
public class Mgr03 {
//实例化Mgr02
private static volatile Mgr03 INSTANCE; //vloatile 保证原子的可见性
//构造私有化
private Mgr03(){}
/**
* 全局同意访问点
* @return
*/
public static synchronized Mgr03 getInstance(){
if(null == INSTANCE){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr03();
}
return INSTANCE;
}
}
测试案例
这里我只测试了多线程的情况,原因很简单,对静态方法加锁,锁住的是这个类本身,无疑这种效率是极低的。
/**
* 测试案例
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()-> System.out.println(Mgr03.getInstance().hashCode())).start();
}
}
那么有没有解决的办法这个性能低的问题呢,答案是当然有的,请看下面一个版本
- 懒汉式单例-第三个版本对方法内部进行加锁(不推荐使用)
/**
* 懒汉式
* 为了优化上一个版本所带来的性能比较低的问题紧接着又出现了以下写法的单例模式
* 就是先判断是不是该对象是不是已经创建出来了 如果当前实例不为空则直接返回,否则则对其进行加锁 这样相比上一个版本
* 确实可以提高我们大大的提高的了效率 当然仔细看一下这个方法其实还是有问题的她依然是线程不安全的
* 如在多线程环境下访问该实例依然可能会造成线程安全问题 故而又出现了下面一种单例写法
*/
public class Mgr04 {
//实例化Mgr02
private static volatile Mgr04 INSTANCE;
//构造私有化
private Mgr04(){}
/**
* 全局同意访问点
* @return
*/
public static final Mgr04 getInstance(){
if(null == INSTANCE){
synchronized(Mgr04.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
}
return INSTANCE;
}
}
测试案例
该版本虽然相比于上个版本大大的提升了性能上的问题,但是随之却又带来了线程安全的问题,所以这里我也只写了一个多线程的测试案例
/**
* 测试案例
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()-> System.out.println(Mgr04.getInstance().hashCode())).start();
}
}
为了解决上面这个问题,故而才出现了懒汉式单例的最终写法DCL(双重检测锁)写法,DCL的英文全名是DoubleCheckLazy,中文含义为双重检查懒汉式
废话不多说,下面看一下双重检查锁单例的写法:
- 懒汉式-双重检查锁(DCL)最终版本
package com.yangzk.tank.v3.singleton;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 懒汉式 双重检测锁
* 这种单例模式一种比较完美的写法 既解决了资源浪费的问题 又解决了线程安全问题及性能低的问题
* 当然这种单例的写法也变的 复杂了起来
* 那么有没有更简单的一种单例的写法呢 问题只有一个答案 那就是当然有 请看下种写法
* 当然 这种写法也不是最安全的写法 因为它没法办法防止反射的破坏
*/
public class Mgr05 {
//实例化Mgr02
private static volatile Mgr05 INSTANCE;
//构造私有化
private Mgr05(){
}
/**
* 全局同意访问点
* @return
*/
public static final Mgr05 getInstance(){
if(null == INSTANCE){
synchronized(Mgr05.class) {
if(null == INSTANCE){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr05();
}
}
}
return INSTANCE;
}
}
测试案例
双重检查锁单例解决了多线程并发安全的问题,但是它不能防止反射破环的情况,当然一般也不会有人这么无聊去破坏它,这里我也只是提一下
但是这种写法却增加了代码的复杂度。
/**
* 测试案例
* @param args
*/
public static void main(String[] args) {
//多线程校验
/* for (int i = 0; i < 100; i++) {
new Thread(()-> System.out.println(Mgr05.getInstance().hashCode())).start();
}*/
//反射破坏单例测试案例
try {
Constructor<Mgr05> declaredConstructor = Mgr05.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
Mgr05 mgr05 = declaredConstructor.newInstance();
Mgr05 mgr051 = declaredConstructor.newInstance();
System.out.println(mgr05 == mgr051);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
- 静态内部类写法(推荐使用)
这种模式综合了懒汉式和饿汉式的优点,这种方式也是比较推荐的一种
它综合了懒汉式单例和饿汉式单例的优点,堪称一种完美的写法,也是一种比较推荐的写法。
废话不多说,上代码:
/**
* 静态内部类写法
* 懒汉式单例模式
* 这种单例写法结合了懒汉式与饿汉式的优点 算的上是一种比较完美的单例写法了
* 既没有产生性能上的问题 有没有造成资源的浪费
* 也阻止了反射的破环和序列化破环问题
* 当然 我们的java的作者就为我们提供了一种单例的写法就是枚举式单例
*/
public class Mgr06 implements Serializable{
private static final long serialVersionUID = 8526934037791845468L;
//构造私有化
private Mgr06(){
if(null != LAZY.INSTANCE){//解决反射破环单例的问题
throw new RuntimeException("不允许实例化多个对象");
}
}
//私有化内部类 只有当被调用的时候才会被初始化
private static class LAZY{
private static final Mgr06 INSTANCE = new Mgr06();
}
/**
* 全局同意访问点
* @return
*/
public static final Mgr06 getInstance(){
return LAZY.INSTANCE;
}
//重写readResovle方法防止单例被序列化反序列化破环
private Object readResolve(){
return LAZY.INSTANCE;
}
}
测试案例
/**
* 测试案例
* @param args
*/
public static void main(String[] args) {
//多线程测试案例
for (int i = 0; i < 100; i++) {
new Thread(()-> System.out.println(Mgr06.getInstance().hashCode())).start();
}
//反射破环单例测试案例
try {
Constructor<Mgr06> declaredConstructor = Mgr06.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
Mgr06 mgr06 = declaredConstructor.newInstance();
Mgr06 mgr061 = declaredConstructor.newInstance();
System.out.println(mgr06 == mgr061);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//序列化反序列化测试案例
Mgr06 instance1 = Mgr06.getInstance();
Mgr06 instance2 = null;
try {
//序列化
//创建一个文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("Mgr06.java");
//创建一个对象输出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
//将对象输出到一个文件中
objectOutputStream.writeObject(instance1);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
//反序列化
FileInputStream fileInputStream = new FileInputStream("Mgr06.java");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
instance2 = (Mgr06)objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
System.out.println("比较序列化之前和之后是不是同一个对象:");
System.out.println(instance1==instance2);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
- 注册式单例(推荐使用)
该种单例式java官方给我们推荐的一种单例写法,是用枚举类进行编写的,故而我称它为枚举式单例。这种写法也是官方推荐的一种写法。
/**
* 枚举式单例
* 最完美的单例 也是官方最推荐的
*
* 枚举没有午餐的构造方法
* jdk层面就已经屏蔽了反射对枚举类的创建
*/
public enum Mgr07 {
INSTANCE;
public static Mgr07 getInstance(){
return INSTANCE;
}
}
测试案例
枚举类实现单例模式,借助枚举类天然的在IO类与反射类方面的特殊处理,可以天然的防反射攻击,防序列化与反序列化破坏。这样实现的单例模式既简单又安全。
/**
* 测试案例
* @param args
*/
public static void main(String[] args) {
//多线程测试案例
for (int i = 0; i < 100; i++) {
new Thread(()-> System.out.println(Mgr07.getInstance().hashCode())).start();
}
//反射测试案例
try {
Constructor<Mgr07> declaredConstructor = Mgr07.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
Mgr07 mgr07 = (Mgr07)declaredConstructor.newInstance();
Mgr07 mgr071 = declaredConstructor.newInstance();
System.out.println(mgr07 == mgr071);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//序列化反序列化测试案例
Mgr07 instance1 = Mgr07.getInstance();
Mgr07 instance2 = null;
try {
//序列化
//创建一个文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("Mgr07.java");
//创建一个对象输出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
//将对象输出到一个文件中
objectOutputStream.writeObject(instance1);
objectOutputStream.flush();
objectOutputStream.close();
fileOutputStream.close();
//反序列化
FileInputStream fileInputStream = new FileInputStream("Mgr07.java");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
instance2 = (Mgr07)objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
System.out.println("比较序列化之前和之后是不是同一个对象:");
System.out.println(instance1==instance2);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
这里我可以提一点,springIOC中所采用的单例也是注册式单例,不同的是它是采用了currentHashmap这种容器式的单例,方法内部采用双重检查锁校验的方式。有兴趣的可以去看一下。
- 总结
单例的写法有很多很多中,我这里就不一一列举了,但是万变不离其宗,主要的目的就是为了让它只进行实例化一个实例或者指定个数的实例,防止一些实例频繁创建销毁所带来的资源浪费以及性能开销问题。