1. 单例模式

341 阅读7分钟

一. 单例模式

单例模式只涉及到一个类,由该类本身来创建自己的对象,同时确保只会创建出一个对象. 这个类对外提供静态方法来访问这个唯一的对象,可以直接访问,但是不允许实例化.

注意:

  1. 单例类只能有一个实例
  2. 单例类必须自己创建唯一的实例对象
  3. 单例类需要对外提供静态方法来访问该对象
  4. 工具类对象、频繁访问数据库或文件的对象(数据源、session工厂等等)

二. 使用场景

  1. 需要频繁创建和销毁的对象
  2. WEB中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来.
  3. 创建的一个对象需要消耗的资源过多,比如I/O与数据库的连接等.

三. 实现方式

  1. 饿汉式-静态常量
  2. 饿汉式-静态代码块
  3. 懒汉式-线程不安全
  4. 懒汉式-线程安全(同步方法)
  5. 懒汉式-线程安全(同步代码块)
  6. 双重检查
  7. 静态内部类
  8. 枚举

四. UML

单例.png

五. 饿汉式-静态常量

优点:在类装载的时候就完成实例化,基于classloader机制避免了多线程的同步问题

缺点:对象在类装载的时候就实例化,不管是否会使用到都进行了实例化,没有起到懒加载的作用

public class Singleton {
    //(1)创建一个唯一的单例对象
    private final static Singleton singleton = new Singleton();
    //(2)构造器私有化,禁止new
    private Singleton() {
    }
    //(3)对外提供一个静态方法的公共方法
    public static Singleton getSingleton() {
        return singleton;
    }
}

六. 饿汉式-静态代码块

优点:在类装载的时候,静态代码块完成对象的实例化,同样基于classloader机制避免了多线程的同步问题

缺点:对象实例在类装载的时候就实例化,没有起到懒加载的作用,也会造成内存浪费

public class Singleton {
    //(1)声明一个唯一的单例对象
    private static Singleton singleton;
    //(2)通过静态代码块实例化对象
    static {
        singleton = new Singleton();
    }
    //(3)构造器私有化,禁止new
    private Singleton() {
    } 
    //(4)对外提供一个静态方法的公共方法
    public static Singleton getSingleton() {
        return singleton;
    }
}

七. 懒汉式-线程不安全

优点:懒加载,只有使用单例对象的时候才会通过getSingeton()方法进行实例化

缺点:线程不安全,多线程情况下同时进入if判断语句,那么对象就会被实例化多次

