程序员Java学习笔记技术干货

设计模式(一)--深入单例模式(涉及线程安全问题)

2017-03-26  本文已影响190人  JackFrost_fuzhu

这又是一个新的系列啦,探究各大设计模式在开发中必须注意思考的一些问题,以及它们的多向使用。

文章结构:(1)单例模式概念以及优缺点(2)各式各样的单例及其线程安全问题。(3)使用推荐。


单例模式概念以及优缺点:

(1)定义:

要求一个类只能生成一个对象,所有对象对它的依赖相同。

(2)优点:

1. 只有一个实例,减少内存开支。应用在一个经常被访问的对象上

2. 减少系统的性能开销,应用启动时,直接产生一单例对象,用永久驻留内存的方式。

3.避免对资源的多重占用

4.可在系统设置全局的访问点,优化和共享资源访问。

(3)缺点:

1.一般没有接口,扩展困难。原因:接口对单例模式没有任何意义;要求“自行实例化”,并提供单一实例,接口或抽象类不可能被实例化。(当然,单例模式可以实现接口、被继承,但需要根据系统开发环境判断)

2.单例模式对测试是不利的。如果单例模式没完成,是不能进行测试的。

3.单例模式与单一职责原则有冲突。原因:一个类应该只实现一个逻辑,而不关心它是否是单例,是不是要单例取决于环境;单例模式把“要单例”和业务逻辑融合在一个类。

(4)使用场景:

1.要求生成唯一序列化的环境

2.项目需要的一个共享访问点或共享的数据点

3.创建一个对象需要消耗资源过多的情况。如:要访问IO和 数据库等资源。

4.需要定义大量的静态常量和静态方法(如工具类)的环境。可以采用单例模式或者直接声明static的方式。

(5)注意事项:

1.类中其他方法,尽量是static

2.注意JVM的垃圾回收机制。

如果一个单例对象在内存长久不使用,JVM就认为对象是一个垃圾。所以如果针对一些状态值,如果回收的话,应用就会出现故障。

3.采用单例模式来记录状态值的类的两大方法:

(一)、由容器管理单例的生命周期。Java EE容器或者框架级容器,自行管理对象的生命周期。
(二)状态随时记录。异步记录的方式或者使用观察者模式,记录状态变化,确保重新初始化也可从资源环境获得销毁前的数据。

二、各式各样的单例及其线程安全问题:

(1)懒汉式单例:

意思:就是需要使用这个对象的时候才去创建这个对象。

