单列模式与多线程
在23个标准设计模式中,单例模式在应用中还是很常见的,但是在多线程环境中,单例模式的使用有非常多的坑,使用好单例模式的一个原则:如何使单例模式在遇到多线程的环境中是安全的、正确的。下面分析几种多线程的实现方式以及遇到的坑。
一、立即加载/饿汉模式
立即加载:实用类的时候已经将对象创建完毕,常见的是直接new实例化,有“着急”,“急迫”的意思,因此也称:“饿汉模式”。在调用方法前,已经实例化对象。代码如下:
单例模式:
public class SingleTon01 { private static SingleTon01 instance=new SingleTon01(); public SingleTon01() { super(); } public static SingleTon01 getInstance(){ return instance; } }
线程:
public class MyThread extends Thread{ @Override public void run() { System.out.println(SingleTon01.getInstance().hashCode()); } }
测试类:
public class Run { public static void main(String[] args) { MyThread m1=new MyThread(); MyThread m2=new MyThread(); MyThread m3=new MyThread(); MyThread m4=new MyThread(); m1.start(); m2.start(); m3.start(); m4.start(); } }
运行结果:
所有线程的对象hashCode均是一样的,证明是单例模式,but,该代码的实现是优缺点的:不能有其他实例变量,因为getInstance方法没有同步,可能会出现线程安全问题。
二、延迟加载/懒汉模式
延迟加载:在调用方法的时候,对象才被实例化,常用的实现方式就是在方法内部实例化对象。代码如下:
单例模式:
public class SingleTon02 { private static SingleTon02 instance; public SingleTon02() { super(); } public static SingleTon02 getInstance(){ if(null==instance){ instance=new SingleTon02(); } return instance; } }
线程类:
public class MyThread extends Thread{ @Override public void run() { System.out.println(SingleTon02.getInstance().hashCode()); } }
测试类:
public class Run { public static void main(String[] args) { MyThread m1=new MyThread(); MyThread m2=new MyThread(); MyThread m3=new MyThread(); MyThread m4=new MyThread(); m1.start(); m2.start(); m3.start(); m4.start(); } }
运行结果:
从运行结果来看,控制台打印了多个hashCode值,说明该实现方式在多线程的环境中是失败的,如何解决呢?其实很简单,让方法同步即可,使用synchronized关键字。改进后代码吐下:
public class SingleTon02 { private static SingleTon02 instance; public SingleTon02() { super(); } synchronized public static SingleTon02 getInstance(){ try { if(null==instance){ Thread.sleep(3000);//模拟业务处理 instance=new SingleTon02(); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return instance; } }
再次运行:
同步之后,证明该单例模式是正确的。但是,这种方式又带来一种缺点,那就是效率问题,因为下一个线程必须需要等上一个线程释放锁之后才能执行,需要排队执行,因此还可以优化,那就是:尝试同步代码块,针对重要代码进行单独同步,以提升效率。
下面总结了一种使用DCL双检查锁机制实现单例模式,该模式适用于在多线程环境中的延迟加载单例模式设计。代码如下:
public class SingleTon03 { private volatile static SingleTon03 instance; public SingleTon03() { super(); } public static SingleTon03 getInstance(){ try { if(null==instance){ Thread.sleep(3000);//模拟业务处理 synchronized (SingleTon03.class) { if(null==instance){ instance=new SingleTon03(); } } } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return instance; } }
这种方式既保证了线程的安全性,还保证了效率。
三、使用静态内之类实现单例模式
前面的改进方式可以实现在多线程的环境中实现单例模式,并且保证线程安全,那么这种静态内置类的方式也可以实现同样的效果。创建静态内类,如下:
public class SingleTon04 { private static class SingleInner{ private static SingleTon04 instance=new SingleTon04(); } public SingleTon04() { super(); } public static SingleTon04 getInstance(){ return SingleInner.instance; } }
线程类:
public class MyThread extends Thread{ @Override public void run() { System.out.println(SingleTon04.getInstance().hashCode()); } }
测试类同上
运行结果:
四、序列化与反序列化实现单例模式
静态内置类固然可以实现单例模式,但是这里有一个坑,那就是在遇到序列化和反序列化的时候,依然会出现问题,依然会出现多个实例化对象,代码如下:
单例模式
public class SingleTon05 implements Serializable{ private static final long serialVersionUID = 888888L; //内部类方式 private static class SingleTonInner{ private static final SingleTon05 instance=new SingleTon05(); } public SingleTon05() { super(); } public static SingleTon05 getInstance(){ return SingleTonInner.instance; } }
序列化运行类:
public class Run2 { public static void main(String[] args) { //写 try { SingleTon05 singleTon05=SingleTon05.getInstance(); FileOutputStream out=new FileOutputStream(new File("singleton05.txt")); ObjectOutputStream objectOutputStream=new ObjectOutputStream(out); objectOutputStream.writeObject(singleTon05); objectOutputStream.close(); out.close(); //打印hashcode System.out.println(singleTon05.hashCode()); } catch (IOException e) { e.printStackTrace(); } //读 try { FileInputStream in=new FileInputStream(new File("singleton05.txt")); ObjectInputStream objectInputStream=new ObjectInputStream(in); SingleTon05 singleTon05=(SingleTon05)objectInputStream.readObject(); objectInputStream.close(); in.close(); //打印hashcode System.out.println(singleTon05.hashCode()); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } }
运行结果:
很明显,写入和读出来的对象不是一个,显然不符合单例模式的设计模式。序列化破坏了单例模式,当然,还有一种破坏单例模式的方式,那就是反射,单例模式中尽量不要使用反射。呢么问题来了,如何改进呢,其实很简单,在序列化的时候在调用一个方法。改进如下:
public class SingleTon05 implements Serializable{ private static final long serialVersionUID = 888888L; //内部类方式 private static class SingleTonInner{ private static final SingleTon05 instance=new SingleTon05(); } public SingleTon05() { super(); } public static SingleTon05 getInstance(){ return SingleTonInner.instance; } protected Object readResolve()throws ObjectStreamException { System.out.println("调用了readResolve方法!"); return SingleTonInner.instance; } }
再次运行:
序列化操作提供了一个很特别的钩子(hook)-类中具有一个私有的被实例化的方法readresolve(),这个方法可以确保类的开发人员在序列化将会返回怎样的object上具有发言权。这样就确保我们在反序列化的时候返回的对象是同一个。
五、使用静态代码块实现单例模式
静态代码块中的代码执行实在实用类的时候加载,因此我们可以应用静态代码块的这种特性来设计单例模式。代码如下:
public class SingleTon06{ private static SingleTon06 instance=null; public SingleTon06() { super(); } static{ instance=new SingleTon06(); } public static SingleTon06 getInstance(){ return instance; } }
线程类测试类同三,结果如下:
六、使用枚举实现单例模式
因为枚举和静态代码块的特性有相似之处,因此也可以使用这种特性来设计单例模式,这种模式非常简单,也推荐时使用。代码如下:
public enum SingleTon07{ INSTANCE; private SingleTon07() { } public static SingleTon07 getInstance(){ return INSTANCE; } }
测试运行类同上,结果如下:
特点就是实现非常简单。