设计模式单例模式

6次阅读

共计 2499 个字符,预计需要花费 7 分钟才能阅读完成。

单例模式(Singleton Pattern)使用的比较多,比如我们的 controller 和 service 都是单例的,但是其和标准的单例模式是有区别的。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

<!–more–>

模式结构

单例模式的结构很简单,只涉及到一个单例类,这个单例类的构造方法是私有的,该类自身定义了一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。

源码导读

单例模式分为懒汉单例和饿汉单例;饿汉单例代码很简单,顾名思义,饿汉单例就是类初始化的时候就将该单例创建,示例代码如下:

public class Singleton {private static final Singleton singleton = new Singleton();
    // 限制产生多个对象
    private Singleton(){}
    // 通过该方法获得实例对象
    public static Singleton getSingleton(){return singleton;}
    // 类中其他方法,尽量是 static
    public static void doSomething(){}
}

但是懒汉单例就不那么简单了,懒汉单例是在访问这个类的实例的时候先判断这个类的实例是否创建好了,如果没创建好就要先创建这个单例。也就是说懒汉单例是第一次访问的的时候创建单例,而不是初始化阶段。这将会导致一个问题,如果在多线程场景下,多个线程同时访问这个单例都发现其未被创建,那么这些线程就会分别创建实例,那么这个单例模式就不那么单例了——实例被多次创建。在阿里开发手册中有两条就是和懒汉单例相关的,告诉我们要如何去避免这种情况,第六节的第一条 和第十二条:

(六) 并发处理

1.【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。

说明:资源驱动类、工具类、单例工厂类都需要注意。

  1. 【推荐】在并发场景下,通过双重检查锁(double-checked locking)实现延迟初始化的优

化问题隐患 (可参考 The “Double-Checked Locking is Broken” Declaration),推荐解

决方案中较为简单一种(适用于 JDK5 及以上版本),将目标属性声明为 volatile 型。

反例:

class Singleton {  
    private Helper helper = null;  
    public Helper getHelper() {if (helper == null) synchronized(this) {if (helper == null)  
            helper = new Helper();}  
        return helper;  
    }  
// other methods and fields...  

} 

volatile 关键字的作用和双重检查锁在我以往的博客中介绍过,文章地址 https://mp.weixin.qq.com/s/r52hmD71TtiJjlOzQUvRlA 这篇博客介绍了并发的一些知识,小伙伴有空可以读一读。在这里 volatile 关键字的作用就是保证数据的可见性,双重检查锁是提高代码性能。下面我们分析一下手册中的反例:

其中它的双重检测锁指的是这段代码:

if (helper == null) synchronized(this) {if (helper == null)  
            helper = new Helper();}  

这里如果不用双重检测锁的话只能在整个 getHelper 方法上上锁,因为这个方法必须要保证在并发情况下只有一个线程会执行 helper = new Helper(); ,这段代码。也就是说代码 会成为这样:

public synchronized Helper getHelper() {if (helper == null)  {if (helper == null)  
            helper = new Helper();}  
        return helper;  
}  

整个方法上锁性能明显是不好的,锁的粒度变大了;双重检查锁里面为什么要做两次 if 判断呢,这个问题留给读者思考,并不是特别难的问题。但是反例里面没有考虑到可见性的问题——假设 a 线程和 b 线程同时访问 getHelper 方法,然后 b 线程被阻塞住,a 线程发现 helper 未被实例化,于是执行 new 方法,然后释放锁;此时 b 线程进来,或许我们直观的感受是 b 线程发现属性被实例化直接返回 helper,但实际上不是,当一个线程修改了线程共享的公共资源的时候(此处是 helper 属性)其他线程未必会被通知到属性被修改,因此 b 线程有可能发现 helper 还是 null 也有可能 b 线程知道 helper 被赋值了。使用 volatile 就可以避免这种情况的发生。因此正确的代码应该是这样的:

class Singleton {  
    private volatile Helper helper = null;  
    public Helper getHelper() {······}  
// other methods and fields...  
} 

扩展

单例模式到这里算是讲完了,我再扩展一下单例相关的知识点——问:service 和 controller 都是单例的,它们的代码也没有锁相关的东西,为什么是线程安全的?

如果你 jvm 模型理解的还算透彻的话,这个问题就很好回答。通俗的说就是 service 或者 controller 里面都是方法,没有基本数据类型和字符串这样的属性。用专业术语回答就是:它们都是无状态的 bean。其实 bean 的概念是在 ejb 规范里面提出来的,后面就被沿用了。感兴趣的小伙伴可以去查查资料,了解一下 ejb 规范里面的三种类型的 bean。这里说一下什么是无状态的 bean,什么是 bean 的状态。

