zoukankan      html  css  js  c++  java
  • 设计模式(五)——单例模式

    设计模式(五)——单例模式

    本文主要介绍单例模式的概念、实现方式,并介绍高并发如何保证单例模式的线程安全?如何保证序列化后的单例对象在反序列化后任然是单例?

    什么是单例模式?

    定义:单例模式是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种模式方法。

    从定义中,我的理解:

    在任何情况下,单例类永远只有一个实例存在

    单例需要有能力为整个系统提供这一唯一实例

    单例的类负责创建自己的对象,类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象

    单例模式的优势:

    在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存)。

    避免对资源的多重占用(比如写文件操作)。

    单例模式实现

    1.饿汉式单例

    饿汉式单例是指在方法调用前,实例就已经创建好了。下面是实现代码:

    public class Singleton {
        //在类内部实例化一个实例
        private static Singleton instance = new Singleton();
        //私有的构造函数,外部无法访问
        private Singleton() {
        }
        //对外提供获取实例的静态方法
        public static Singleton getInstance() {
            return instance;
        }
    }
    

    由于该类在类加载的时候就已创建出来了,所以避免了线程安全问题。

    这种饿汉式实现,会有一些问题:

    饿汉式单例,在类被加载的时候对象就会实例化。这也许会造成不必要的消耗,因为有可能这个实例根本就不会被用到。而且,如果这个类被多次加载的话也会造成多次实例化。

    解决的方法就是静态内部类和懒汉式

    2.懒汉式单例

    懒汉式单例是指在方法调用获取实例时才创建实例,因为相对饿汉式显得“不急迫”,所以被叫做“懒汉模式”。下面是实现代码:

    public class Singleton {
        //定义实例
        private static Singleton instance;
        //私有构造方法
        private Singleton(){}
        //对外提供获取实例的静态方法
        public static Singleton getInstance() {
            //在对象被使用的时候才实例化
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    上述实现很容易看出存在线程安全问题。在多线程的环境中,2个线程同时进入if判断,就会创建2个不同的对象。

    3.线程安全的懒汉式单例

    针对线程不安全的懒汉式的单例,其实解决方式很简单,就是给创建对象的步骤加锁:

    同步方法

    public class Singleton {
        //定义实例
        private static Singleton instance;
        //私有构造方法
        private Singleton(){}
        //对外提供获取实例的静态方法,对该方法加锁
        public static synchronized Singleton getInstance() {
            //在对象被使用的时候才实例化
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    这种写法能够在多线程中很好的工作,而且看起来它也具备很好的延迟加载,但是,遗憾的是,他效率很低,因为99%情况下不需要同步。(因为上面的synchronized的加锁范围是整个方法,该方法的所有操作都是同步进行的,但是对于非第一次创建对象的情况,也就是没有进入if语句中的情况,根本不需要同步操作,可以直接返回instance。)

    同步代码块

    public class Singleton {
        //定义实例
        private static Singleton instance;
        //私有构造方法
        private Singleton(){}
        //对外提供获取实例的静态方法,对该方法加锁
        public static Singleton getInstance() {
            synchronized (Singleton.class){
            	//在对象被使用的时候才实例化
            	if (instance == null) {
                	instance = new Singleton();
            	}
            }
            return instance;
        }
    }
    

    这种写法能够在多线程中很好的工作,但是这样代码块被锁定了,同样效率低。

    重要代码同步

    public class Singleton {
        //定义实例
        private static Singleton instance;
        //私有构造方法
        private Singleton(){}
        //对外提供获取实例的静态方法,对该方法加锁
        public static synchronized Singleton getInstance() {
            //在对象被使用的时候才实例化
            if (instance == null) {
                synchronized (Singleton.class){
                	instance = new Singleton();
                }
            }
            return instance;
        }
    }
    

    运行效率提高了,但是却没能保证线程的同步。

    双重校验锁

    public class Singleton {
    
        private static Singleton singleton;
    
        private Singleton() {}
    
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    通过使用同步代码块的方式减小了锁的范围。这样可以大大提高效率。(对于已经存在singleton的情况,无须同步,直接return)。

    上面的代码看似没有什么问题,实现了惰性初始化,解决了同步问题,还减小了锁的范围,提高了效率。但是,还是存在隐患。隐患的原因主要与java内存模型有关。

    线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。
    由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。
    线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有穿过B使用的内存(缓存一致性)),程序很可能会崩溃。

    volatile双重校验锁

    public class Singleton {
    
        private static volatile Singleton singleton;
    
        private Singleton() {}
    
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    上面这种双重校验锁的方式用的比较广泛,他解决了前面提到的所有问题。但是,即使是这种看上去完美无缺的方式也可能存在问题,那就是遇到序列化的时候。

    4.静态内置类实现单例模式

    public class StaticInnerClassSingleton {
        //在静态内部类中初始化实例对象
        private static class SingletonHolder {
            private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
        }
        //私有的构造方法
        private StaticInnerClassSingleton() {
        }
        //对外提供获取实例的静态方法
        public static final StaticInnerClassSingleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    

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

    5.使用static代码块实现单例

    public class Singleton2 {
        //在类内部定义
        private static Singleton2 instance;
        static {
            //实例化该实例
            instance = new Singleton2();
        }
        //私有的构造函数,外部无法访问
        private Singleton2() {
        }
        //对外提供获取实例的静态方法
        public static Singleton2 getInstance() {
            return instance;
        }
    }
    
    

    这个方法和饿汉式差不多

    6.序列化与反序列化的单例模式实现

    要想防止序列化对单例的破坏,只要在Singleton类中定义readResolve就可以解决该问题:

    public class Singleton implements Serializable{
        private volatile static Singleton singleton;
        private Singleton (){}
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    
        private Object readResolve() {
            return singleton;
        }
    }
    

    7.枚举模式实现单例

    要想防止序列化对单例的破坏,只要在Singleton类中定义readResolve就可以解决该问题:

    public enum  Singleton {
        INSTANCE;
        Singleton() {
        }
    }
    

    这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象(下面会介绍),可谓是很坚强的壁垒啊,在深度分析Java的枚举类型—-枚举的线程安全性及序列化问题中有详细介绍枚举的线程安全问题和序列化问题,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过,但是不代表他不好。

    总结

    本文中介绍了几种实现单例的方法,主要包括饿汉、懒汉、使用静态内部类、双重校验锁、枚举等。还介绍了如何防止序列化破坏类的单例性。

    从单例的实现中,我们可以发现,一个简单的单例模式就能涉及到这么多知识。在不断完善的过程中可以了解并运用到更多的知识。所谓学无止境。

    参考文档

    http://www.hollischuang.com/archives/1373
    http://blog.csdn.net/cselmu9/article/details/51366946

  • 相关阅读:
    Ubuntu的shell之bash和dash
    Linux下烧写工具DNW和USB驱动安装(一)
    make -C M=
    uname -r和uname -a了解
    如何添加Samba用户
    Ubuntu下配置samba实现文件夹共享
    [Jenkins]运行shell报错:寻找匹配的 `"' 是遇到了未预期的文件结束符
    [Shell] 调试shell脚本的技巧 | 校验shell脚本语法 |寻找匹配的 `"' 是遇到了未预期的文件结束符
    [Python]通过python-jenkins操作jenkins slave启动job | 通过python-jenkins实现ios自动化打包接口
    pod: command not found
  • 原文地址:https://www.cnblogs.com/yy1024/p/5703608.html
Copyright © 2011-2022 走看看