详解五种单例模式
文章目录
一、什么是单例模式
单例模式,属于创建类型的一种常用的软件设计模式。定义是:一个类有且只有唯一一个实例,提供系统使用。
二、单例模式应用的场景
- Web应用配置文件的读取,由于配置文件是共享资源,一般采用单例模式
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
- …
三、单例模式的优缺点
- 优点:节约资源以及提高资源的利用率,如果一个应用总是产生相同的实例,实例一多,就会导致系统内存不足,运行响应缓慢,甚至宕机
- 缺点: 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失
四、单例模式四种模式
单例模式三个主要特点:1、构造方法私有化;2、实例化的变量引用私有化;3、获取实例的方法共有。
0、idea 多线程debug调试
在接下来讲解中,使用多线程测试,因此如何使用idea 多线程debug是非常重要的,以懒汉模式为例
- 打下断点,右键改为Thread,点击Done
- 点击运行debug按钮
1、懒汉模式
类初始化时不会立刻创建实例,而是当调用方法时创建
缺点:线程不安全,延迟初始化,严格意义上不是不是单例模式 ,因为可能创建多个实例
class LazySingleton {
private static LazySingleton singleton = null;
private LazySingleton() {}
public static LazySingleton getInstance() {
// 判断实例是否已创建
if (null == singleton) {
System.out.println("Thread of creating instance: " + Thread.currentThread().getName());
singleton = new LazySingleton();
// 创建实例
System.out.println("Thread of instance: " + singleton);
}
return singleton;
}
}
==============================测试======================================
public class SingleTest {
public static void main(String[] args) {
for (int i = 1; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep((long) (Math.random() * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + LazySingleton.getInstance());
}
}, "thread " + i).start();
}
}
}
测试结果
2、饿汉模式
类初始化时立刻创建实例,线程安全
缺点: 资源利用率不高,可能永远不被调用
class HungrySinglenton {
// 类初始化时就已经创建
private static HungrySinglenton singlenton = new HungrySinglenton();
private HungrySinglenton(){
System.out.println("类初始化就创建");
}
public static HungrySinglenton getInstance() {
return singlenton;
}
}
==========================测试===============================================
public class SingleTest {
public static void main(String[] args) {
for (int i = 1; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep((long) (Math.random() * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + HungrySinglenton.getInstance());
}
}, "thread " + i).start();
}
}
}
测试结果
3、双重检测锁
进行了两次的判断,第一次是为了避免不要的实例,第二次是为了进行同步,避免多线程问题。由于singleton对象的创建在JVM中可能会进行重排序,在多线程访问下存在风险,使用volatile修饰signleton实例变量有效,解决该问题。
class DoubleCheckLockSingleton {
/*使用volatile 防止 类初始化重排*/
private volatile static DoubleCheckLockSingleton singleton;
private DoubleCheckLockSingleton() {}
public static DoubleCheckLockSingleton getInstance() {
// 判断是否存在,避免不要的实例,不过多线程可以进入
if (null == singleton) {
// 使用synchronized,单线程通行,防止多线程进入
synchronized (DoubleCheckLockSingleton.class) {
System.out.println("thread of passing the first lock:" + Thread.currentThread().getName());
// 再次判断是否存在,防止多线程创建,达到双重检测
if (null == singleton) {
System.out.println("thread of creating instance: " + Thread.currentThread().getName());
singleton = new DoubleCheckLockSingleton();
}
}
}
return singleton;
}
}
==============================测试===========================================
// 使用多线程测试
public class SingleTest {
public static void main(String[] args) {
for (int i = 1; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep((long) (Math.random() * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + DoubleCheckLockSingleton.getInstance());
}
}, "thread " + i).start();
}
}
}
测试结果
4、静态内部类
只有第一次调用方法时,虚拟机才加载 Inner 并初始化instance ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。目前此方式是所有单例模式中最推荐的模式,但具体还是根据项目选择。
缺点:无法传参,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去
class StaticInnerSingleton {
private static StaticInnerSingleton singleton = null;
private StaticInnerSingleton(){
System.out.println("类被调用创建实例");
}
public static StaticInnerSingleton getInstance() {
return StaticInner.singleton;
}
private static class StaticInner {
private static StaticInnerSingleton singleton = new StaticInnerSingleton();
}
}
==============================测试===================================
public class SingleTest {
public static void main(String[] args) {
for (int i = 1; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep((long) (Math.random() * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
System.out.println(StaticInnerSingleton.getInstance());
}
}, "thread " + i).start();
}
}
}
测试结果
5、枚举单例
在介绍枚举单例前说下,上面四种单例模式有一个缺点:能通过反射机制调用私有的构造器,创建新的实例,破坏单例模式;而枚举单例就是能解决这一问题
5.1 反射攻击双重检查锁单例模式
// 使用多线程测试
public class SingleTest {
public static void main(String[] args) {
DoubleCheckLockSingleton singleton1 = DoubleCheckLockSingleton.getInstance();
Constructor<DoubleCheckLockSingleton> constructor =
DoubleCheckLockSingleton.class.getDeclaredConstructor(null);
constructor.setAccessible(true); // 设置访问权限
DoubleCheckLockSingleton singleton3 = constructor.newInstance();
System.out.println(singleton3 == singleton1);
}
}
测试结果
5.2 如何防止反射破坏
在第四节第3段的DoubleCheckLockSingleton类中的私有构造方法添加判断
private DoubleCheckLockSingleton() {
// 防止多线程进入,会发布又没用同步代码库快的测试结果
synchronized(DoubleCheckLockSingleton.class) {
if(flag == false) {
flag = !flag;
System.out.println(Thread.currentThread().getName() + " come in");
} else {
throw new RuntimeException("单例模式被侵犯!");
}
}
}
=============================测试================================================
public class SingleTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
// 非反射案例
DoubleCheckLockSingleton singleton1 = DoubleCheckLockSingleton.getInstance();
System.out.println(singleton1);
}
}, "thread-1 ").start();
new Thread(new Runnable() {
@Override
public void run() {
// 反射
try {
Constructor<DoubleCheckLockSingleton> constructor =
DoubleCheckLockSingleton.class.getDeclaredConstructor(null);
constructor.setAccessible(true); // 设置访问权限
DoubleCheckLockSingleton singleton3 = constructor.newInstance();
System.out.println("反射创建实例是否为同一个:" + (singleton3 == singleton1));
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}, "thread-3").start();
}
}
测试结果
没有使用synchronized的结果
使用synchronized的结果
5.3 枚举单例的出现
在jdk1.5后出现枚举实现单例模式
enum EnumSingleton {
ENUM_SINGLETON,
;
public EnumSingleton getInstance(){
return ENUM_SINGLETON;
}
}
=================================测试============================================
public class SingleTest {
public static void main(String[] args) {
//四种单例多线程检测
// multiThreadSingle();
// 反射检测
// invokeSingleton();
// 枚举检测
EnumSingleton singleton1=EnumSingleton.ENUM_SINGLETON;
EnumSingleton singleton2=EnumSingleton.ENUM_SINGLETON;
System.out.println("非反射情况下是否为同一个实例:"+(singleton1==singleton2));
Constructor<EnumSingleton> constructor= null;
try {
constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);//其父类的构造器
constructor.setAccessible(true);
EnumSingleton singleton3 = constructor.newInstance("ENUM_SINGLETON", 666);
System.out.println("反射情况下是否为单例:" + (singleton1==singleton3));
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
测试结果
5.4 枚举单例抵制反射分析
-
为什么使用getDeclaredConstructor(String.class,int.class),而不是 getDeclaredConstructor()?
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { // ==========枚举Enum类,实际上就是Enum======== // 所以反射调用父类构造方法 protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; } }
Constructor类的newInstance方法源码中:
public final class Constructor<T> extends Executable {
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
// =============================如果是枚举的话,反射失败====================================
if ((clazz.getModifiers() & Modifier.ENUM ) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
}