(一)单例模式

1,单例模式:

          为了限制该类对象被随意的创建,需要保证该类构造方法是私有的,这样外部类就无法创建该类型的对象了,另外,为了方便给客户对象提供对此单例对象的使用,给它提供一个全局访问点。

     2,实现方式:

  通常单例模式在Java语言中,有两种构建方式:

  • 懒汉式—线程不安全:最基础的实现方式,线程上下文单例,不需要共享给所有线程,也不需要加synchronize之类的锁,以提高性能。
  • 懒汉式—线程安全:加上synchronize之类保证线程安全的基础上的懒汉模式,相对性能很低,大部分时间并不需要同步
  • 饿汉方式。指全局的单例实例在类装载时构建。
  • 双检锁式。在懒汉式基础上利用synchronize关键字和volatile关键字确保第一次创建时没有线程间竞争而产生多个实例,仅第一次创建时同步,性能相对较高
  • 登记式。作为创建类的全局属性存在,创建类被装载时创建
  • 枚举。java中枚举类本身也是一种单例模式  

   3,单例中懒汉和饿汉的本质区别在于以下几点:

    (1)饿汉式: 线程安全的,在类创建的同时就已经创建好一个静态对象供系统使用,以后不再改变。

        懒汉式:如果在创建实例对象时不加上synchronized则会导致对象的访问不是线程安全的。

  (2)实现方式上划分:

         懒汉式是延迟加载,它在需要的时候才会创建对象,而饿汉式是在虚拟机启动的时候就会创建。

         饿汉式无需关注多线程问题,写法简单明了,能用则用。

   注意:它是加载类时创建实例,所以如果是工厂模式,缓存了很多实例,那么就得考虑效率问题,因为这个类一加载则把所有实例不管用不用都一块创建。

    (3)两者建立单例对象的时间不同。“懒汉式”是在你真正用到的时候才去建这个单例对象,“饿汉式”是在不管用不用得上,一开始就建立这个单例对象。

   4,优缺点对比:

    饿汉式:没有加锁,执行效率会提高。但是类加载时就初始化,浪费内存。它基于 classloder 机制避免了多线程的同步问题,

    懒汉式:线程不安全。

 5,简单的单例模式

     (1)饿汉模式:      

public class HungrySingleton {
    //保存该类对象的实例,饿汉式的做法:在声明的同时初始化该对象
    private static HungrySingleton instance=new HungrySingleton();
    //将构造函数私有化,不对外提供构造函数
    private HungrySingleton(){}
    //对外提供访问该类对象的方法
    public static HungrySingleton getInstance(){
        return instance;
    }
}

    (2)懒汉模式:

          出于性能等方面的考虑,采用延迟实例化单例对象(static属性在加载类时就被初始化),只有第一次使用该类的实例时才去实例化。-------------延迟创建

/**
 * 非线程安全的懒汉式
 */
public class UnThreadSafeLazySingleton {
    private static UnThreadSafeLazySingleton singleton=null;
    private UnThreadSafeLazySingleton(){}
    public static UnThreadSafeLazySingleton getInstance(){
           if(singleton==null){
               singleton=new UnThreadSafeLazySingleton();
           }
           return singleton;
    }
}

  6,线程不安全的解决办法:    

         在高并发环境中,getInstance()方法返回的多个指向不同该类的实例,这样在没有自动内存回收的平台会产生内存泄漏。

  (1) 加同步锁--------synchronized 同步方法

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

 (2)加同步块------双重检测  Double-Check Locking

     synchronized 是重量级锁,在高并发访问情况下,很影响性能,所以缩小锁的范围来提高性能    

// 双重检测 
public class DoubleCheckSingleton {
    private volatile static DoubleCheckSingleton singleton=null;
    private DoubleCheckSingleton(){}
    public static DoubleCheckSingleton getInstance(){
        if(singleton==null){//检查是否已经被创建
            synchronized (DoubleCheckSingleton.class){ //同步块
                if (singleton==null){//再次检测是否被创建----双重检测
                    singleton=new DoubleCheckSingleton();
                }
            }
        }
        return singleton;
    }
}

     new Singleton()分为三步,1、分配内存空间,2、初始化对象,3、设置instance指向被分配的地址。然而指令的重新排序,可能优化指令为1、3、2的顺序。如果是单个线程访问,不会有任何问题。但是如果两个线程同时获取getInstance,其中一个线程执行完1和3步骤,此时其他的线程可以获取到instance的地址,在进行if(instance==null)时,判断出来的结果为false,导致其他线程直接获取到了一个未进行初始化的instance,这可能导致程序的出错。所以用volatile修饰instance,禁止指令的重排序,保证程序能正常运行。(Bug很难出现,没能模拟出来)。

 参考地址:volatile

(3) 既要保证线程安全又要延迟加载的效果还可以采用 持有者按需初始化模式(Initilization on demand holer

/**
 * Initialization on demand holer 持有者按需初始化
 * @Author: wxw
 * @create: 2019-10-20-20:53
 * 线程安全的延迟加载的单例初始化
 */
public class LazyLoadedSingleton {
    private LazyLoadedSingleton(){}
    private static class LazyHolder{ //持有单例类
        private static final LazyLoadedSingleton singleton=new LazyLoadedSingleton();
    }
    public static LazyLoadedSingleton getInstance(){
        return LazyHolder.singleton;
    }
}

说明:由于Jvm加载 LazyLoadedSingleton 类时,该类没有static的属性,所以加载完成后可以返回,只有第一次使用getInstance()方法时,Jvm才会加载LazyHolder类,由于它包含了一个static属性的singleton实例

 所以会首先初始化这个变量,这样并不会出现并发问题,这样既保证了线程安全又支持懒加载的单例模式。

7,单实例singleton的序列化问题

(1)问题: 如果单例类实现了Seriaizable接口,在默认情况下每次反序列化都会创建一个新的实例对象,这样一个系统会使用多个对象共使用。

       解决方案:在readResolve()方法中替换掉反序列化出来的那个新实例,让其指向内存中那个单例对象即可。

(2)readResolve()方法:此方法在反序列化完成之前执行

public class SerialibleSingleton implements Serializable {
     private static SerialibleSingleton singleton=new SerialibleSingleton();
     private SerialibleSingleton(){}
     public static SerialibleSingleton getInstance(){
         return singleton;
     }
     //反序列化完成之前执行,为了替换掉反序列化出来的那个新实例
     private Object readResolve(){
         return singleton;
     }
}

9,单例模式优缺点:

   优点: 
    1.在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例 
    2.单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。 
    3.提供了对唯一实例的受控访问。 
    4.由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。 
    5.允许可变数目的实例。 
    6.避免对共享资源的多重占用。 
缺点: 
    1.不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。 
    2.由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。 
    3.单例类的职责过重,在一定程度上违背了“单一职责原则”。 
    4.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。 
使用注意事项: 
    1.使用时不能用反射模式创建单例,否则会实例化一个新的对象 
    2.使用懒单例模式时注意线程安全问题 
    3.饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式) 

参考博客:1,线程安全分析单例

                  2,单例模式概述

                  3,单例模式优缺点对比

                  4, 一个线程安全的、无需synchronization的、且比无竞争的同步高效的单例模式

Java半颗糖
原文地址:https://www.cnblogs.com/2019wxw/p/11710289.html