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

    java单例模式五种实现方式

    补充知识:
    1.单例模式的破坏与防御
    反射:对于枚举类,该破解方法不适用。
    序列化:对于枚举类,该破解方法不适用。
    该测试首先需要声明Singleton为实现了可序列化接口。
    防御
    对于序列化与反序列化,我们需要添加一个自定义的反序列化方法,使其不再创建对象而是直接返回已有实例,就可以保证单例模式。
    我们再次用下面的类进行测试,就发现结果为true。
    2.类加载机制?
    static关键字的作用是把类的成员变成类相关,而不是实例相关,static块会在类首次被用到的时候进行加载,
    不是对象创建时,所以static块具有线程安全性。
    ClassLoader
    JVM中存在两种ClassLoader,启动内装载器(bootstrap)和用户自定义装载器(user-defined class loader),在一个JVM中可能存在多个ClassLoader,每个ClassLoader拥有自己的NameSpace。一个ClassLoader只能拥有一个class对象类型的实例,但是不同的ClassLoader可能拥有相同的class对象实例,这时可能产生致命的问题。

    普通初始化块
    当Java创建一个对象时, 系统先为对象的所有实例变量分配内存(前提是该类已经被加载过了), 然后开始对这些实例变量进行初始化,
    顺序是: 先执行初始化块或声明实例变量时指定的初始值(这两处执行的顺序与他们在源代码中排列顺序相同), 再执行构造器里指定的初始值.

    静态初始化块
    又名类初始化块(普通初始化块负责对象初始化, 类初始化块负责对类进行初始化). 静态初始化块是类相关的, 系统将在类初始化阶段静态初始化, 而不是在创建对象时才执行. 因此静态初始化块总是先于普通初始化块执行.

    执行顺序
    系统在类初始化以及对象初始化时, 不仅会执行本类的初始化块[static/non-static], 而且还会一直上溯到java.lang.Object类, 先执行Object类中的初始化块[static/non-static], 然后执行其父类的, 最后是自己.
    顶层类(初始化块, 构造器) -> … -> 父类(初始化块, 构造器) -> 本类(初始化块, 构造器)

    小结
    static{} 静态初始化块会在类加载过程中执行;{} 则只是在对象初始化过程中执行, 但先于构造器;

    3.内部类
    内部类访问权限
    Java 外部类只有两种访问权限:public/default, 而内部类则有四种访问权限:private/default/protected/public. 而且内部类还可以使用static修饰;内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。如果成员内部类Inner用private修饰,则只能在外部类的内部访问,
    如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;
    如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。
    成员内部类可以看做是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。
    内部类分为成员内部类与局部内部类, 相对来说成员内部类用途更广泛, 局部内部类用的较少(匿名内部类除外), 成员内部类又分为静态(static)内部类与非静态内部类, 这两种成员内部类同样要遵守static与非static的约束(如static内部类不能访问外部类的非静态成员等)
    非静态内部类
    非静态内部类在外部类内使用时, 与平时使用的普通类没有太大区别;
    Java不允许在非static内部类中定义static成员,除非是static final的常量类型
    如果外部类成员变量, 内部类成员变量与内部类中的方法里面的局部变量有重名, 则可通过this, 外部类名.this加以区分.
    非静态内部类的成员可以访问外部类的private成员, 但反之不成立, 内部类的成员不被外部类所感知. 如果外部类需要访问内部类中的private成员, 必须显示创建内部类实例, 而且内部类的private权限对外部类也是不起作用的:
    静态内部类
    使用static修饰内部类, 则该内部类隶属于该外部类本身, 而不属于外部类的某个对象.
    由于static的作用, 静态内部类不能访问外部类的实例成员, 而反之不然;
    匿名内部类
    如果(方法)局部变量需要被匿名内部类访问, 那么该局部变量需要使用final修饰.

    4.枚举方法:参见枚举类解析
    枚举
    枚举类继承了java.lang.Enum, 而不是Object, 因此枚举不能显示继承其他类; 其中Enum实现了Serializable和Comparable接口(implements Comparable, Serializable);
    非抽象的枚举类默认使用final修饰,因此枚举类不能派生子类;
    枚举类的所有实例必须在枚举类的第一行显示列出(枚举类不能通过new来创建对象); 并且这些实例默认/且只能是public static final的;
    枚举类的构造器默认/且只能是private;
    枚举类通常应该设计成不可变类, 因此建议成员变量都用private final修饰;
    枚举类不能使用abstract关键字将枚举类声明成抽象类(因为枚举类不允许有子类), 但如果枚举类里面有抽象方法, 或者枚举类实现了某个接口, 则定义每个枚举值时必须为抽象方法提供实现,

    线程安全
    由于枚举类的会在编译期编译为继承自java.lang.Enum的类,其构造函数为私有,不能再创建枚举对象,
    枚举对象的声明和初始化都是在static块中,所以由JVM的ClassLoader机制保证了线程的安全性。但是不能实现延迟加载
    序列化
    由于枚举类型采用了特殊的序列化方法,从而保证了在一个JVM中只能有一个实例。
    枚举类的实例都是static的,且存在于一个数组中,可以用values()方法获取该数组
    在序列化时,只输出代表枚举类型的名字属性 name
    反序列化时,根据名字在静态的数组中查找对应的枚举对象,由于没有创建新的对象,因而保证了一个JVM中只有一个对象。

    3======
    Java中单例(Singleton)模式是一种广泛使用的设计模式。单例模式的主要作用是保证在Java程序中,某个类只有一个实例存在。一些管理器和控制器常被设计成单例模式。 单例模式有很多好处,它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式也许是一个值得考虑的选择。

    1、饿汉模式
    public class Singleton{
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton newInstance(){
    return instance;
    }
    }
    从代码中我们看到,类的构造函数定义为private的,保证其他类不能实例化此类,然后提供了一个静态实例并返回给调用者。饿汉模式是最简单的一种实现方式,饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。它的好处是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。
    这种实现方式适合单例占用内存比较小,在初始化时就会被用到的情况。 但是,如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。

    2、懒汉模式
    public class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton newInstance(){
    if(null == instance){
    instance = new Singleton();
    }
    return instance;
    }
    }
    懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择。但是这里的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,因此需要加锁解决线程同步问题,实现如下。
    public class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public static synchronized Singleton newInstance(){
    if(null == instance){
    instance = new Singleton();
    }
    return instance;
    }
    }
    3、双重校验锁
    加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载,然而它存在着性能问题,依然不够完美。synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。因此就有了双重校验锁,
    public class Singleton {
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance() {
    if (instance == null) {
    synchronized (Singleton.class) {
    if (instance == null) {//2
    instance = new Singleton();
    }
    }
    }
    return instance;
    }
    }
    可以看到上面在同步代码块外多了一层instance为空的判断。由于单例对象只需要创建一次,如果后面再次调用getInstance()只需要直接返回单例对象。因此,大部分情况下,调用getInstance()都不会执行到同步代码块,从而提高了程序性能。不过还需要考虑一种情况,假如两个线程A、B,A执行了if (instance == null)语句,它会认为单例对象没有创建,此时线程切到B也执行了同样的语句,B也认为单例对象没有创建,然后两个线程依次执行同步代码块,并分别创建了一个单例对象。为了解决这个问题,还需要在同步代码块中增加if (instance == null)语句,也就是上面看到的代码2。

    我们看到双重校验锁即实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题,是否真的就万无一失了呢?
    这里要提到Java中的指令重排优化。
    所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。
    这个问题的关键就在于由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。以上就是双重校验锁会失效的原因,

    不过还好在JDK1.5及之后版本增加了volatile关键字。volatile的一个语义是禁止指令重排序优化,也就保证了instance变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。
    public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance() {
    if (instance == null) {
    synchronized (Singleton.class) {
    if (instance == null) {
    instance = new Singleton();
    }
    }
    }
    return instance;
    }
    }

    4、静态内部类--同时保证延迟加载和线程安全。
    public class Singleton{
    private static class SingletonHolder{
    public static Singleton instance = new Singleton();
    }
    private Singleton(){}
    public static Singleton newInstance(){
    return SingletonHolder.instance;
    }
    }
    这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。

    5、枚举
    public enum Singleton{
    instance;
    public void whateverMethod(){}
    }
    上面提到的四种实现单例的方式都有共同的缺点:
    1)需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
    2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
    而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。

    总结
    本文总结了五种Java中实现单例的方法,其中前两种都不够完美,双重校验锁和静态内部类的方式可以解决大部分问题,平时工作中使用的最多的也是这两种方式。
    枚举方式虽然很完美的解决了各种问题,但是这种写法多少让人感觉有些生疏。个人的建议是,在没有特殊需求的情况下,使用第三种和第四种方式实现单例模式。

    2====

    单例模式是最常见的一个模式,在Java中单例模式被大量的使用。这同样也是我在面试时最喜欢提到的一个面试问题,然后在面试者回答后可以进一步挖掘其细节,这不仅检查了关于单例模式的相关知识,同时也检查了面试者的编码水平、多线程方面的知识,这些在实际的工作中非常重要。
    开始
    定义:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
    类型:创建类模式

    类图知识点:
    1.类图分为三部分,依次是类名、属性、方法
    2.以<<开头和以>>结尾的为Stereotype
    3.修饰符+代表public,-代表private,#代表protected,什么都没有代表包可见。
    4.带下划线的属性或方法代表是静态的。
    5.对类图中对象的关系不熟悉的朋友可以参考文章:设计模式中类的关系。

    单例模式应该是23种设计模式中最简单的一种模式了。
    它有以下几个要素:
    私有的构造方法
    指向自己实例的私有静态引用
    以自己实例为返回值的静态的公有的方法。
    单例模式根据实例化对象时机的不同分为两种:一种是饿汉式单例,一种是懒汉式单例。
    饿汉式单例在单例类被加载时候,就实例化一个对象交给自己的引用;而懒汉式在调用取得实例方法的时候才会实例化对象。
    代码如下:
    class Singleton{
    private Singleton(){}
    private static final Singleton singleton = new Singleton();
    public static Singleton getInstance(){
    return singleton;}
    }
    Lazy mode:
    class Singleton{
    private Singleton(){}
    private static Singleton singleton ;
    public static synchronized Singleton getInstance(){
    if(singleton==null)
    singleton = new Singleton();
    return singleton;
    }
    }

    1) 哪些类是单例模式的候选类?在Java中哪些类会成为单例?
    (1) 系统资源,如文件路径,数据库链接,系统常量等
    (2)全局状态化类,类似AutomicInteger的使用

    单例模式的优点:
    在内存中只有一个对象,节省内存空间。
    避免频繁的创建销毁对象,可以提高性能。
    避免对共享资源的多重占用。
    可以全局访问。
    适用场景:由于单例模式的以上优点,所以是编程中用的比较多的一种设计模式。
    我总结了一下我所知道的适合使用单例模式的场景:
    需要频繁实例化然后销毁的对象。
    创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
    有状态的工具类对象。
    频繁访问数据库或文件的对象。
    这里将检查面试者是否有对使用单例模式有足够的使用经验。他是否熟悉单例模式的优点和缺点。

    2)你能在Java中编写单例里的getInstance()的代码?

    (1)静态成员直接初始化,或者在静态代码块初始化都可以
    class Singleton{
    private Singleton(){}
    private static Singleton singleton ;
    public static synchronized Singleton getInstance(){
    if(singleton==null)
    singleton = new Singleton();
    return singleton;
    }
    }
    该实现只要在一个ClassLoad下就会提供一个对象的单例。
    但是美中不足的是,不管该资源是否被请求,它都会创建一个对象,占用jvm内存。
    饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间。

    从lazy initialization思想出发,出现了下2的写法
    (2) 根据lazy initialization思想,使用到时才初始化。
    class Singleton{
    private Singleton(){}
    private static Singleton singleton ;
    public static synchronized Singleton getInstance(){
    if(singleton==null)
    singleton = new Singleton();
    return singleton;
    }
    }
    该实现方法加了同步锁,可以有效防止多线程在执行getInstance方法得到2个对象。
    缺点:只有在第一次调用的时候,才会出现生成2个对象,才必须要求同步。而一旦singleton不为null,系统依旧花费同步锁开销,有点得不偿失。

    因此再改进出现写法3
    class Singleton{
    private Singleton(){}
    private static Singleton singleton ;
    public static Singleton getInstance(){
    if(singleton==null)//1
    synchronized(Singleton.class){//2
    singleton = new Singleton();//3
    }
    return singleton;
    }
    }
    这种写法减少了锁开销,但是在如下情况,却创建了2个对象:
    a:线程1执行到1挂起,线程1认为singleton为null
    b:线程2执行到1挂起,线程2认为singleton为null
    c:线程1被唤醒执行synchronized块代码,走完创建了一个对象
    d:线程2被唤醒执行synchronized块代码,走完创建了另一个对象
    所以看出这种写法,并不完美。

    (4)为了解决3存在的问题,引入双重检查锁定
    public static Singleton getInstance(){
    if(singleton==null)//1
    synchronized(Singleton.class){//2
    if(singleton==null)//3
    singleton = new Singleton();//4
    }
    return singleton;
    }
    在同步锁代码块内部,再判断一次对象是否为null,为null才创建对象。这种写法已经接近完美:
    a:线程1执行到1,已经进入synchronized的时候,线程挂起,线程1占有Singleton.class资源锁;
    b:线程2执行到1,当它准备synchronized块时,因为Singleton.class被占用,线程2阻塞;
    c:线程1被唤醒,判断出对象为null,执行完创建一个对象
    d:线程2被唤醒,判断出对象不为null,不执行创建语句
    如此分析,发现似乎没问题。
    但是实际上并不能保证它在单处理器或多处理器上正确运行;
    问题就出现在singleton = new Singleton()这一行代码。
    它可以简单的分成如下三个步骤:
    mem= singleton();//1
    instance = mem;//2
    ctorSingleton(instance);//3
    这行代码先在内存开辟空间,赋给singleton的引用,然后执行new初始化数据,
    但是注意初始化是要消耗时间。如果此时线程3在执行步骤1的时候,发现singleton 为非null,就直接返回,那么线程3返回的其实是一个没构造完成的对象。
    我们期望1,2,3 按照反序执行,但是实际jvm内存模型,并没有明确的有序指定。这归咎于java的平台的内存模型允许“无序写入”。

    (5) 在4的基础上引入volatile
    class Singleton{
    private Singleton(){}
    private static volatile Singleton singleton ;
    public static Singleton getInstance(){
    if(singleton==null)//1
    synchronized(Singleton.class){//2
    if(singleton==null)//3
    singleton = new Singleton();
    }
    return singleton;
    }
    }

    Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。
    这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。

      根据上面的分析,常见的两种单例实现方式都存在小小的缺陷,那么有没有一种方案,既能实现延迟加载,又能实现线程安全呢
    (6) Lazy initialization holder class模式
      这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。
      1.相应的基础知识
    什么是类级内部类?
      简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。
      类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。
    而对象级内部类的实例,是绑定在外部对象实例中的。
      类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。
      类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。
     
    多线程缺省同步锁的知识
      大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。
    这些情况包括:
      1.由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
      2.访问final字段时
      3.在创建线程之前创建对象时
      4.线程可以看见它将要处理的对象时

      2.解决方案的思路
      要想很简单地实现线程安全,可以采用静态初始化器的方式,它可以由JVM来保证线程的安全性。
    比如前面的饿汉式实现方式。但是这样一来,不是会浪费一定的空间吗?因为这种实现方式,会在类装载的时候就初始化对象,不管你需不需要。
      如果现在有一种方法能够让类装载的时候不去初始化对象,那不就解决问题了?一种可行的方式就是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,那就不会创建对象实例,从而同时实现延迟加载和线程安全。

    public class Singleton {

    private Singleton(){}
    /**
    * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
    * 没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载。
    */
    private static class SingletonHolder{
    /**
    * 静态初始化器,由JVM来保证线程安全
    */
    private static Singleton instance = new Singleton();
    }

    public static Singleton getInstance(){
    return SingletonHolder.instance;
    }
    }

    (6) 单例和枚举
    按照《高效Java第二版》中的说法:单元素的枚举类型已经成为实现Singleton的最佳方法。
    用枚举来实现单例非常简单,只需要编写一个包含单个元素的枚举类型即可。
    public enum Singleton {
    /**
    * 定义一个枚举的元素,它就代表了Singleton的一个实例。
    */
    uniqueInstance;

    /**
    * 单例可以有自己的操作
    */
    public void singletonOperation(){
    //功能处理
    }
    }
    使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,
    是更简洁、高效、安全的实现单例的方式。

    3)在getInstance()方法上同步有优势 还是仅同步必要的块更优优势?你更喜欢哪个方式?
    这确实是一个非常好的问题,我几乎每次都会提该问题,用于检查面试者是否会考虑由于锁定带来的性能开销。
    因为锁定仅仅在创建实例时才有意义,然后其他时候实例仅仅是只读访问的,
    因此只同步必要的块的性能更优,并且是更好的选择。
    缺点:只有在第一次调用的时候,才会出现生成2个对象,才必须要求同步。而一旦singleton 不为null,系统依旧花费同步锁开销,有点得不偿失。

    4)什么是单例模式的延迟加载或早期加载?你如何实现它?
    这是和Java中类加载的载入和性能开销的理解的又一个非常好的问题。
    我面试过的大部分面试者对此并不熟悉,但是最好理解这个概念。

    5) Java平台中的单例模式的实例有哪些?
    这是个完全开放的问题,如果你了解JDK中的单例类,请共享给我。
    java.lang.Runtime;

    6) 单例模式的两次检查锁是什么? volatile的作用?
    可以使用“双重检查加锁(double checked locking)”的方式来实现,就可以既实现线程安全,又能够使性能不受很大的影响。
    那么什么是“双重检查加锁”机制呢?
      所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,
    进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。
    这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

      “双重检查加锁”机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
    Volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。
    可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。它在某些情况下比synchronized的开销更小。
      注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只只能用在java5及以上的版本。


    7)你如何阻止使用clone()方法创建单例实例的另一个实例?
    该类型问题有时候会通过如何破坏单例或什么时候Java中的单例模式不是单例来被问及;

    在JAVA里要注意的是,所有的类都默认的继承自Object,所以都有一个clone方法。
    为保证只有一个实例,要把这个口堵上。
    有两个方面,一个是单例类一定要是final的,这样用户就不能继承它了。
    另外,如果单例类是继承于其它类的,还要override它的clone方法,让它抛出异常。


    8)如果阻止通过使用反射来创建单例类的另一个实例?
    开放的问题。在我的理解中,从构造方法中抛出异常可能是一个选项。
    通过反射创建单例类的另一个实例:
    如果借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器,反射攻击:

    =====
    1========
    Java单例模式(Singleton)以及实现
    一. 什么是单例模式
    因程序需要,有时我们只需要某个类同时保留一个对象,不希望有更多对象,此时,我们则应考虑单例模式的设计。
    二. 单例模式的特点
    1. 单例模式只能有一个实例。
    2. 单例类必须创建自己的唯一实例。
    3. 单例类必须向其他对象提供这一实例。
    三. 单例模式VS静态类
    在知道了什么是单例模式后,我想你一定会想到静态类,“既然只使用一个对象,为何不干脆使用静态类?”,这里我会将单例模式和静态类进行一个比较。
    1. 单例可以继承和被继承,方法可以被override,而静态方法不可以。
    2. 静态方法中产生的对象会在执行后被释放,进而被GC清理,不会一直存在于内存中。
    3. 静态类会在第一次运行时初始化,单例模式可以有其他的选择,即可以延迟加载。
    4. 基于2, 3条,由于单例对象往往存在于DAO层(例如sessionFactory),
    如果反复的初始化和释放,则会占用很多资源,而使用单例模式将其常驻于内存可以更加节约资源。
    5. 静态方法有更高的访问效率。
    6. 单例模式很容易被测试。
    几个关于静态类的误解:
    误解一:静态方法常驻内存而实例方法不是。实际上,特殊编写的实例方法可以常驻内存,而静态方法需要不断初始化和释放。
    误解二:静态方法在堆(heap)上,实例方法在栈(stack)上。实际上,都是加载到特殊的不可写的代码内存区域中。
    静态类和单例模式情景的选择:
    情景一:不需要维持任何状态,仅仅用于全局访问,此时更适合使用静态类。
    情景二:需要维持一些特定的状态,此时更适合使用单例模式。

    结论:由结果可以得知单例模式为一个面向对象的应用程序提供了对象惟一的访问点,
    不管它实现何种功能,整个应用程序都会同享一个实例对象。
    对于单例模式的几种实现方式,知道饿汉式和懒汉式的区别,线程安全,资源加载的时机,
    还有懒汉式为了实现线程安全的3种方式的细微差别。
    饿汉式和懒汉式区别?
    汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,
    而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。
    另外从以下两点再区分以下这两种方式:
    1、线程安全:
    饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,
    懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,这三种实现在资源加载和性能方面有些区别。
    2、资源加载和性能:
    饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,
    而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
    至于1、2、3这三种实现又有些区别,
    第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的,
    第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗
    第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。

    什么是线程安全?
    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
    或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。
    -------
    概念:
      java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例、登记式单例。
      单例模式有以下特点:
      1、单例类只能有一个实例。
      2、单例类必须自己创建自己的唯一实例。
      3、单例类必须给所有其他对象提供这一实例。
      单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
    在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。
    每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。
    每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。
    总之,选择单例模式就是为了避免不一致状态,避免政出多头。

    目的:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
    应用场景:
    数据库连接、缓存操作、分布式存储。
    怎么实现单例模式:
    思考:既然是单例,构造器私有; 只能依靠类来访问了,方法前加static吧;
    可static方法只能访问static变量啊,那就把变量也搞成static吧;

    单例分类
    饿汉式:类加载时便已初始化实例,用空间换时间,不管现在是否使用,先创建了再说;
    线程安全吗?可以通过反射机制攻击;线程安全[多个类加载器除外];
    懒汉式:我懒我快乐,用的时候在创建实例被,不着急;
    具体细分为:双重校验锁、内部类实现
    枚举实现:简单,一行代码搞定;

    /**
    *饿汉式1:线程安全,但数据大的时候,占空间
    这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,这时候初始化instance显然没有达到lazy loading的效果。不推荐。
    */
    class Singleton{
    private static Singleton singleton = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
    return singleton;
    }
    }
    /**
    *饿汉式2
    */
    class Singleton0{
    private static Singleton0 singleton = null;
    static {
    singleton = new Singleton0();
    }
    private Singleton0(){}
    public static Singleton0 getInstance(){
    return singleton;
    }
    }

    //懒汉式:用的的时候在初始化实例;用时间换空间;线程不安全;
    /**
    *懒汉式1:大多数线程安全,但每次获取实例都要加锁,效率低下;
    但是效率极其低下,同步锁锁的是对象,每次取对象都有加锁,因此不推荐,性能很低。
    */
    class Singleton2{
    private static Singleton2 _singleton = null;

    private Singleton2(){}
    public static synchronized Singleton2 getInstance(){
    if(null == _singleton){
    _singleton = new Singleton2();
    }
    return _singleton;
    }
    }

    /**
    *懒汉式2:大多数线程安全,有所改进,加锁次数少了;
    *双重校验锁
    */
    class Singleton3{
    private static Singleton3 _singleton = null;

    private Singleton3(){}
    public static Singleton3 getInstance(){
    if(null == _singleton){
    synchronized(Singleton3.class){
    if(null == _singleton){
    _singleton = new Singleton3();
    }
    }
    }
    return _singleton;
    }
    }

    public class SingletonDemo {
    private volatile static SingletonDemo instance;
    private SingletonDemo(){
    System.out.println("Singleton has loaded");
    }

    public static SingletonDemo getInstance(){
    if(instance==null){
    synchronized (SingletonDemo.class){
    if(instance==null){
    instance=new SingletonDemo();
    }
    }
    }
    return instance;
    }
    }
    接下来我解释一下在并发时,双重校验锁法会有怎样的情景:
    STEP 1. 线程A访问getInstance()方法,因为单例还没有实例化,所以进入了锁定块。
    STEP 2. 线程B访问getInstance()方法,因为单例还没有实例化,得以访问接下来代码块,而接下来代码块已经被线程1锁定。
    STEP 3. 线程A进入下一判断,因为单例还没有实例化,所以进行单例实例化,成功实例化后退出代码块,解除锁定。
    STEP 4. 线程B进入接下来代码块,锁定线程,进入下一判断,因为已经实例化,退出代码块,解除锁定。
    STEP 5. 线程A初始化并获取到了单例实例并返回,线程B获取了在线程A中初始化的单例。
    理论上双重校验锁法是线程安全的,并且,这种方法实现了lazyloading。

    /**
    *懒汉式3:线程安全;
    *new singleton();这步非原子操作,jvm会进行重排序;导致线程不安全;
    *volatile 可禁止重排序;
    */
    class Singleton4{
    private static volatile Singleton4 _singleton = null;

    private Singleton4(){}
    public static Singleton4 getInstance(){
    if(null == _singleton){
    synchronized(Singleton4.class){
    if(null == _singleton){
    _singleton = new Singleton4();
    }
    }
    }
    return _singleton;
    }
    }

    /**
    *懒汉式4:线程安全(静态内部类)
    使用内部类的好处是,静态内部类不会在单例加载时就加载,而是在调用getInstance()方法时才进行加载,达到了类似懒汉模式的效果,而这种方法又是线程安全的。
    */
    class Singleton5{
    private static class SingletonHolder{
    private static Singleton5 singleton = new Singleton5();
    }
    private Singleton5(){}
    public static Singleton5 getInstance(){
    return SingletonHolder.singleton;
    }
    }

     

    这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,
    它跟第三种和第四种方式不同的是(很细微的差别):
    第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),
    而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。
    想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。
    这个时候,这种方式相比第三和第四种方式就显得很合理。


    /**
    *懒汉式5:线程安全;既实现了线程安全,又避免了同步带来的性能影响。
    *内部类实现;
    */
    class Singleton6{
    private static class SingletonHolder{
    public static final Singleton6 _singleton = new Singleton6();
    }
    private Singleton6(){}

    public static final Singleton6 getInstance(){
    return SingletonHolder._singleton;
    }
    }
    /**
    *上面提到的所有实现方式都有两个共同的缺点:
    *(1)都需要额外的工作(Serializable、transient、readResolve())来实现序列化,
    否则每次反序列化一个序列化的对象实例时都会创建一个新的例。
    *(2)使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
    (事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。)
    */

    应用:以下一个单例类使用的例子,以懒汉式为例,这里为了保证线程安全,使用了双重检查锁定的方式:
    public class TestSingleton {
    String name = null;
    private TestSingleton() {
    }

    private static volatile TestSingleton instance = null;

    public static TestSingleton getInstance() {
    if (instance == null) {
    synchronized (TestSingleton.class) {
    if (instance == null) {
    instance = new TestSingleton();
    }
    }
    }
    return instance;
    }
    }
    可以看到里面加了volatile关键字来声明单例对象,既然synchronized已经起到了多线程下原子性、有序性、可见性的作用,为什么还要加volatile呢,原因已经在下面评论中提到。

    第六种,线程安全(枚举类)
    /**
    *枚举实现单例模式:简洁,无偿地提供了序列化机制,由JVM从根本上提供保障,绝对防止多次实例化。线程安全
    *没有延迟加载
    */
    enum Singleton7{
    instance;
    }
    1 public enum Singleton6 {
    2 INSTANCE;
    3 public void whateverMethod(){}
    4 }
    这种方式是Effective 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,推荐!

    enum SingletonDemo{
    INSTANCE;
    public void otherMethods(){
    System.out.println("Something");
    }
    }
    Effective Java作者Josh Bloch 提倡的方式,在我看来简直是来自神的写法。解决了以下三个问题:
    (1)自由序列化。
    (2)保证只有一个实例。
    (3)线程安全。
    如果我们想调用它的方法时,仅需要以下操作:
    public class Hello {
    public static void main(String[] args){
    SingletonDemo.INSTANCE.otherMethods();
    }
    }
    这种充满美感的代码真的已经终结了其他一切实现方法了。
    ==============

    补充:Object obj = new Object(); 这一句到底发生了什么?
    1.给obj分配内存,在栈中分配并初始化为null
    2.调用构造函数,生成对象实例,在堆中分配
    3.把obj指向在堆中分配的对象
    由于指令重排序优化,执行顺序可能会变成1,3,2,
    在多线程中,那么当一个线程执行完1,3之后,被另一个线程抢占,
    这时obj已经不是null了,就会直接返回。然而2还没有执行过,也就是说这个对象实例还没有初始化过。
    懒汉式的模型中懒汉式2即有此问题存在。

    总结
    有两个问题需要注意:
    1、如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类 装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。

    2、如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
    对第一个问题修复的办法是:
    1 private static Class getClass(String classname) throws ClassNotFoundException {
    2 ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    3
    4 if (classLoader == null)
    5 classLoader = Singleton.class.getClassLoader();
    6
    7 return (classLoader.loadClass(classname));
    8 }
    对第二个问题修复的办法是:
    重写readResolve()方法。防止反序列化获取多个对象的漏洞。
    无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。 实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。

     

     

  • 相关阅读:
    1.tomcat部署项目的几种方式和weblogic部署方式及一点通讯
    C程序第二章节:算法
    java 代码第一天练习
    git生成公钥私钥
    sourceTree操作
    gradle插件安装
    gradle环境搭建
    bootstrap
    20170223
    大企业招聘真的是这样进行的吗?
  • 原文地址:https://www.cnblogs.com/awkflf11/p/9072566.html
Copyright © 2011-2022 走看看