public class Singleton {
    //(1)声明一个唯一的单例对象
    private static Singleton singleton;
    //(2)构造器私有化,禁止new
    private Singleton() {
    }
    //(3)对外提供一个静态方法的公共方法,调用时才进行对象实例化
    public static Singleton getSingleton() {
        //不安全:如果多个线程同时进入方法体中,singleton就被创建了多次
        if (singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

八. 懒汉式-线程安全(同步方法)

优点:懒加载,同时线程安全,避免了线程调度导致的不安全问题

缺点:每次调用getInstance()方法时线程都被synchronized关键字锁住了,会引起线程阻塞,给静态方法加锁相当于给类加锁,锁的粒度太大,因此效率低性能差

public class Singleton {
    //(1)声明一个唯一的单例对象
    private static Singleton singleton;
    //(2)构造器私有化,禁止new
    private Singleton() {
    }
    //(3)对外提供一个静态方法的公共方法,调用时完成对象实例化
    //synchronized:同步方法,确保同一时间只会有一个线程能够调用该方法去实例化对象
    public static synchronized Singleton getSingleton() {
            if (singleton == null) {
                singleton = new Singleton();
            }
            return singleton;
    }
}

九. 懒汉式-线程不安全(同步代码块)

优点:懒加载,只有使用单例对象的时候才会通过getSingeton()方法进行实例化

缺点:线程不安全,多线程情况下同时进入if判断语句,那么对象就会被实例化多次

public class Singleton {
    //(1)声明一个唯一的单例对象
    private static Singleton singleton;
    //(2)构造器私有化,禁止new
    private Singleton() {
    }
    //(3)对外提供一个静态方法的公共方法,调用时完成对象实例化
    public static Singleton getSingleton() {
        if (singleton == null) {
            // 多线程进入if语句中依然线程不安全,而且加锁效率低
            synchronized (Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

十. 双重检查-volatile

优点:volatile关键字保证内存可见性,一个线程实例化对象其他线程可见,第二次if判断的时候就会判断单例对象已经实例化,同时volatile是轻量级的同步机制粒度更小效率更高.

volatile:可以理解为轻量级的synchronized,内部基于CSA算法保证内存可见性,同时多线程下防止指令重排序,但是并不保证原子性,CAS算法存在的隐患是ABA问题以及循环CAS导致影响线程效率.

public class Singleton {
    //(1)声明一个唯一的单例对象
    // volatile:当对象实例化其他线程可见
    private static volatile Singleton singleton;
    //(2)构造器私有化,禁止new
    private Singleton() {
    }
    //(3)对外提供一个静态方法的公共方法,调用时完成对象实例化
    public static Singleton getSingleton() {
        if (singleton == null) {
            // 确保只有一个线程会去实例化对象
            synchronized (singleton){
                // 其他线程可见,就不会再去实例化对象了
                if (singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

十一. 静态内部类

优点:当类加载的时候,静态内部类不会立即装载和实例化,当用到单例对象的时候才会加载静态内部类,实现了懒加载;同时类加载的时候是线程安全的,因为类的静态属性只有在第一次被加载的时候才会实例化,所以单例对象也只会被实例化一次

public class Singleton {
    //(1)创建静态内部类:完成单例对象的实例化
    static class Instance{
        private static final Singleton INSTANCE = new Singleton();
    }
    //(2)构造器私有化,禁止new
    private Singleton() {
    }
    //(3)对外提供一个静态方法的公共方法,调用时完成对象实例化
    public static Singleton getSingleton() {
        return Instance.INSTANCE;
    }
}

十二. 枚举

优点:枚举天生保证序列化单例,让JVM来帮我们保证线程安全和单一实例的问题;不仅线程安全,而且还能防止反序列化创建新的对象

public class Test{
    public static void main(String[] args) {
        System.out.println(Singleton.INSTANCE);
    }
}
enum Singleton {
    // 唯一的枚举实例
    INSTANCE;
}

原理分析:可以对Singleton反编译一下

在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中;反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象;

以下面枚举为例,序列化的时候只将INSTANCE这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同

public final class Singleton extends Enum<Singleton> {
    public static final Singleton INSTANCE;
    public static Singleton[] values();
    public static Singleton valueOf(String s);
    static {};
}

十三. 典型单例--Runtime

public class Runtime {
    private static final java.lang.Runtime currentRuntime = new Runtime();
    public static java.lang.Runtime getRuntime() {
        return currentRuntime;
    }
    private Runtime() {
    }
}

十四. 拓展--Spring的单例实现

大家可以阅读一下spring源码或者相关的博客

单例的获取顺利是singletonObjects——>earlySingletonObjects——>singletonFactories这样的三级层次

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
    private final Map<String, Object> earlySingletonObjects = new HashMap(16);
}

在singletonObjects中获取bean的时候,没有使用synchronized关键字,而在singletonFactories和earlySingletonObjects中的操做都是在synchronized代码块中完成的,正好和他们各自的数据类型对应,singletonObjects使用的使用ConcurrentHashMap线程安全,而singletonFactories和earlySingletonObjects使用的是HashMap,线程不安全.

从字面意思来讲:singletonObjects指单例对象的cache,singletonFactories指单例对象工厂的cache,earlySingletonObjects指提早曝光的单例对象的cache。以上三个cache构成了三级缓存,Spring就用这三级缓存巧妙的解决了循环依赖问题.