单列模式与多线程

  在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;
    }
    
}

测试运行类同上,结果如下:

  特点就是实现非常简单。

 

posted @ 2018-12-14 11:30  我心自在  阅读(787)  评论(0编辑  收藏  举报