有状态就是有数据存储功能。有状态对象 (Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。无状态就是一次操作,不能保存数据。无状态对象 (Stateless Bean),就是没有实例变量的对象. 不能保存数据,是不变类,是线程安全的。其

正文完
 0

设计模式单例模式

6次阅读

共计 2796 个字符,预计需要花费 7 分钟才能阅读完成。

单例模式

有时候又称“单件模式”或者“单态模式”,是 Java 中比较常见的创建型设计模式,他的最主要的目的是保证一个类只有一个实列,并提供一个访问它的全局访问点。比较经典的例子就是 Windows 系统中的“回收站”。

主要适用情况:

  1. 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。
  2. 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。

单例模式具有以下几个特征:

  • 单例类只能有一个实例;
  • 单例类必须自己创建自己的唯一实例;
  • 单例类必须给所有其他对象提供这一实例;

那么怎么确保只能有一个实例?这其实就是单例模式实现的核心,主要实现思路是:

  1. 私有化构造方法,防止外界创建该类的对象。
  2. 提供静态实例和获取实例的静态方法,向外部对象提供单例类对象。

单例模式实现

常见的单例模式实现方式:

饿汉式 ”实现方式:

public class Singleton {

    // 在自己内部定义自己的一个实例,只供内部调用
    private static final Singleton singleton = new Singleton();
    
    /// 私有化构造函数,避免外界创建该类的对象
    private Singleton() {}
    
    // 这里提供了一个供外部访问本 class 的静态方法,可以直接访问
    public static Singleton getInstance() {return singleton;}

}

另一种“ 饿汉式 ”实现方式:

public class Singleton {

    // 在自己内部定义自己的一个实例,只供内部调用
    private static final Singleton singleton;

    static {singleton = new Singleton();
    }

    /// 私有化构造函数,避免外界创建该类的对象
    private Singleton() {}

    // 这里提供了一个供外部访问本 class 的静态方法,可以直接访问
    public static Singleton getInstance() {return singleton;}

}

“饿汉式”单例基于 classloder 机制避免了多线程的同步问题,不过,singleton 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

懒汉式 ”实现方式:

public class Singleton {

    // 在自己内部定义自己的一个实例,只供内部调用
    private static Singleton singleton ;
    
    /// 私有化构造函数,避免外界创建该类的对象
    private Singleton() {}

    // 这里提供了一个供外部访问本 class 的静态方法,可以直接访问
    public static Singleton getInstance() {if (null == singleton)
            singleton = new Singleton();
        return singleton;
    }
    
}

上面这种“懒汉式”单例在多线程的情况下可能会出现问题,想象下,如果两个线程同时调用 getInstance() 方法,都出现 singleton 为空,因此可能出现多个单例实例对象,违背了单例原则,为了防止这种情况,一般都是通过添加同步锁来解决这样的问题

通常加锁情况:

1. 将方法进行同步

public class Singleton {

    // 在自己内部定义自己的一个实例,只供内部调用
    private static Singleton singleton ;

    /// 私有化构造函数,避免外界创建该类的对象
    private Singleton() {}

    // 这里提供了一个供外部访问本 class 的静态方法,可以直接访问
    public static synchronized  Singleton getInstance() {if (null == singleton)
            singleton = new Singleton();
        return singleton;
    }

}

这种写法能够在多线程中很好的工作,而且看起来它也具备很好的 lazy loading,但是,遗憾的是,效率很低,99% 情况下不需要同步。每次获取单例对象的时候都需要同步,如果多个线程同时获取单例对象,将需要花比较长的时间,其实真正需要同步的是单例对象实第一次例化时需要同步,一旦实例化后就不需要同步了。

2. 将对象进行同步

public class Singleton {

    // 在自己内部定义自己的一个实例,只供内部调用
    private volatile static Singleton singleton;

    /// 私有化构造函数,避免外界创建该类的对象
    private Singleton() {}

    // 这里提供了一个供外部访问本 class 的静态方法,可以直接访问
    public static Singleton getInstance() {if (null == singleton)
            synchronized (Singleton.class) {
                // 同步锁后,再进行一次 singleton 对象的判断
                if (null == singleton) {
                    // 如果单例类的对象为空,则实例化单例类的对象
                    singleton = new Singleton();}
            }

        return singleton;
    }

}

这也是比较常见的一种加锁模式,常称作“双重锁的形式”。上面这些单例模式在反射时,使用多个类加载器时,可能倒是单例失效,产生多个实例,因此可以将创建实例过程对外部隔离

3. 静态内部类单例模式

public final class Singleton {

    /// 私有化构造函数,避免外界创建该类的对象
    private Singleton() {}

    // 这里提供了一个供外部访问本 class 的静态方法,可以直接访问
    public static Singleton getInstance() {return SingletonHolder.mSington;}

    private static class SingletonHolder{private static final Singleton mSington = new Singleton();
    }

}

第一次加载 Singleton 类时不会去初始化 mSington,只有第一次调用 getInstance(),虚拟机加载 SingletonHolder 并初始化 mSington,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。所以,推荐使用静态内部类单例模式。

简单总结

  • 单例模式确保程序中一个类最多只有一个实例;
  • 单例模式也提供访问这个实例的全局点;
  • 在 Java 中实现单例模式需要私有化构造器、一个静态方法和一个静态变量;
  • 确定在性能和资源上的限制,然后小心的选择适当的方案来实现单例,以解决多线程的问题;
  • 如果使用多个类加载器,可能导致单例失效而产生多个实例
正文完
 0

设计模式--单例模式

6次阅读

共计 3328 个字符,预计需要花费 9 分钟才能阅读完成。

单例模式

定义:

确保某一个类只有一个实例对象,并且该对象是自行实例化的,通过统一的接口向整个系统提供这个实例对象。

使用场景:

避免产生多个对象消耗过多的资源(比如该对象需要用到 IO,Database 等等),或者某个类的实例化对象应该只有一个的情况。

因为内存中只有一个实例对象的存在,减少了内存开支,同时,如果该对象的产生需要较多资源的时候(内部需要依赖其他对象 …),我们可以采取只生成一个对象,然后让这个对象永久驻留在内存中的方式实现。

如果需要定义大量的静态常量和静态方法,也可以采用单例模式实现。

关键点:

1.构造函数不对外开放,一般为 private。

2.通过一个 static 方法或者枚举返回给外部单例对象。

3.在多线程的条件下也能保证只有一个单例对象。

4.确保单例类对象再反序列化的时候不会创建新的对象。

实现方式:

1. 饿汉单例模式

public class Singleton {private static Singleton instance = new Singleton();

    private Singleton(){}

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

优点:实现简单,在类加载的时候完成了初始化工作,避免了多线程同步问题。

缺点:没有实现懒加载,如果这个单例对象没有被使用过,但是对应的类却加载到内存中的话,也会白白的占用不必要的内存。

2. 懒汉单例模式

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

优点:实现了懒加载,在用到单例对象的时候再对其进行初始化,一定程度上节约了资源。

缺点:getInstance 挂了一把锁,每次获取这个单例对象都需要同步,不管是不是并发情况下,都会早成不必要的同步开销。

3.DCL 双重检查锁单例模式

懒汉单例模式中,我们并不需要整个 getInstance 方法都是同步的,我们只需要确保再 instance 创建的时候,进行同步即可。

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

优点:线程安全,懒加载,执行效率高,只有在 instance 为 null 的时候才会有同步开销。

缺点:

Double-Checked Lock 看起来是非常完美的。但是根据 Java 的语言规范,上面的代码并非绝对可靠。
出现上述问题, 最重要的 2 个原因如下:

1, 编译器优化了程序指令, 以加快 cpu 处理速度.
2, 多核 cpu 动态调整指令顺序,允许指令乱序执行, 以加快并行运算能力.

问题出现的顺序:

1, 线程 A, 发现对象未实例化, 准备开始实例化

2, 由于 编译器优化了程序指令, 允许对象在构造函数未调用完前, 将共享变量的引用指向部分构造的对象, 虽然对象未完全实例化, 但已经不为 null 了.

3, 线程 B, 发现部分构造的对象已不是 null, 则直接返回了该对象(此时它为 null 本应该先创建再返回却直接返回了)。

通俗来说,如果线程 A 的指令发现 instance 为 null,则会去执行初始化的指令,初始化指令最终翻译成汇编指令可能是如下三个部分:

①为内存对象分配内存

②构造函数初始化成员字段

③将创建的对象指定到分配的内存空间中

如果 123 顺序执行是没有问题的,但是可能存在 132 乱序执行的情况,如果 3 执行完成,CPU 切换到了另一个线程,同样执行 getInstance 方法去获取单例对象,单例对象不为空,但是获取到的对象确实不正确的。

这就是 DCL 失效问题。

改进的办法是,为 instance 加上 volatile 修饰符,保证对其修改其它线程立即可见。

private volatile static Singleton instance = null;

虽然 volatile 又需要额外的性能开销,但是相比安全性,这个开销是值得的。

静态内部类单例模式

public class Singleton{private Singleton(){}
    
    public static Singleton getInstance(){return SingletonHolder.sInstance;}
    
    private static class SingletonHolder{private static final Singleton sInstance = new Singleton();
    }
}

根据类加载机制,对于内部类而言,只有再需要的时候才会加载,也就是说位于 SingletonHolder 中的 sInstance 只有在第一次调用到 getInstance 的时候,才会被创建,从而既实现了懒加载,也能够确保线程安全(由 JVM 确保,在类加载的时候,只有一个线程会执行类加载动作,也就是创建单例对象只会由一个线程完成),推荐使用。

枚举单例

public class EnumSingleton{private EnumSingleton(){}
    public static EnumSingleton getInstance(){return Singleton.INSTANCE.getInstance();
    }
    
    private static enum Singleton{
        INSTANCE;
        private EnumSingleton singleton;
        // 在加载的时候进行初始化,JVM 保证该方法只会被调用一次。private Singleton(){singleton = new EnumSingleton();
        }
        public EnumSingleton getInstance(){return singleton;}
    }
}

枚举类和普通类是一样的,但是不同的是枚举实例的创建默认是线程安全的,并且在任何情况下都是只有一个实例对象存在,即便是序列化反序列化也是。

单例模式对 (反) 序列化的改进

上面所有的单例模式,除了借助枚举来实现外,都存在一个缺点,也就是第四个关键点,我们需要保证单例对象在序列化和反序列化中可以保证对象的一致性,也就是不能通过反序列化违反单例的系统中只存在一个唯一对象的规定。

当然,这个情况的前提是,我们的单例类实现了序列化接口。

通过类的 readResolve 函数,开发人员可以控制反序列化过程,杜绝在反序列化的时候生成新对象:

public final class Singleton implements Serializable{
    private static final long serialVersionUID = 0L;
    private static final Singleton INSTANCE = new Singleton();
    
    private Singleton(){}
    
    public static Singleton getInstance(){return INSTANCE;}
    
    private Object readResolve(){return INSTANCE;}
}

同样的,该方法因为需要用到序列化,自然是要符合序列化的要求,即内部字段也是要可序列化的。

我们将 serialVersionUID 置为 fianl,是为了保证在修改了单例类的内部情况的时候,反序列化也不会抛出 InvalidClassException 异常,只会将新修改的字段置为默认值。

单例模式的缺点:

优点在开头已经说明了,单例模式的缺点在于它一般没有接口,扩展困难,基本上修改源代码是扩展单例模式的唯一方法。再有,如果单例对象持有 Context,很容易引发内存泄露问题,所以一般是用 ApplicationContext。

正文完
 0

设计模式单例模式

6次阅读

共计 842 个字符,预计需要花费 3 分钟才能阅读完成。

单例模式是 确保一个类只有一个实例,自行实例化并向系统提供这个实例
一个类只有一个实例对象,避免了重复实例的频繁创建和销毁降低了资源消耗
并且共用一个对象有利于数据同步,例如 WINDOWS 的任务管理器、回收站、网站的计数器、线程池对象、配置文件的读取对象等
两种创建方式:
1. 饿汉单例模式(最常用):单例实例在类装载时就构建,急切初始化。(预先加载法)
特点:线程安全、在类加载的同时已经创建好一个静态对象,调用时反应速度快,有可能从没用到,有一点点的资源浪费

// 饿汉单例模式 Demo
public class SingletonTest {
    //1. 私有化该类的构造方法(不让别人 new,只能自己 new)private SingletonTest() {}
    //2. 自己内部 new 一个对象
    public static SingletonTest instance = new SingletonTest();
    //3. 给一个 get 方法,让外界取它
    public SingletonTest getInstance() {return instance;}
}

2. 懒汉单例模式:单例实例在第一次被使用时构建,延迟初始化。

// 懒汉单例模式 Demo
public class SingletonTest2 {
    //1. 私有化该类的构造方法(不让别人 new,只能自己 new)private SingletonTest2() {}
    //2. 自己内部维护一个 null 对象(只要被调用一次就不再是了)public static SingletonTest2 instance = null;
    //3. 给一个 get 方法,让外界取它,只有有人用才会 new 一个对象出来
    public SingletonTest2 getInstance() {if (instance == null) {
        //TODO 多线程下可能会出现重复 new 的情况
        instance = new SingletonTest2();}
        return instance;
    }
}

总结: 两种模式各有所长 一种是时间换空间 一种是空间换时间 根据具体场景使用

正文完
 0