一、单例
- what--单例模式保证了系统内存中该类只存在一个对象
- how--核心步骤是构造函数私有化
- why--节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
单例模式使用的场景:需要频繁的进行创建和销毁的对象;创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象;频繁访问数据库或文件的对象(比如数据源、session 工厂等)
1.1 饿汉--静态常量
//饿汉式(静态常量)
class SingleTon{
//1. 构造器私有化,外部不能new
private SingleTon(){}
//2.本类内部创建对象实例
private final static SingleTon instance = new SingleTon();
//3. 提供一个公有的静态方法,返回实例对象
public static SingleTon getInstance() {
return instance;
}
}
1.2 饿汉--静态代码块
//饿汉式(静态代码块)
class Singleton2 {
//1. 构造器私有化, 外部不能new
private Singleton2() {}
//2.本类内部创建对象实例
private static Singleton2 instance;
// 3. 在静态代码块中,创建单例对象
static {
instance = new Singleton2();
}
//4. 提供一个公有的静态方法,返回实例对象
public static Singleton2 getInstance() {
return instance;
}
}
饿汉优缺点说明:
优点:这种写法比较简单,就是在类装载的时候就完成实例化(还没用就实例化,故称为饿汉式)。避免了线程安全问题。
缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
1.3 饿汉--枚举
public enum Singleton {
INSTANCE;
}
如上代码等同于
- 每个枚举都是通过 Class 在内部实现的,且所有的枚举值都是 public static final 的
- 枚举跟普通类一样可以用自己的变量、方法、抽象方法和构造函数,且构造函数只能使用 private 访问修饰符,所以外部无法调用。
public enum Singleton {
public static final INSTANCE = new Singleton();
private Singleton (){}
}
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
1.4 懒汉式--线程不安全
单线程可用,多线程不安全
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
1.5 懒汉式--线程安全
class SingleTon4{
private static SingleTon4 instance;
private SingleTon4(){}
//提供一个静态的公有方法,synchronized 加入同步处理的代码,解决线程安全问题
public static synchronized SingleTon4 getInstance(){
if (instance==null){
instance = new SingleTon4();
}
return instance;
}
}
- 优点:解决了线程安全问题
- 缺点:效率太低了,每个线程在想获得类的实例时候,执行 getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。方法进行同步效率太低
- 结论:在实际开发中,不推荐使用这种方式
1.6 懒汉式-双重检查+锁
再来讨论一下懒汉模式中加锁的问题,对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查+锁模式
public class Singleton {
//私有构造方法
private Singleton() {}
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为null
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题。上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查+锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
1.7 懒汉--静态内部类
- JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。
- 类的static属性,在类加载--初始化阶段进行赋值,并且是由JVM保证线程安全。
public class Singleton {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
- 第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance方法时,虚拟机才会去加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
- 静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。