1.概述
单例模式可以说是最常使用的设计模式了,它的作用是确保某个类只有一个实例,自行实例化并向整个系统提供这个实例。在实际应用中,线程池、缓存、日志对象、对话框对象常被设计成单例,总之,选择单例模式就是为了避免不一致状态,实际上就是保证在JVM中只有一个实例。
2.单例七种写法
分别是「饿汉」、「懒汉(非线程安全)」、「懒汉(线程安全)」、「双重校验锁」、「静态内部类」、「枚举」和「容器类管理」
2.1饿汉
public class SingletonV1 {
/**
* 饿汉式 优点:先天性线程是安全的,当类初始化的 就会创建该对象 缺点:如果饿汉式使用过多,可能会影响项目启动的效率问题。
*/
private static SingletonV1 singletonV1 = new SingletonV1();
/**
* 将构造函数私有化 禁止初始化
*/
private SingletonV1() {
}
public static SingletonV1 getInstance() {
return singletonV1;
}
public static void main(String[] args) {
SingletonV1 instance1 = SingletonV1.getInstance();
SingletonV1 instance2 = SingletonV1.getInstance();
System.out.println(instance1 == instance2);
}
}
显然这种写法比较简单,但问题是无法做到延迟创建对象,事实上如果该单例类涉及资源较多,创建比较耗时间时,我们更希望它可以尽可能地延迟加载,从而减小初始化的负载,于是便有了如下的懒汉式单例:
2.2 懒汉式
2.2.1 线程不安全
public class SingletonV2 {
/**
* 懒汉式 (线程不安全)
*/
private static SingletonV2 singletonV2;
private SingletonV2() {
}
/**
* 在真正需要创建对象的时候使用...
*
* @return
*/
public static SingletonV2 getInstance() {
if (singletonV2 == null) {
try {
Thread.sleep(2000);
} catch (Exception e) {
}
singletonV2 = new SingletonV2();
}
return singletonV2;
}
public static void main(String[] args) {
// SingletonV2 instance1 = SingletonV2.getInstance();
// SingletonV2 instance2 = SingletonV2.getInstance();
// System.out.println(instance1 == instance2);
// 1.模拟线程不安全
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
public void run() {
SingletonV2 instance1 = SingletonV2.getInstance();
System.out.println(Thread.currentThread().getName() + "," + instance1);
}
}).start();
}
}
}
2.2.2线程安全
public class SingletonV3 {
/**
* 懒汉式 线程安全
*/
private static SingletonV3 singletonV3;
private SingletonV3() {
}
/**
* 能够解决线程安全问题,创建和获取实例时都上锁 ,效率非常低,所以推荐使用双重检验锁
*
* @return
*/
public synchronized static SingletonV3 getInstance() {
try {
Thread.sleep(2000);
} catch (Exception e) {
}
if (singletonV3 == null) {
System.out.println("创建实例SingletonV3");
singletonV3 = new SingletonV3();
}
System.out.println("获取SingletonV3实例");
return singletonV3;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
public void run() {
SingletonV3 instance1 = SingletonV3.getInstance();
System.out.println(Thread.currentThread().getName() + "," + instance1);
}
}).start();
}
}
}
这种写法能够在多线程中很好的工作避免同步问题,同时也具备lazy loading机制,遗憾的是,由于synchronized的存在,效率很低,在单线程的情景下,完全可以去掉synchronized,为了兼顾效率与性能问题,改进后代码如下:
2.3 双重检验锁(DCL)
public class SingletonV4 {
/**
* volatile 禁止重排序和 提高可见性
*/
private volatile static SingletonV4 singletonV4;
private SingletonV4() {
}
public static SingletonV4 getInstance() {
if (singletonV4 == null) { // 第一次判断如果没有创建对象 开始上锁...
synchronized (SingletonV4.class) {
if (singletonV4 == null) { // 当用户抢到锁,判断初始化
try {
Thread.sleep(2000);
} catch (Exception e) {
}
singletonV4 = new SingletonV4();
}
}
}
return singletonV4;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
public void run() {
SingletonV4 instance1 = SingletonV4.getInstance();
System.out.println(Thread.currentThread().getName() + "," + instance1);
}
}).start();
}
}
}
这种编写方式被称为“双重检查锁”,主要在getInstance()方法中,进行两次null检查。这样可以极大提升并发度,进而提升性能。毕竟在单例中new的情况非常少,绝大多数都是可以并行的读操作,因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,也就提高了执行效率。但是必须注意的是volatile关键字,该关键字有两层语义。第一层语义是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可简单理解为高速缓存(直接与CPU打交道)和主存(日常所说的内存条),注意工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化,我们写的代码(特别是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同,这在单线程并没什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在Java 1.5后才得以实现,因此1.5前的版本仍然是不安全的,即使使用了volatile关键字。
2.4 静态内部内形式
public class SingletonV5 {
private SingletonV5() {
System.out.println("对象初始...");
}
public static SingletonV5 getInstance() {
return SingletonV5Utils.singletonV5;
}
/**
* 静态内部方式能够避免同步带来的效率问题和有能实现延迟加载
*/
public static class SingletonV5Utils {
private static SingletonV5 singletonV5 = new SingletonV5();
}
public static void main(String[] args) {
System.out.println("项目启动成功");
SingletonV5 instance1 = SingletonV5.getInstance();
SingletonV5 instance2 = SingletonV5.getInstance();
System.out.println(instance1 == instance2);
}
}
正如上述代码所展示的,我们把Singleton实例放到一个静态内部类中,这样可以避免了静态实例在Singleton类的加载阶段就创建对象,毕竟静态变量初始化是在SingletonV5类初始化时触发的,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。从上述4种单例模式的写法中,似乎也解决了效率与懒加载的问题,但是它们都有两个共同的缺点:
序列化可能会破坏单例模式,比较每次反序列化一个序列化的对象实例时都会创建一个新的实例
2.5 枚举形式
public enum EnumSingleton {
INSTANCE;
// 枚举能够绝对有效的防止实例化多次,和防止反射和序列化破解
public void add() {
System.out.println("add方法...");
}
}
public static void main(String[] args) throws Exception {
EnumSingleton instance1 = EnumSingleton.INSTANCE;
EnumSingleton instance2 = EnumSingleton.INSTANCE;
System.out.println(instance1 == instance2);
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
EnumSingleton v3 = declaredConstructor.newInstance();
System.out.println(v3==instance1);
}
代码相当简洁,我们也可以像常规类一样编写enum类,为其添加变量和方法,访问方式也更简单,使用SingletonEnum.INSTANCE进行访问,这样也就避免调用getInstance方法,更重要的是使用枚举单例的写法,我们完全不用考虑序列化和反射的问题。枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性。
2.6容器管理
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String, Object>();
public static void registerService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
{
return objMap.get(key);
}
}
}
这种使用SingletonManager 将多种单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。
3.如何防止破坏单例
虽然单例通过私有构造函数,可以实现防止程序猿初始化对象,但是还可以通过反射和序列化技术破解单例。
3.1 使用反射技术破解单例
// 1. 使用懒汉式创建对象
SingletonV3 instance1 = SingletonV3.getInstance();
// 2. 使用Java反射技术初始化对象 执行无参构造函数
Constructor<SingletonV3> declaredConstructor = SingletonV3.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
SingletonV3 instance2 = declaredConstructor.newInstance();
System.out.println(instance1 == instance2);
3.2 如何防止被反射破解
私有构造函数
private SingletonV3() throws Exception {
synchronized (SingletonV3.class) {
if (singletonV3 != null) {
throw new Exception("该对象已经初始化..");
}
}
}
3.3使用序列化技术破解单例
Singleton instance = Singleton.getInstance();
FileOutputStream fos = new FileOutputStream("E:\\code\\Singleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("E:\\code\\Singleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton singleton2 = (Singleton) ois.readObject();
System.out.println(singleton2==instance)
//返回序列化获取对象 ,保证为单例
public Object readResolve() {
return singletonV3;
}
调用readResolve()可以防止单例不被序列化破坏