zoukankan      html  css  js  c++  java
  • 设计模式之单例模式

    设计模式之单例模式

    单例模式(singleton)是设计模式中最简单的模式之一,它作用于单个类,且该类只有一个实例对象。

    单例模式的实现有很多种:懒汉,饿汉,加锁,双重加锁等,接下来我将一一说明每种方式的实现。

    饿汉模式:

    /**
     * 饿汉式:不管你是否需要我这个实例,在我类加载的时候都给你初始化出来。
     * 类加载到内存后,就实例化一个单例,JVM保证线程安全
     * 简单实用,推荐使用!
     * 唯一缺点:不管用到与否,类装载时就完成实例化
     */
    public class Mgr01 {
        private static final Mgr01 INSTANCE = new Mgr01();
    //将构造方法私有化,其他类无法调用该类的构造方法无法对其进行初始化。
    private Mgr01() {} public static Mgr01 getInstance() { return INSTANCE; } //进行验证,判断m1,m2是否为同一个对象 public static void main(String[] args) { Mgr01 m1 = Mgr01.getInstance(); Mgr01 m2 = Mgr01.getInstance(); System.out.println(m1 == m2); } }

     

    懒汉模式

    /**
     * lazy loading
     * 也称懒汉式
     * 在需要实例时,才进行初始化。
     * 虽然达到了按需初始化的目的,但却带来一些的问题
     */
    public class Mgr03 {
        private static Mgr03 INSTANCE;
    
        private Mgr03() {
        }
    
        public static Mgr03 getInstance() {
            //有可能线程A,B同时进行了判断,均为null,然后线程A,B都进行了初始化的操作,这样就生成了两个实例,与单例模式不符。
            if (INSTANCE == null) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Mgr03();
            }
            return INSTANCE;
        }
    
        //验证,一个类中的同一个对象的hashcode是相同的,
        public static void main(String[] args) {
            for(int i=0; i<100; i++) {
                new Thread(()->
                        System.out.println(Mgr03.getInstance().hashCode())
                ).start();
            }
        }
    }

    这是Mgr03的运行结果,可以看出这种写法会造成实例对象的不一致,不符合单例模式的要求。


    懒汉模式不安全,对方法进行加锁可避免这个问题

    /**
     * lazy loading
     * 也称懒汉式
     * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
     * 可以通过synchronized解决,但也带来效率下降
     */
    public class Mgr04 {
        private static Mgr04 INSTANCE;
    
        private Mgr04() {
        }
    
        public static synchronized Mgr04 getInstance() {
            if (INSTANCE == null) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Mgr04();
            }
            return INSTANCE;
        }
    
        public static void main(String[] args) {
            for(int i=0; i<100; i++) {
                new Thread(()->{
                    System.out.println(Mgr04.getInstance().hashCode());
                }).start();
            }
        }
    }

    结果如图:

    通过给getInstance方法进行synchronized的方法来实现线程安全,但是性能方法会被大打折扣。


    这时候有人想让锁的粒度更细一些,来增加性能,代码如下:

    /**
     * lazy loading
     * 也称懒汉式
     * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
     * 可以通过synchronized解决,但也带来效率下降
     */
    public class Mgr05 {
        private static Mgr05 INSTANCE;
    
        private Mgr05() {
        }
    
        public static Mgr05 getInstance() {
            if (INSTANCE == null) {
                //妄图通过减小同步代码块的方式提高效率,然而会出现和不加锁的懒汉一样问题
                //线程A,B同时判断了INSTANCE为null,A获得了锁,new了一个出来,B等待A释放锁之后又new了一个出来。
                synchronized (Mgr05.class) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr05();
                }
            }
            return INSTANCE;
        }
    
        public static void main(String[] args) {
            for(int i=0; i<100; i++) {
                new Thread(()->{
                    System.out.println(Mgr05.getInstance().hashCode());
                }).start();
            }
        }
    }

    这种减少同步代码块的方法,可以找出来问题,就是A,B线程有可能同时判断为null,A获得锁创建完对象后,由于B已经判断过INSTANCE为null,所以当B获得锁之后直接创建对象,造成了线程安全问题。既然已经知道了原因,所以在获得锁之后再加上一层判断即可,代码如下:

    /**
     * lazy loading
     * 也称懒汉式
     * 通过双重判断来解决线程安全问题。
     */
    public class Mgr06 {
    //这里加上valatile是为了防止指令重排,
    private static volatile Mgr06 INSTANCE; //JIT private Mgr06() { } public static Mgr06 getInstance() {
    //这里必须加上判断,可以减少后面线程获取,判断不为null,直接往下走,不需要每一件线程都去争取锁。
    if (INSTANCE == null) { //双重检查 synchronized (Mgr06.class) { if(INSTANCE == null) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Mgr06(); } } } return INSTANCE; } public static void main(String[] args) { for(int i=0; i<100; i++) { new Thread(()->{ System.out.println(Mgr06.getInstance().hashCode()); }).start(); } } }

    还有两种比较少见的方法:静态内部类的方式和枚举的方式。

    静态内部类

    /**
     * 静态内部类方式
     * JVM保证单例
     * 加载外部类时不会加载内部类,只有调用getInstance方法时,才会进行加载。这样可以实现懒加载
     */
    public class Mgr07 {
    
        private Mgr07() {
        }
    
        private static class Mgr07Holder {
            private final static Mgr07 INSTANCE = new Mgr07();
        }
    
        public static Mgr07 getInstance() {
            return Mgr07Holder.INSTANCE;
        }
    
    
        public static void main(String[] args) {
            for(int i=0; i<100; i++) {
                new Thread(()->{
                    System.out.println(Mgr07.getInstance().hashCode());
                }).start();
            }
        }
    
    
    }

    这个方法的线程安全是由JVM保证的。


    枚举的方法:

    /**
     * 不仅可以解决线程同步,还可以防止反序列化。
     */
    public enum Mgr08 {
    
        INSTANCE;
    
        public void m() {}
    
        public static void main(String[] args) {
            for(int i=0; i<100; i++) {
                new Thread(()->{
                    System.out.println(Mgr08.INSTANCE.hashCode());
                }).start();
            }
        }

    这是最牛逼的方法,也是最简单的方法。是《Effective Java》的作者书中说明的一种实现单例的方法。

    总结

    1. 单例模式实现的思想就是构造方法私有化。
    2. 实际开发中饿汉模式就足以满足。不需要很复杂的二重判断,做到心中有剑,手中无剑。
    3. 枚举是最好的实现单例的方式。
    4. 使用双重检查实现单例时,必须交上volatile关键字,防止指令重排。
  • 相关阅读:
    如何查看ipynb文件
    使用python绘制爱心
    使用python将十进制数转为ip地址
    python使用下划线分割数字,提高可读性
    python的字符串基本操作
    pandas为csv添加新的行和列
    使用pandas库实现csv行和列的获取
    pycharm批量更改变量名
    (转)Doxygen文档生成工具
    MVC架构学习之Smarty学习——病来而蔫
  • 原文地址:https://www.cnblogs.com/zeminzhang/p/14007306.html
Copyright © 2011-2022 走看看