单例模式的三个要点:一是某个类只有一个实例,二是它必须自行创建这个实例,三是它必须自行向整个系统提供这个实例。
单例模式在应用中比较常见,比如我们的web系统中service层和dao层的对象就是单例模式,如果控制层采用的是struts的话是多例模式,如果是SpringMVC的话就是单例模式。(这里涉及的是spring的作用域)
本文主要研究在多线程环境中如何确保在多线程环境下使用单例模式是安全的、正确的。
0.单例模式的例子
比如windows操作系统的回收站就是1个单例。
Java语言的Runtime也是一个单例模式,每个Java应用里都有唯一的Runtime实例,通过这个实例,应用可以与运行环境交互。我们查看源码如下:
public class Runtime { private static Runtime currentRuntime = new Runtime(); public static Runtime getRuntime() { return currentRuntime; } private Runtime() {} 。。。 }
1.立即加载/"饿汉模式"
立即加载就是使用类的时候已经将对象创建完毕,常用的办法就是直接new实例化。而立即记载从中文的语境来看,有"着急","迫切"的意思,所以也称为饿汉模式。
一个简单的立即加载的代码:
package cn.qlq.thread.fifteen; /** * 立即加载==饿汉模式 * @author Administrator * */ public class Singleton_1 { private static Singleton_1 instance = new Singleton_1(); private Singleton_1 () { } public static Singleton_1 getInstance(){ //缺点是不能有其他实例变量,因为此方法没有同步所以有可能出现线程非安全问题 return instance; } }
测试的话开启多个线程打印getInstance()返回对象的hashCode即可。
2.延迟加载/"饱汉模式"
延迟加载就是在调用get方法的时候才创建对象,常见的办法就是在get中new对象。延迟加载有缓慢、不急迫的意思,因此被称为"饱汉模式"。
1.一种简单的写法:(多线程中出现多个实例)
package cn.qlq.thread.fifteen; /** * 延迟加载==饱汉模式 * @author Administrator * */ public class Singleton_2 { private static Singleton_2 instance ; private Singleton_2 () { } public static Singleton_2 getInstance(){ if(instance == null){ instance = new Singleton_2(); } return instance; } }
上面代码在单线程环境下是正确的,但是在多线程环境下很可能出现取出多个实例的情况。
一种多线程环境下的延迟加载出现多个实例的例子:
package cn.qlq.thread.fifteen; /** * 延迟加载==饱汉模式 * * @author Administrator * */ public class Singleton_2 { private static Singleton_2 instance; private Singleton_2() { } public static Singleton_2 getInstance() { try { if (instance == null) { // 模拟处理其他事情 Thread.sleep(3 * 1000); instance = new Singleton_2(); } } catch (InterruptedException e) { e.printStackTrace(); } return instance; } public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { System.out.println(Singleton_2.getInstance().hashCode()); } }; new Thread(runnable).start(); new Thread(runnable).start(); new Thread(runnable).start(); } }
结果:
713786339
713786339
763347431
2.解决方案
2.1 声明synchronized关键字:
public static synchronized Singleton_2 getInstance() { try { if (instance == null) { // 模拟处理其他事情 Thread.sleep(3 * 1000); instance = new Singleton_2(); } } catch (InterruptedException e) { e.printStackTrace(); } return instance; }
将getInstance方法加synchronized关键字修饰,同步整个方法,使得效率比较低下。
2.2 同步代码块:
public static Singleton_2 getInstance() { synchronized (Singleton_2.class) { try { if (instance == null) { // 模拟处理其他事情 Thread.sleep(3 * 1000); instance = new Singleton_2(); } } catch (InterruptedException e) { e.printStackTrace(); } } return instance; }
上面同步代码块相当于同步方法,所以效率还是低下。
2.3 使用DCL双检查锁机制(推荐这种)---需要结合volatile变量使用
package cn.qlq.thread.fifteen; /** * 延迟加载==饱汉模式 * * @author Administrator * */ public class Singleton_2 { private volatile static Singleton_2 instance; private Singleton_2() { } public static Singleton_2 getInstance() { try { if (instance == null) { synchronized (Singleton_2.class) { // 模拟处理其他事情 Thread.sleep(3 * 1000); if (instance == null) { instance = new Singleton_2(); } } } } catch (InterruptedException e) { e.printStackTrace(); } return instance; } }
DCL(Double check Lock)机制结合volatile是大多数多线程结合单例模式使用的场景。
3.使用静态内置类实现单例模式
使用静态内置类也可以实现单例模式。
package cn.qlq.thread.fifteen; /** * 静态内置类实现单例模式 * * @author Administrator * */ public class Singleton_3 { private static class Singleton_3Handle { private static final Singleton_3 instance = new Singleton_3(); } private Singleton_3() { } public static Singleton_3 getInstance() { return Singleton_3Handle.instance; } }
4. 序列化与反序列化的单例模式
静态内置类可以达到线程安全,但如果遇到序列化对象时,使用默认方式运行得到的结果还是多例的。
package cn.qlq.thread.fifteen; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * 静态内置类实现单例模式 * * @author Administrator * */ public class Singleton_4 implements Serializable { /** * serialize id */ private static final long serialVersionUID = 4079299493423471357L; private static class Singleton_4Handle { private static final Singleton_4 instance = new Singleton_4(); } private Singleton_4() { } public static Singleton_4 getInstance() { return Singleton_4Handle.instance; } /*protected Object readResolve() { System.out.println("调用readResolve方法"); return Singleton_4Handle.instance; }*/ public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { Singleton_4 instance = Singleton_4.getInstance(); ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(new File("tt.txt"))); outputStream.writeObject(instance); outputStream.flush(); outputStream.close(); System.out.println(instance.hashCode()); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("tt.txt"))); Singleton_4 readObject = (Singleton_4) inputStream.readObject();// 会自动调用readResolve inputStream.close(); System.out.println(readObject.hashCode()); } }
结果:
1221073180
1940009821
解决办法:将上面readResolve方法的注释去掉:(在inputStream.readObject时候会自动调用readResolve方法)
结果:
1221073180
调用readResolve方法
1221073180
5. static静态代码块实现单例模式
package cn.qlq.thread.fifteen; /** * 静态代码块实现单例模式 * * @author Administrator * */ public class Singleton_5 { private static Singleton_5 instance = null; private Singleton_5() { } static { instance = new Singleton_5(); } public static Singleton_5 getInstance() { return instance; } }
6.使用枚举实现单例模式
使用枚举和使用静态代码块的特性相似,在使用枚举时,构造方法会被自动调用,也可以应用其这个特性实现单例模式。
package cn.qlq.thread.fifteen; /** * 枚举实现单例模式 * * @author Administrator * */ public enum Singleton_6 { instance; private Singleton_6() { System.out.println("调用构造方法"); } public Singleton_6 getInstance() { return instance; } public static void main(String[] args) { System.out.println(Singleton_6.instance.getInstance()); } }
结果:
调用构造方法
instance
补充:这里反汇编查看枚举类的代码发现其继承自Enum类,并且静态代码块中初始化实例
$ javap -c Singleton_6.class Compiled from "Singleton_6.java" public final class Singleton_6 extends java.lang.Enum<Singleton_6> { public static final Singleton_6 instance; public static Singleton_6[] values(); Code: 0: getstatic #1 // Field $VALUES:[LSingleton_6; 3: invokevirtual #2 // Method "[LSingleton_6;".clone:()L java/lang/Object; 6: checkcast #3 // class "[LSingleton_6;" 9: areturn public static Singleton_6 valueOf(java.lang.String); Code: 0: ldc_w #4 // class Singleton_6 3: aload_0 4: invokestatic #5 // Method java/lang/Enum.valueOf:(Lj ava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; 7: checkcast #4 // class Singleton_6 10: areturn public Singleton_6 getInstance(); Code: 0: getstatic #10 // Field instance:LSingleton_6; 3: areturn public static void main(java.lang.String[]); Code: 0: getstatic #7 // Field java/lang/System.out:Ljava/ io/PrintStream; 3: getstatic #10 // Field instance:LSingleton_6; 6: invokevirtual #11 // Method getInstance:()LSingleton_6 ; 9: invokevirtual #12 // Method java/io/PrintStream.printl n:(Ljava/lang/Object;)V 12: return static {}; Code: 0: new #4 // class Singleton_6 3: dup 4: ldc #13 // String instance 6: iconst_0 7: invokespecial #14 // Method "<init>":(Ljava/lang/Strin g;I)V 10: putstatic #10 // Field instance:LSingleton_6; 13: iconst_1 14: anewarray #4 // class Singleton_6 17: dup 18: iconst_0 19: getstatic #10 // Field instance:LSingleton_6; 22: aastore 23: putstatic #1 // Field $VALUES:[LSingleton_6; 26: return }
上面暴露了枚举类,违反了单一职责原则。解决办法如下:(枚举类作为静态内部类)
package cn.qlq.thread.fifteen; /** * 枚举实现单例模式 * * @author Administrator * */ public class Singleton_7 { public enum Singleton_7Handle { instance; private Singleton_7 singleton_7; private Singleton_7Handle() { singleton_7 = new Singleton_7(); System.out.println("调用构造方法"); } public Singleton_7 getSingleton_7() { return singleton_7; } public Singleton_7 getInstance() { return instance.getSingleton_7(); } } public static void main(String[] args) { System.out.println(Singleton_7Handle.instance.getInstance()); } }
结果:
调用构造方法
cn.qlq.thread.fifteen.Singleton_7@4eb09321
补充:其实运用反射可以破坏单例模式,创建多个实例,例如:
package cn.qlq.thread.fifteen; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; /** * * @author Administrator * */ public class Singleton_8 { private static Singleton_8 instance = null; private Singleton_8() { } static { instance = new Singleton_8(); } public static Singleton_8 getInstance() { return instance; } public static void main(String[] args) throws Exception { System.out.println(Singleton_8.getInstance().hashCode()); Class clazz = Singleton_8.class; Constructor declaredConstructor = clazz.getDeclaredConstructor(); declaredConstructor.setAccessible(true); Singleton_8 newInstance = (Singleton_8) declaredConstructor.newInstance(); Singleton_8 newInstance2 = (Singleton_8) declaredConstructor.newInstance(); System.out.println(newInstance.hashCode()); System.out.println(newInstance2.hashCode()); } }
结果:
817899724
397836821
1326857436
为了防止反射创建多个实例,我们可以在构造方法中判断如果存在对象不允许再次创建,也就是抛出一个运行时异常。
private Singleton_8() { if (instance != null) { throw new RuntimeException("无法创建对象"); } }
查看Java类库的Objects类的源码如下:
private Objects() { throw new AssertionError("No java.util.Objects instances for you!"); }