美文网首页
8中单例模式的写法

8中单例模式的写法

作者: 前度天下 | 来源:发表于2020-05-26 13:57 被阅读0次
    第一种:采用静态内部类的写法
    public class Singleton {
        private static class SingletonHandler {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        //默认构造器弄成私有,禁止外部调用new
        private Singleton() {
        }
    
        public static final Singleton getInstance(){
            return SingletonHandler.INSTANCE;
        }
    }
    
    第二种:饿汉模式显示单例模式
    public class Singleton {
        private static Singleton instance = new Singleton();
    
        //默认构造器弄成私有,禁止外部调用new
        private Singleton() {
        }
    
        public static Singleton getInstance(){
            return instance;
        }
    }
    
    第三种:饿汉变种实现单例模式
    public class Singleton {
        private static Singleton instance = null;
    
        static {
            instance = new Singleton();
        }
    
        //默认构造器弄成私有,禁止外部调用new
        private Singleton() {
        }
    
        public static Singleton getInstance(){
            return instance;
        }
    }
    

    以上三种方式都是通过定义静态的成员变量,以保证单例对象可以在类初始化的过程中被实例化。
    以上三种有个共同特点:

    • 构造器私有化
    • 静态属性私有化
    • 获取实例的方法共有

    这其实是利用了ClassLoader的线程安全机制。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。

    所以, 除非被重写,这个方法默认在整个装载过程中都是线程安全的。所以在类加载过程中对象的创建也是线程安全的。

    这里就引发了另一个问题类加载机制。需要另一篇文章做支撑。

    上面这三种情况不是所有的情况下是线程安全的,如果采用反射机制是可以调用私有构造器的

    public class Test {
        public static void main(String[] args) throws Exception {
            Singleton singleton = Singleton.getInstance();
            Singleton instance = Singleton.getInstance();
            System.out.println(singleton == instance);
            
            Constructor<Singleton> singletonConstructor = Singleton.class.getDeclaredConstructor();
            singletonConstructor.setAccessible(true);
            Singleton ref = singletonConstructor.newInstance();
            System.out.println(singleton == ref);
        }
    }
    

    通过反射的方式,创建出来的Singleton实例就不是单例的了。

    还有一个情况就是序列化之后也不是单例

    public class Singleton implements Serializable {
        private static Singleton instance = null;
    
        static {
            instance = new Singleton();
        }
    
        //默认构造器弄成私有,禁止外部调用new
        private Singleton() {
        }
    
        public static Singleton getInstance(){
            return instance;
        }
    }
    
    public class Test {
        public static void main(String[] args) throws Exception {
            Singleton instance = Singleton.getInstance();
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.obj"));
            outputStream.writeObject(instance);
            outputStream.flush();
            outputStream.close();
    
            FileInputStream inputStream = new FileInputStream("test.obj");
            ObjectInputStream inputStream1 = new ObjectInputStream(inputStream);
            Singleton instance1 = (Singleton)inputStream1.readObject();
            inputStream1.close();
            inputStream.close();
    
            System.out.println(instance == instance1);
        }
    }
    

    任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。
    那有没有方法可以防止反射攻击和序列化破坏单例模式呢?

    public class  Singleton implements Serializable{
        private static final long serialVersionUID = -4264591697494981165L;
    
        // 静态内部类
        private static class SingletonHandler {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        private Singleton(){
            // 防止反射创建多个对象
            if(SingletonHandler.INSTANCE!=null){
                throw new RuntimeException("只能实例化一次");
            }
        }
    
        public static final Singleton getInstance(){
            return SingletonHandler.INSTANCE;
        }
        // 防止序列化创建多个对象,这个方法是关键
        private Object readResolve(){
            return SingletonHandler.INSTANCE;
        }
    }
    

    通过这种方式就可以防止反射攻击和序列化问题

    第四种写法:枚举类单例
    public enum  EnumSingleton {
        INSTANCE;
        public EnumSingleton getInstance(){
            return INSTANCE;
        }
    }
    

    这个写法好简单,枚举类型其实是继承了Enum抽象类的,而抽象类中是没有无参构造器,所以不管你反射的时候调用无参构造器,还是调用父类的有参构造器都会抛出异常,这样就避免了反射攻击。也解决了序列化问题。
    参考:https://www.cnblogs.com/chiclee/p/9097772.html

    枚举类是JDK1.5才出现的,那之前的程序员面对反射攻击和序列化问题是怎么解决的呢?其实就是像Enum源码那样解决的,只是现在可以用enum可以使我们代码量变的极其简洁了。
    Joshua Bloch说的“单元素的枚举类型已经成为实现Singleton的最佳方法”

    这里序列化Serializable ,实现了这个接口之后,所以的方法和属性都自动序列化了,有时候我们不需要对所有的属性都进行序列化,所以就引出了transient关键字。打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。

    1.一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。

    2.transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。

    3.被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。

    参考:https://www.cnblogs.com/lanxuezaipiao/p/3369962.html

    所以第四种单例模式是线程安全的。原因就是枚举其实底层是依赖Enum类实现的,这个类的成员变量都是static类型的,并且在静态代码块中实例化的,和饿汉有点像, 所以他天然是线程安全的。

    第五种双重检查的方式(俗称Double Check方式)实现单例模式
    第六种使用同步代码块(效率很低)
    public class Singleton {  
        private static Singleton instance=null;  
         
        private Singleton() {  
             
        }  
         
        public synchronized static Singleton getInstance(){  
            if (instance == null) {  
                instance = new Singleton();  
            }  
             
            return instance;  
        }  
    }
    
    第七种使用CAS(非阻塞方式也叫乐观锁)实现单例模式
    public class  Singleton{
        private static final AtomicReference<Singleton> INSTANCE= new AtomicReference<Singleton>();
    
        private Singleton(){}
    
        public static final Singleton getInstance(){
            for(;;){
                Singleton current = INSTANCE.get();
                if(current != null){
                    return current;
                }
                current = new Singleton();
                if(INSTANCE.compareAndSet(null,current)){
                    return current;
                }
            }
        }
    }
    

    用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。

    CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。
    另外,代码中,如果N个线程同时执行到 singleton = new Singleton();的时候,会有大量对象被创建,可能导致内存溢出。

    那这里就引出了另外一个问题就是CAS原理

    什么是CAS? CAS:Compare and Swap,即比较再交换。

    jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

    参考:https://www.jianshu.com/p/ab2c8fce878b

    第八种ThreadLocal(以空间换时间方式)实现单例模式

    ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制(synchronized)采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。

    同步机制仅提供一份变量,让不同的线程排队访问,而ThreadLocal为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

    public class  Singleton{
        private static final ThreadLocal<Singleton> singleton = new ThreadLocal<Singleton>(){
            @Override
            protected Singleton initialValue() {
                return new Singleton();
            }
        };
    
        private Singleton(){}
    
        public static Singleton getInstance(){
            return singleton.get();
        }
    }
    

    ThreadLocal 扩展知识:参考https://www.jianshu.com/p/3c5d7f09dfbd

    相关文章

      网友评论

          本文标题:8中单例模式的写法

          本文链接:https://www.haomeiwen.com/subject/gygmahtx.html