//懒汉式单例
public class Singleton1 {
    private static Singleton1 singleton1=null;
    public Singleton1(){

    }
    public static Singleton1 getInstance(){
        if (singleton1==null){
            try {
                Thread.sleep(200);//我们知道初始化一个对象需要一定时间的嘛,我们用sleep假设这个时间
                singleton1 = new Singleton1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return singleton1;
    }
}
//测试线程
public class SingleThread1 extends Thread {
//哈希值对应的是唯一的嘛,如果不一样了,就说明使用的不是同一个对象咯。
    @Override
    public void run() {
        System.out.println(Singleton1.getInstance().hashCode());
    }

}

//测试类
public class SingletonTest {

    public static void main(String []args){
        SingleThread1[] thread1s = new SingleThread1[10];
        for (int i= 0;i<thread1s.length;i++){
            thread1s[i] = new SingleThread1();
        }
        for (int j = 0; j < thread1s.length; j++) {
            thread1s[j].start();
        }
    }
}
//打印的结果:
569219718
1259146238
565373737
732830316
679555294
1886445805
1557403724
635681435
622018771
1439317371

线程安全的懒汉式单例设计:

1.锁住获取方法方式:

public class Singleton3 {
    private static Singleton3 instance = null;

    private Singleton3(){}
    //锁住获取方法的方式
    public synchronized static Singleton3 getInstance() {
        try {
            if(instance != null){
            }else{
                Thread.sleep(300);
                instance = new Singleton3();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }
}

2.锁住部分代码块的方式:

public class Singleton2 {
    private static Singleton2 instance = null;
     private Singleton2(){
     }
    public static Singleton2 getInstance() {
        try {
            //锁住代码块的方式
            synchronized (Singleton2.class) {
                if(instance != null){

                }else{
                    Thread.sleep(200);
                    instance = new Singleton2();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }
}

3.锁住初始化对象操作的方式:但是!!!这不是线程安全的!!一会有这个方式的优化从而实现线程安全。

为什么??

因为多个访问已经进入到创建的那里了。

public class Singleton4 {
    private static Singleton4 instance = null;
    private Singleton4(){}
    public static Singleton4 getInstance() {
        try {
            if(instance != null){
            }else{
                Thread.sleep(300);
                //只锁住初始化操作的方式
                synchronized (Singleton4.class) {
                    instance = new Singleton4();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }
}

4.锁住初始化对象操作的方式,但有个再检查操作:

public class Singleton5 {
    //使用volatile关键字保其可见性
    volatile private static Singleton5 instance = null;
    private Singleton5(){}
    public static Singleton5 getInstance() {
        try {
            if(instance != null){
            }else{
                Thread.sleep(300);
                //锁住初始化操作的方式
                synchronized (Singleton5.class) {
                    if(instance == null){//二次检查
                        instance = new Singleton5();
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }
}

使用了volatile关键字来保证其线程间的可见性;在同步代码块中使用二次检查,以保证其不被重复实例化。集合其二者,这种实现方式既保证了其高效性,也保证了其线程安全性。

解析volatile在此的作用:

volatile(涉及java内存模型的知识)会禁止CPU对内存访问重排序(并不一定禁止指令重排),也就是CPU执行初始化操作,那么他会保证其他CPU看到的操作顺序是1.给 instance 分配内存--2.调用 Singleton 的构造函数来初始化成员变量--3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了),(虽然在CPU内由于流水线多发射并不一定是这个顺序)

不使用volatile的问题是什么呢??

在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

用volatile的意义并不在于其他线程一定要去内存总读取instance,而在于它限制了CPU对内存操作的重拍序,使其他线程在看到3之前2一定是执行过的。


(2)饿汉式单例:

意思是:类装载时就实例化该单例类

public class Singleton6 {
    //一初始化类就初始化这个单例了!!!
    private static Singleton6 singleton6= new Singleton6();
    private Singleton6(){

    }
    public static Singleton6 getInstance(){
        return singleton6;
    }
}

基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。这个是没有懒加载的功能的!!!

饿汉式单例变种:

public class Singleton7 {
    private static Singleton7 instance = null;
    static {
        instance = new Singleton7();
    }
    private Singleton7() {
    }
    public static Singleton7 getInstance() {
        return instance;
    }
}

(3)静态内部类实现懒加载:

//静态内部类单例
public class Singleton8 {
    private static class SingletonHolder {
        private static final Singleton8 INSTANCE = new Singleton8();
    }
    private Singleton8 (){}
    public static final Singleton8 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟饿汉式的两种方式不同的是:饿汉式的两种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance还未被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。

静态内部类方式单例再度研究:序列化和反序列化问题:

public class MySingleton implements Serializable {

    private static final long serialVersionUID = 1L;

    //内部类
    private static class MySingletonHandler{
        private static MySingleton instance = new MySingleton();
    }

    private MySingleton(){}

    public static MySingleton getInstance() {
        return MySingletonHandler.instance;
    }
}
public class SaveAndReadForSingleton {
    public static void main(String[] args) {
        MySingleton singleton = MySingleton.getInstance();
        //创建个文件流
        File file = new File("MySingleton.txt");
        //使用节点流,直接与文件关联
        try {
            //写入文件
            FileOutputStream fos = new FileOutputStream(file);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(singleton);
            fos.close();
            oos.close();
            System.out.println(singleton.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            //读取文件流
            FileInputStream fis = new FileInputStream(file);
            ObjectInputStream ois = new ObjectInputStream(fis);
            MySingleton rSingleton = (MySingleton) ois.readObject();
            fis.close();
            ois.close();
            System.out.println(rSingleton.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

这样的单例测试出来时,hash是不一样的,因为没有同步到序列化与反序列化问题。说明反序列化后返回的对象是重新实例化的,单例被破坏了。

解决:当JVM从内存中反序列化地"组装"一个新对象时,就会自动调用readResolve方法来返回我们指定好的对象,readResolve允许class在反序列化返回对象前替换、解析在流中读出来的对象。实现readResolve方法,一个class可以直接控制反序化返回的类型和对象引用。

public class MySingleton1 implements Serializable {

    private static final long serialVersionUID = 1L;

    //内部类
    private static class MySingletonHandler{
        private static MySingleton1 instance = new MySingleton1();
    }

    private MySingleton1(){}

    public static MySingleton1 getInstance() {
        return MySingletonHandler.instance;
    }

    //该方法在反序列化时会被调用,该方法不是接口定义的方法,有点儿约定俗成的感觉
    protected Object readResolve() throws ObjectStreamException {
        System.out.println("调用了readResolve方法!");
        return MySingletonHandler.instance;
    }
}

修改SaveAndReadForSingleton文件中的MySingleton,输出

2133927002
调用了readResolve方法!解决序列化与反序列化问题!
2133927002

(4)枚举:

//枚举实现单例
public enum EnumSingletonFactory {
    singletonFactory;
    private EnumSingleton instance;
    private EnumSingletonFactory(){//枚举类的构造方法在类加载是被实例化
        instance = new EnumSingleton();
    }
    public EnumSingleton getInstance(){
        return instance;
    }
}

在thread中调用实现:

@Override  
    public void run() {     System.out.println(EnumFactory.singletonFactory.getInstance().hashCode());  
    }

但是此博客 引起我思考,是违反单一职责的,因为它暴露了枚举的细节,所以我们需要改造他。

//使用工厂来生成枚举类
//通过工厂类的静态方法去访问枚举类,然后通过枚举类访问它的单例。
public class ClassFactory {
    private enum MyEnumSingleton{
        singletonFactory;

        private EnumSingleton instance;

        private MyEnumSingleton(){//枚举类的构造方法在类加载是被实例化
            instance = new EnumSingleton();
        }

        public EnumSingleton getInstance(){
            return instance;
        }
    }

    public static EnumSingleton getInstance(){
        return MyEnumSingleton.singletonFactory.getInstance();
    }
}

在thread中调用实现:

 @Override  
    public void run() {   
        System.out.println(ClassFactory.getInstance().hashCode());  
    }  

枚举类的方式不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。不过实际工程代码中,很少去用此方式。


三、推荐使用:

上述的各种单例都讲完了:基本是五种写法。懒汉,恶汉,双重校验锁,枚举和静态内部类。

(1)饿汉式单例。

原因:类的加载机制保证了,类初始化时,只执行一次静态代码块以及类变量初始化。直接保证了唯一性,保证了线程安全。(一般使用非静态代码块方式)

(2)静态内部类方式:

原因:懒加载呗!!!应用在一些十分巨大的单例bean中。


参考博客:此博客让我对单例加深了一大层,感谢感谢!!

http://blog.csdn.net/cselmu9/article/details/51366946


好了,设计模式(一)--深入单例模式(涉及线程安全问题)讲完了。本博客是我复习阶段的一些笔记,拿来分享经验给大家。欢迎在下面指出错误,共同学习!!你的点赞是对我最好的支持!!

更多内容,可以访问JackFrost的博客

上一篇下一篇

猜你喜欢

热点阅读