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

    定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    我们可以通过定义一个全局变量给不同的客户端调用,使得不同客户端获取到的都是同一个对象,但是这并不能防止客户端去实例化多个对象,想要保证一个类仅有一个实例,最好的办法就是让类自身保存一个唯一的实例,这个类不仅要能保证客户端不能通过new来创建该类一个实例,即用private来修饰构造方法,还要提供一个获取该类唯一实例的方法。从这几个条件我们可以给出单例类的一种实现方式,代码如下

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

    以上这种实现方法有一个明显的缺陷,就是在多线程的情况下将不能保证只实例化一个对象,因为判断singleton为null和初始化singleton并不是原子操作,可能在初始化singleton前有多个线程执行了条件判断,这几个线程就都会进入到if内,这几个线程都会新建一个实例,违背了定义中仅有一个实例的原则,通过以下测试代码来验证这一缺陷

    Set<Singleton> singletons = Collections.synchronizedSet(new HashSet<>());
    ExecutorService es = Executors.newCachedThreadPool();
    CyclicBarrier barrier = new CyclicBarrier(1000);
    for (int i = 0; i < 1000; i++) {
        es.execute(() -> {
            try {
                barrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            singletons.add(Singleton.getInstance());
        });
    }
    Thread.sleep(5000);
    for (Singleton singleton : singletons) {
        System.out.println(singleton);
    }

    运行以上代码有可能会输出如下图所示的结果

    多个线程同时调用单例类中获取实例的方法,并存到一个Set集合中进行去重后获取到了三个不同的实例(也有可能是一个或者更多个)

    为了避免多线程下会出现多个实例的问题,可以在getInstance方法上增加synchronized,不过这样虽然能解决上述问题,但是当某个线程在执行该方法的时候,其他的线程都将处于阻塞状态,这会严重影响代码的效率,我们可以在方法中使用同步代码块来减少同步的代码量,以下是改造后的getInstance方法

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

     以上代码中使用了两层判断,第一层判断是在同步代码块以外,当已经初始化好一个实例时,多个线程就能一起获取该实例,而第二层判断是在同步代码块内,是让那些通过第一层判断的线程同步执行,避免出现实例化多个对象的问题。这种加锁的方法看上去没有问题,但是JVM在创建一个新对象的时候是需要三个步骤的

    1、分配内存

    2、初始化构造器

    3、将对象指向分配的内存的地址

    若按以上步骤执行也不会有问题,但是JVM会针对字节码进行调优,有可能会将2,3对调执行,那就有可能会出现一个线程还未初始化构造器的时候,另一个线程获取了该实例并使用,将会出现错误信息。当然这种情况也只是可能会发生,可以通过volatile来修饰属性singleton,禁止JVM指令重排序优化。

    还有一种使用内部类的方法来实现单例模式,代码如下

    public class Singleton {
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            return InnerSingleton.singleton;
        }
    
        private static class InnerSingleton {
            static Singleton singleton = new Singleton();
        }
    }

    一个类的静态属性只会在第一次加载类的时候初始化,所以singleton是单例的,并且在初始化完之前其他线程是无法被调用的。

    还有一种饿汉式加载的实现方法

    public class Singleton {
    
        private static Singleton singleton = new Singleton();
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            return singleton;
        }
    }

    这种方法在第一次加载类的时候就会初始化好一个单例,若我们只想使用Singleton类中其他的功能,并不需要获取实例,那将会造成内存的浪费。不过像那些在项目中必须要用到的实例,比如加载配置文件的类,就可以使用这种方式。

    完整代码

  • 相关阅读:
    右下角老弹出盗版提示,以及登录界面出现正版验证对话框
    动态TSQL语句常見問題與解決方案
    验证码
    远程调用存储过程
    windows powershell
    屏蔽IE浏览器的刷新(不包括单个刷新按钮)
    获取数据库中的数据库有多少个
    网页刷新方法集合
    Win7中IIS7安装配置
    sql 根据字段查表名
  • 原文地址:https://www.cnblogs.com/guohaien/p/10484813.html
Copyright © 2011-2022 走看看