• 单例设计模式


    概念:单例对象的类必须保证只有⼀个实例存在。

    适⽤场景: 单例模式只允许创建⼀个对象,因此节省内存,加快对象访问速度,因此对象需要被公⽤的场合适合使⽤,如多个模块使⽤同⼀个数据源连接对象等等。如:

    1. 需要频繁实例化然后销毁的对象。
    2. 创建对象时耗时过多或者耗资源过多,但⼜经常⽤到的对象。
    3. 有状态的⼯具类对象。
    4. 频繁访问数据库或⽂件的对象。

    常⻅写法:

    public class Singleton {
        /**
         * 优点:没有线程安全问题,简单
         * 缺点:提前初始化会延⻓类加载器加载类的时间;如果不使⽤会浪费内存空间;不能传递参数
         */
        private static final Singleton instance = new Singleton();
        private Singleton(){};
        public static Singleton getInstance(){
            return instance;
            }
    }
    

    饿汉式:在实例化类的时候就会创建对象。

    优点:

    • 会提前初始化,你调用的时候就不需要再去初始化。性能是好的。
    • 没有线程安全问题,简单

    缺点:

    • 提前初始化会延⻓类加载器加载类的时间;
    • 如果不使⽤会浪费内存空间,因为,即使不用它,它也会在那;
    • 不能传递参数。
    public class Singleton{
        /**
        * 优点:解决线程安全,延迟初始化( Effective Java推荐写法)
        */
        private Singleton(){}
        public static Singleton getInstance () {
            return Holder.SINGLE_TON;
            }
        private static class Holder{
            private static final Singleton SINGLE_TON = new Singleton();
        }
    }
    

    懒汉式:延迟初始化的手段

    调用它的时候,会去初始化;不调用它的时候,就不初始化。

    优点:

    解决线程安全,延迟初始化( Effective Java推荐写法)

     public class Singleton {
         private volatile static Singleton uniqueSingleton;
         private Singleton() {
         }
         
         public Singleton getInstance() {
             if (null == uniqueSingleton) {//第一重检查
                 synchronized (Singleton.class) {//第一重加锁
                     if (null == uniqueSingleton) {//第二重检查
                         uniqueSingleton = new Singleton();//创建对象
                     }
                 }
             }
             return uniqueSingleton;
         }
     }        
    

    双重检查锁原理:

    第一次判断为空以后,第一次加锁。

    因为加锁不是一个原子操作,在加锁的过程中,uniqueSingleton对象可能已经被初始化掉了,所以加锁完毕以后,还需要去判断一下,uniqueSingleton对象是不是为空?

    加锁后,进行第二次检验(检查)

    如果uniqueSingleton对象为空,进行创建对象。

    注意:

    volatile

    • 会保证可见性
    • 不会保证原子性。

    具体原理:

    加了一个log的前置指令(即内存屏障),会保证屏障后面的指令不会放到屏障之前,也不会把屏障之前的指令放到屏障之后。在执行到屏障的时候,一定会保证屏障之前的指令全执行完了。

    进行写操作的时候:

    • 有缓存:写操作会导致缓存失效;
    • 无缓存:将缓存里的修改,直接写入到缓存里。

    volatile的作用:

    1. 阻止指令重排序;

      加入volatile,会加入一个内存屏障。加入内存屏障后顺序就不能变了。是禁止指令重排序。

    1. 保证可见性;

      平时操作时,会有一个主内存和一个工作内存的。

      如果不加volatile,数据是放在工作内存的,进行操作的时候,需要从工作内存刷到主内存。加了volatile,相当于不用工作内存,直接用主内存。相当于多个线程对一个对象操作的时候,多个线程对结果是可见的。

    为什么考虑指令重排序?

    volatile指令重排序

    在执⾏程序时,为了提供性能,处理器和编译器常常会对指令进⾏重排序,但是不能随意重排序,不是你 想怎么排序就怎么排序,它需要满⾜以下两个条件:

    1. 在单线程环境下不能改变程序运⾏的结果;
    2. 存在数据依赖关系的不允许重排序

    指令排序

    uniqueSingleton = new Singleton();
    
    1. 分配内存空间
    2. 初始化对象
    3. 将对象指向刚分配的内存空间

    但是有些编译器为了性能的原因,可能会将第⼆步和第三步进⾏重排序,顺序就成了:

    1. 分配内存空间
    2. 将对象指向刚分配的内存空间
    3. 初始化对象

    现在考虑重排序后,两个线程发⽣了以下调⽤:

    Time Thread A Thread B
    T1 检查到uniqueSingleton为空
    T2 获取锁
    T3 再次检查到uniqueSingleton为 空
    T4 为uniqueSingleton分配内存空 间
    T5 将uniqueSingleton指向内存空 间
    T6 检查到uniqueSingleton不为空
    T7 访问uniqueSingleton(此时对 象还未完成初始化)
    T8 初始化uniqueSingleton

    在这种情况下,T7时刻线程B对uniqueSingleton的访问,访问的是⼀个初始化未完成的对象。

    使⽤了volatile关键字后,重排序被禁⽌,所有的写(write)操作都将发⽣在读(read)操作之前。

    单例模式的破坏

    Singleton sc1 = Singleton.getInstance(); 
    Singleton sc2 = Singleton.getInstance(); 
    System.out.println(sc1); // sc1,sc2是同⼀个对象 
    System.out.println(sc2); 
    
    /*通过反射的⽅式直接调⽤私有构造器*/ 
    Class clazz = (Class) Class.forName("com.le arn.example.Singleton"); 
    Constructor c = clazz.getDeclaredConstructor(null); 
    c.setAccessible(true); // 跳过权限检查 
    Singleton sc3 = c.newInstance(); 
    Singleton sc4 = c.newInstance(); 
    System.out.println("通过反射的⽅式获取的对象sc3:" + sc3); // sc3,sc 4不是同⼀个对象  
    System.out.println("通过反射的⽅式获取的对象sc4:" + sc4); 
    //以上代码是说明单例是可以被破坏的
    
    //防⽌反射获取多个对象的漏洞 (即防止单例被破坏的解决办法。)
    private Singleton() { 
        if (null != SingletonClassInstance.instance) 
            throw new RuntimeException(); 
    }
    

    spring中bean的单例

    a. 当多个⽤户同时请求⼀个服务时,容器会给每⼀个请求分配⼀个线程,这时多个线程会并发执⾏该请求对应的业务逻辑(成员⽅法)。此时就要注意了,如果该处理逻辑中有对单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。

    有状态就是有数据存储功能有状态对象(Stateful Bean)就是有实例变量的对象,可以保存数据,是⾮线程安全的。在不同⽅法调⽤间不保留任何状态。

    ⽆状态就是⼀次操作,不能保存数据。⽆状态对象(Stateless Bean),就是没有实例变量的对象 . 不能保存数据,是不变类,是线程安全的。

    b. 实现

    public abstract class AbstractBeanFactory implements Configurable
        BeanFactory{
        /**
        * 充当了Bean实例的缓存,实现⽅式和单例注册表相同
        */
        private final Map singletonCache=new HashMap();
        public Object getBean(String name)throws BeansException{
            return getBean(name,null,null);
        }
        ...
            public Object getBean(String name,Class requiredType,Object[] args)throws BeansException{
            //对传⼊的Bean name稍做处理,防⽌传⼊的Bean name名有⾮法字符(或则做转码)
            String beanName=transformedBeanName(name);
            Object bean=null;
            //⼿⼯检测单例注册表
            Object sharedInstance=null;
            //使⽤了代码锁定同步块,原理和同步⽅法相似,但是这种写法效率更⾼
            synchronized(this.singletonCache){
                sharedInstance=this.singletonCache.get(beanName);
            }
            if(sharedInstance!=null){
                ...
                    //返回合适的缓存Bean实例
                    bean=getObjectForSharedInstance(name,sharedInstance);
            }else{...
                //取得Bean的定义
                RootBeanDefinition mergedBeanDefinition=getMergedBeanDefinition(beanName,false);
                  ...
                      //根据Bean定义判断,此判断依据通常来⾃于组件配置⽂件的单例属性开关
                      //<bean id="date" class="java.util.Date" scope="singleton"/>
                      //如果是单例,做如下处理
                      if(mergedBeanDefinition.isSingleton()){
                          synchronized(this.singletonCache){//再次检测单例注册表
                              sharedInstance=this.singletonCache.get(beanName);
                              if(sharedInstance==null){
                                  ...
                                      try {
                                          //真正创建Bean实例 
                                          sharedInstance=createBean(beanName,mergedBeanDefinition,args);
                                          //向单例注册表注册Bean实例
                                          addSingleton(beanName,sharedInstance);
                                      }catch (Exception ex) {
                                          ...
                                      }finally{
                                          ...
                                      }
                              }
                          }
                          bean=getObjectForSharedInstance(name,sharedInstance);
                      }
                  //如果是⾮单例,即prototpye,每次都要新创建⼀个Bean实例
                  //<bean id="date" class="java.util.Date" scope="prototype"/>
                  else{
                      bean=createBean(beanName,mergedBeanDefinition,args);
                  }
                 }
            ...
                return bean;
        }
    }
    

    编写不易,转载注明出处:https://www.cnblogs.com/lmh15054109/p/13861582.html

  • 相关阅读:
    redis实时同步工具redis-shake
    elasticsearch单机部署
    ogg从库进程监控
    mha安装部署
    mha自定义路径安装
    gnuplot输出柱状图
    gnuplot输出曲线图
    gnuplot命令行模式不支持中文标题的解决办法
    ogg中logdump使用自动输入执行
    JDK(java se development kit)的构成
  • 原文地址:https://www.cnblogs.com/lmh15054109/p/13861582.html
Copyright © 2020-2023  润新知