23 种设计模式之单例模式
文章目录
一、认识
①一句话来说就是,某个类只能有一个实例,提供一个全局的访问点。
②单例模式的要点有三个:
- 一是某个类只能有一个实例;
- 二是它必须自行创建这个实例;
- 三是它必须自行向整个系统提供这个实例。
③使用Singleton的好处还在于可以节省内存,因为它限制了实例的个数,有利于Java垃圾回收(garbage collection),而且确保所有对象都访问唯一实例。但是不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。用单例模式,就是在适用其优点的状态下使用
二、UML类图
UML说明:
- 1.构造方法私有化:可以使得该类不被实例化即不能被new
- 2.在类本身里创建自己的对象
- 3.提供一个公共的方法供其他对象访问
三、代码实现
1. 饿汉式
第一种:
public class Singleton {
/**
* static:
* ①表示共享变量,语意符合
* ②使得该变量能在getInstance()静态方法中使用
* final:
* ①final修饰的变量值不会改变即常量,语意也符合,当然不加final也是可以的
* ②保证修饰的变量必须在类加载完成时就已经进行赋值。
* final修饰的变量,前面一般加static
*/
private static final Singleton singleton = new Singleton();
/**
* 私有化构造方法,使外部无法通过构造方法构造除singleton外的类实例
* 从而达到单例模式控制类实例数目的目的
*/
private Singleton(){}
/**
* 类实例的全局访问方法
* 因为构造方法以及被私有化,外部不可能通过new对象来调用其中的方法
* 加上static关键词使得外部可以通过类名直接调用该方法获取类实例
* @return
*/
public static Singleton getSingleton() {
return singleton;
}
}
第二种
public class SingletonStatic {
private static final SingletonStatic singletonStatic;
/**
* 和第一种没有什么区别,这种看起来高大上面试装逼使用
*/
static {
singletonStatic = new SingletonStatic();
}
private SingletonStatic() {}
public static SingletonStatic getSingletonStatic(){
return singletonStatic;
}
}
说明:
- 优点: 一般使用static和final修饰变量(具体作用已经在代码里描述了),只在类加载时才会初始化,以后都不会,线程绝对安全,无锁,效率高。
- 缺点: 类加载的时候就初始化,不管用不用,都占用空间,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单)
注: 这里有两个小知识点:
- 如果是final非static成员,必须在构造器、代码块、或者直接定义赋值
- 如果是final static 成员变量,必须直接赋值 或者在静态代码块中赋值
2. 懒汉式
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton() {
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
说明:
- 优点: 在外部需要使用的时候才进行实例化,不使用的时候不会占用空间。
- 缺点: 线程不安全。看上去,这段代码没什么明显问题,但它不是线程安全的。假设当前有N个线程同时调用getInstance()方法,由于当前还没有对象生成,所以一部分同时都进入if语句new
Singleton(),那么就会由多个线程创建多个user对象。
3. 线程安全的懒汉式
public class Singleton {
private static Singleton singleton;
private Singleton(){};
private static synchronized Singleton getSingleton(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
说明:
- 优点: 解决了懒汉式线程不安全的问题
- 缺点: 线程阻塞,影响性能。
4. DCL单例 - 高性能的懒汉式
public class Singleton {
/*volatile在这里发挥的作用是:禁止指令重排序(编译器和处理器为了优化程序性能
* 而对指令序列进行排序的一种手段。)
* singleton = new Singleton();这句代码是非原子性操作可分为三行伪代码
* a:memory = allocate() //分配内存,在jvm堆中分配一段区域
* b:ctorInstanc(memory) //初始化对象,在jvm堆中的内存中实例化对象
* c:instance = memory //赋值,设置instance指向刚分配的内存地址
* 上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。
* 重排序是为了优化性能,但是不管怎么重排序,在单线程下程序的执行结果不能被改变
* 保证最终一致性。而在多线程环境下,可能发生重排序,会影响结果。
* ①若A线程执行到代码singleton = new Singleton()时;
* ②同时若B线程进来执行到代码到第一层检查if (singleton == null)
* ③当cpu切换到A线程执行代码singleton = new Singleton();时发生了指令重排序,
* 执行了a-b,没有执行c,此时的singleton对象只有地址,没有内容。然后cpu又切换到了B线程,
* 这时singleton == null为false(==比较的是内存地址),
* 则代码会直接执行到了return,返回一个未初始化的对象(只有地址,没有内容)。
* */
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getSingleton() {
/*第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
* ①当多个线程第一次进入,所有线程都进入if语句
* ②当多个线程第二次进入,因为singleton已经不为null,因此所有线程都不会进入if语句,
* 即不会执行锁,从而也就不会因为锁而阻塞,避免锁竞争*/
if (singleton == null) {
/*第一层锁,保证只有一个线程进入,
* ①多个线程第一次进入的时候,只有一个线程会进入,其他线程处于阻塞状态
* 当进入的线程创建完对象出去之后,其他线程又会进入创建对象,所以有了第二次if检查
* ②多个线程第二次是进入不到这里的,因为已被第一次if检查拦截*/
synchronized (Singleton.class) {
/*第二层检查,防止除了进入的第一个线程的其他线程重复创建对象*/
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
说明: 代码注释已详细讲解volatile在该单例模式的作用,已经双重锁的作用。
- 优点: 解决了线程阻塞的问题
- 缺点: 多个线程第一次进入的时候会造成大量的线程阻塞,代码不够优雅。
5. 静态内部类的方式
public class Singleton {
private Singleton(){}
private static class LayzInner{
private static Singleton singleton = new Singleton();
}
public static Singleton getSingleton(){
return LayzInner.singleton;
}
}
说明:
- 优点: 第一次类创建的时候加载,避免了内存浪费,不存在阻塞问题,线程安全,唯一性
- 缺点: 序列化-漏洞:反射,会破坏内部类单例模式
6. 枚举单例模式
public enum EnumSingleton {
INSTANCE;
private Singleton singleton;
EnumSingleton(){
singleton = new Singleton();
}
public Singleton getSingleton(){
return singleton;
}
}
说明: 单元素的枚举类型已经成为实现Singleton的最佳方法,无法反射创建对象,是特殊的饿汉式。
7. 静态内部类升级版
借鉴枚举单例的内部实现的方式
public class Singleton {
private Singleton(){
if(LayzInner.singleton != null){
throw new RuntimeException("不能够进行反射!");
}
}
private static class LayzInner{
private static Singleton singleton = new Singleton();
}
public static Singleton getSingleton (){
return LayzInner.singleton;
}
}
说明:
- 优点:第一次类创建的时候加载,避免了内存浪费,不存在阻塞问题,线程安全,唯一性,解决了反射会破坏内部类单例模式的问题
- 缺点:不是官方的
8. 容器式单例
public class Singleton {
private Singleton() {
}
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
public static Object getBean(String className) {
synchronized (ioc) {
if (ioc.containsKey(className)) {
Object o = null;
try {
o = Class.forName(className).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return o;
} else {
return ioc.get(className);
}
}
}
}
说明: Spring ioc 单例 是懒汉式 枚举上的升级
9. ThreadLocal单例
package com.rf.designPatterns.singleton.threadLocalSingleton;
/**
* @description: ThreadLocal线程单例,为每一个线程提供一个对象,在访问的时候相互不影响
*/
public class ThreadLocalSingleton {
//创建ThreadLocal实例对象,并且重写initialValue方法
private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton =
new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
//构造方法
private ThreadLocalSingleton(){
}
//提供对外暴露的方法,获取ThreadLocalSingleton对象
public static ThreadLocalSingleton getInstance(){
return threadLocalSingleton.get();
}
}
说明:
局部单例模式:某一个线程里唯一
①ThreadLocal的作用呢,是提供线程内的局部变量,在多线程环境访问时,能保证各个线程内的ThreadLocal变量各自独立。也就是说每个线程的ThreadLocal变量是自己专用的,其他线程是访问不到的。
②ThreadLocal最常用于在多线程环境下存在对非线程安全对象的并发访问,而且该对象不需要在线程内共享,如果对该对象加锁,会造成大量线程阻塞影响程序性能,这时候就可以使用ThreadLocal来使每个线程都持有该对象的副本,这是典型的空间换取时间从而提高执行效率的方式。例如项目里经常使用的SimpleDateFormat日期格式化对象,该对象是线程不安全的,而且不需要在线程内共享,因此可以使用ThreadLocal保证其线程安全。
本文含有隐藏内容,请 开通VIP 后查看