文章目录概念一、懒汉式单例1、在getInstance方法上加同步2、双重检查锁定3、静态内部类二、饿汉式单例饿汉式和懒汉式区别1、名字上区别2、从以下两点再区分以下这两种方式1、线程安全2、资源加载和性能3、区分1、2、3三种不同懒汉什么是线程安全也可以参考菜鸟教程https://www.runoob.com/design-pattern/singleton-pattern.html概念java中单例模式是一种常见的设计模式单例模式的写法有好几种这里主要介绍三种懒汉式单例、饿汉式单例、登记式单例。单例模式有以下特点1、单例类只能有一个实例。2、单例类必须自己创建自己的唯一实例。3、单例类必须给所有其他对象提供这一实例。单例模式确保某个类只有一个实例而且自行实例化并向整个系统提供这个实例。在计算机系统中线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机但只能有一个Printer Spooler以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口系统应当集中管理这些通信端口以避免一个通信端口同时被两个请求同时调用。总之选择单例模式就是为了避免不一致状态避免政出多头。一、懒汉式单例//懒汉式单例类.在第一次调用的时候实例化自己publicclassSingleton{privateSingleton(){}privatestaticSingletonsinglenull;//静态工厂方法publicstaticSingletongetInstance(){if(singlenull){singlenewSingleton();}returnsingle;}publicvoiddoSomething(){System.out.println(singleton is doing something);}}Singleton通过将构造方法限定为private避免了类在外部被实例化在同一个虚拟机范围内Singleton的唯一实例只能通过getInstance()方法访问。单例模式的调用非常简单只需要调用类名方法名即可。publicstaticvoidmain(Stringargs[]){Singleton.getInstance().doSomething();}事实上通过Java反射机制是能够实例化构造方法为private的类的那基本上会使所有的Java单例实现失效。通过反射来打破单例模式只能构建一个对象的实例约束//获得构造器ConstructorconSingleton.class.getDeclaredConstructor();//设置为可访问con.setAccessible(true);//构造两个不同的对象Singletonsingleton1(Singleton)con.newInstance();Singletonsingleton2(Singleton)con.newInstance();//验证是否是不同对象System.out.println(singleton1.equals(singleton2));但是以上懒汉式单例的实现没有考虑线程安全问题并发环境下很可能出现多个Singleton实例。要实现线程安全有以下三种方式都是对getInstance这个方法改造保证懒汉式单例线程安全。1、在getInstance方法上加同步publicstaticsynchronizedSingletongetInstance(){if(singlenull){singlenewSingleton();}returnsingle;}缺点对整个方法加了同步锁浪费计算机线程调度的等待时间2、双重检查锁定synchronized锁住局部代码块使用起来比synchronized锁住整个方法要灵活得多提升程序执行效率。publicstaticSingletongetInstance(){if(singletonnull){synchronized(Singleton.class){if(singletonnull){singletonnewSingleton();}}}returnsingleton;}问题为什么需要两次判断if(singleTonnull)?第一次校验由于单例模式只需要创建一次实例如果后面再次调用getInstance方法时则直接返回之前创建的实例因此大部分时间不需要执行同步方法里面的代码大大提高了性能。如果不加第一次校验的话那跟上面的懒汉模式没什么区别每次都要去竞争锁。第二次校验如果没有第二次校验假设线程t1执行了第一次校验后判断为null这时t2也获取了CPU执行权也执行了第一次校验判断也为null。接下来t2获得锁创建实例。这时t1又获得CPU执行权由于之前已经进行了第一次校验结果为null不会再次判断获得锁后直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验如果实例为空则进行创建。需要注意的是private static volatile SingleTon3 singleTonnull;需要加volatile关键字否则会出现错误。问题的原因在于JVM和CPU的指令重排优化的存在。在某个线程创建单例对象时在构造方法被调用之前就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance取到的就是状态不正确的对象程序就会出错。指令重排是什么意思呢比如java中简单的一句 instance new Singleton会被编译器编译成如下JVM指令memory allocate(); //1分配对象的内存空间 ctorInstance(memory); //2初始化对象 instance memory; //3设置instance指向刚分配的内存地址但是这些指令顺序并非一成不变有可能会经过JVM和CPU的优化指令重排成下面的顺序memory allocate(); //1分配对象的内存空间 instance memory; //3设置instance指向刚分配的内存地址 ctorInstance(memory); //2初始化对象当线程A执行完1,3,时instance对象还未完成初始化但已经不再指向null。此时如果线程B抢占到CPU资源执行 ifinstance null的结果会是false从而返回一个没有初始化完成的instance对象。//优化后的单例publicclassSingleton{privateSingleton(){}//私有构造函数privatevolatilestaticSingletonsingletonnull;//单例对象//静态工厂方法publicstaticSingletongetInstance(){if(singletonnull){//双重检测机制synchronized(Singleton.class){//同步锁if(singletonnull){//双重检测机制singletonnewSingleton();}}}returnsingleton;}}用最简单的方式理解volatile修饰符阻止了变量访问前后的指令重排保证了指令执行顺序经过volatile的修饰当线程A执行instance new Singleton的时候JVM执行顺序是什么样始终保证是下面的顺序memory allocate(); //1分配对象的内存空间 ctorInstance(memory); //2初始化对象 instance memory; //3设置instance指向刚分配的内存地址如此在线程B看来instance对象的引用要么指向null要么指向一个初始化完毕的Instance而不会出现某个中间态保证了安全。4静态内部类同样也是利用了类的加载机制它与饿汉模式不同的是它是在内部类里面去创建对象实例。这样的话只要应用中不使用内部类JVM就不会去加载这个单例类也就不会创建单例对象从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。3、静态内部类这种比上面1、2都好一些既实现了线程安全又避免了同步带来的性能影响。publicclassSingleton{privatestaticclassLazyHolder{privatestaticfinalSingletonINSTANCEnewSingleton();}privateSingleton(){}publicstaticfinalSingletongetInstance(){returnLazyHolder.INSTANCE;}}深入理解单例模式静态内部类单例原理——————————————————————————————二、饿汉式单例饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用以后不再改变所以天生是线程安全的。//饿汉式单例类.在类初始化时已经自行实例化publicclassSingleton1{privateSingleton1(){}privatestaticfinalSingleton1singlenewSingleton1();//静态工厂方法publicstaticSingleton1getInstance(){returnsingle;}}饿汉式和懒汉式区别1、名字上区别饿汉就是类一旦加载就把单例初始化完成保证getInstance的时候单例是已经存在的了懒汉比较懒只有当调用getInstance的时候才初始化这个单例。2、从以下两点再区分以下这两种方式1、线程安全饿汉式天生就是线程安全的可以直接用于多线程而不会出现问题懒汉式本身是非线程安全的为了实现线程安全有几种写法分别是上面的1、2、3这三种实现在资源加载和性能方面有些区别。2、资源加载和性能饿汉在类创建的同时就实例化一个静态对象出来不管之后会不会使用这个单例都会占据一定的内存但是相应的在第一次调用时速度也会更快因为其资源已经初始化完成。懒汉式会延迟加载在第一次使用该单例的时候才会实例化对象出来第一次调用时要做初始化如果要做的工作比较多性能上会有些延迟之后就和饿汉式一样了。3、区分1、2、3三种不同懒汉第1种在方法调用上加了同步虽然线程安全了但是每次都要同步会影响性能毕竟99%的情况下是不需要同步的。第2种在getInstance中做了两次null检查确保了只有第一次调用单例的时候才会做同步这样也是线程安全的同时避免了每次都同步的性能损耗第3种利用了classloader的机制来保证初始化instance时只有一个线程所以也是线程安全的同时没有性能损耗所以一般我倾向于使用这一种。什么是线程安全如果你的代码所在的进程中有多个线程在同时运行而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的而且其他的变量的值也和预期的是一样的就是线程安全的。或者说一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题那就是线程安全的。应用以下是一个单例类使用的例子以懒汉式为例这里为了保证线程安全使用了双重检查锁定的方式publicclassTestSingleton{Stringnamenull;privateTestSingleton(){}privatestaticvolatileTestSingletoninstancenull;publicstaticTestSingletongetInstance(){if(instancenull){synchronized(TestSingleton.class){if(instancenull){instancenewTestSingleton();}}}returninstance;}publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.namename;}publicvoidprintInfo(){System.out.println(the name is name);}}可以看到里面加了volatile关键字来声明单例对象既然synchronized已经起到了多线程下原子性、有序性、可见性的作用为什么还要加volatile呢原因已经在下面评论中提到还有疑问可参考http://www.iteye.com/topic/652440和http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.htmlpublicclassTMain{publicstaticvoidmain(String[]args){TestStreamts1TestSingleton.getInstance();ts1.setName(jason);TestStreamts2TestSingleton.getInstance();ts2.setName(0539);ts1.printInfo();ts2.printInfo();if(ts1ts2){System.out.println(创建的是同一个实例);}else{System.out.println(创建的不是同一个实例);}}}运行结果结论由结果可以得知单例模式为一个面向对象的应用程序提供了对象惟一的访问点不管它实现何种功能整个应用程序都会同享一个实例对象。对于单例模式的几种实现方式知道饿汉式和懒汉式的区别线程安全资源加载的时机还有懒汉式为了实现线程安全的3种方式的细微差别。优点1由于单例模式在内存中只有一个实例减少了内存开支特别是一个对象需要频繁地创建、销毁时而且创建或销毁时性能又无法优化单例模式的优势就非常明显。2由于单例模式只生成一个实例所以减少了系统的性能开销当一个对象的产生需要比较多的资源时比如读取配置、产生其他依赖对象时则可以通过在应用启动时直接产生一个单例对象然后用永久驻留内存的方式来解决。3单例模式可以避免对资源的多重占用例如一个写文件操作由于只有一个实例存在内存中避免对同一个资源文件的同时写操作。4单例模式可以在系统设置全局的访问点优化和共享资源访问例如可以设计一个单例类负责所有数据表的映射处理。缺点1单例模式一般没有接口扩展很困难若要扩展除了修改代码基本上没有第二种途径可以实现。2单例对象如果持有Context那么很容易引发内存泄露此时需要注意传给单例对象的Context最好是Application Context