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

      在学习单例模式前,我们首先要了解两个问题。

      1、单例模式有哪些作用

      第一、控制资源的使用,通过线程同步来控制资源的并发访问;第二、控制实例产生的数量,达到节约资源的目的。第三、作为通信媒介使用,也就是数据共享,它可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程之间实现通信。

      2、什么时候需要使用单例

      一个类在应用中如果有两个或者两个以上的实例会引起错误,或者某个类在整个应用中,同一时刻,有且只能有一种状态,则需要被设计为单例。在日常应用中使用的比较多就是配置类,还有我们常用的数据库连接池等都是使用了单例模式。

      java中单例模式的几种的方式

      1、饿汉式

    package singleton;
    
    public class HungerSingleton {
    
        private static HungerSingleton simpleSingleton = new HungerSingleton();
        private HungerSingleton(){};
        public static HungerSingleton getSingleton() {
            return simpleSingleton;
        }
    }

      优点:线程安全,访问时速度快,因为项目启动时已经创建好;缺点:项目启动慢,初始化需要占用空间即便没有用到

      2、简单懒汉式

    package singleton;
    
    public class SimpleSingleton {
    
        private static SimpleSingleton simpleSingleton;
        private SimpleSingleton(){};
        
        public static SimpleSingleton getSingleton() {
            if(simpleSingleton== null) {
                simpleSingleton = new SimpleSingleton();
            }
            return simpleSingleton;
        }
    }

      优点:使用时才会创建,节省空间,启动速度快,缺点:多线程下容易创建多个实例,造成莫名的错误,通过下面一个简单的例子可以验证

    package singleton;
    
    import java.util.HashSet;
    import java.util.Set;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Client {
        
        private volatile boolean flag;
        public boolean isFlag() {
            return flag;
        }
        public void setFlag(boolean flag) {
            this.flag = flag;
        }
    
    
        public static void main(String[] args) throws InterruptedException {
            Set<String> set = new HashSet<>();
            ExecutorService executorService = Executors.newCachedThreadPool();
            Client client = new Client();
            client.setFlag(false);
            for(int i=0;i<100;i++) {
                executorService.execute(new Runnable() {
                    
                    @Override
                    public void run() {
                        while(true) {
                            if(client.isFlag()) {
                                SimpleSingleton s = SimpleSingleton.getSingleton();
                                set.add(s.toString());
                                break;
                            }
                        }
                    }
                });
            }
            Thread.sleep(5000);
            client.setFlag(true);
            Thread.sleep(5000);
            System.out.println("并发情况下获取的实例情况");
            for (String string : set) {
                System.out.println(string);
            }
            executorService.shutdown();
        }
    }

      运行结果如下:

      3、同步懒汉式

    package singleton;
    
    public class SynSingleton {
    
        private static SynSingleton simpleSingleton;
        private SynSingleton(){};
        
        public static synchronized  SynSingleton getSingleton() {
            if(simpleSingleton== null) {
                simpleSingleton = new SynSingleton();
            }
            return simpleSingleton;
        }
    }

      优点:线程安全,启动快,初始化时不占用空间,缺点:同步代码较多,容易造成线程等待

      4、双重检查懒汉式

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

      优点:线程安全,启动快,节省初始化空间,效率高,缺点:代码复杂,可能出现未知错误,至于为什么会出现未知错误,那我们就要先了解jvm创建对象的过程。

      jvm创建对象分为三个步骤:1.分配内存2.初始化构造器3.将对象指向分配的内存的地址,这种顺序在上述双重加锁的方式是没有问题的,因为这种情况下JVM是完成了整个对象的构造才将内存的地址交给了对象。但是如果2和3步骤是相反的(2和3可能是相反的是因为JVM会针对字节码进行调优,而其中的一项调优便是调整指令的执行顺序),就会出现问题了,因为这时将会先将内存地址赋给对象,针对上述的双重加锁,就是说先将分配好的内存地址指给synchronizedSingleton,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为synchronizedSingleton对象已经实例化了,直接返回一个引用。如果在初始化构造器之前,这个线程使用了synchronizedSingleton,就会产生莫名的错误。

      5、双重检查 + volatile

      这种就是在双重检查的基础上给 simpleSingleton 属性加上volatile关键字,这样便可以禁止jvm指定重排序,即避免上双重检查出现未知错误的情况。

      6、最常用的单例模式,静态内部类

    package singleton;
    
    public class StandardSingleton {
    
        private StandardSingleton(){};
        
        public static StandardSingleton getSingleton() {
            
            return InnerSingleton.standardSingleton;
        }
        
        private static class InnerSingleton{
            static StandardSingleton standardSingleton = new StandardSingleton();
        }
    }

      关于这种方式,有人会问会不会和双重检查有一样的问题,答案是不会,因为jvm在加载静态属性是内部实现了同步,所以不用考虑多线程下的问题。

  • 相关阅读:
    selenium介绍
    python爬虫之requests模块介绍
    SQLAlchemy框架用法详解
    JS判断是否为移动版浏览器
    goahead Web Server 环境搭建(Linux)
    Android 应用层APP发送短信
    Git使用相关问题汇总
    Spring boot 默认首页配置
    Android Studio高版本中文输入异常
    Android ADB 常用命令详解
  • 原文地址:https://www.cnblogs.com/hhhshct/p/10024827.html
Copyright © 2011-2022 走看看