单例模式

1. 单例模式介绍

  1. 所谓类的单例设计模式, 就是采取一定的方法保证在整个的软件系统中, 对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
  2. 比如 HibernateSessionFactory, 它充当数据存储源的代理, 并负责创建 Session 对象。SessionFactory 并不是轻量级的, 一般情况下, 一个项目通常只需要一个 SessionFactory 就够,这是就会使用到单例模式。

单例设计模式八种方式

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

2. 饿汉式(静态常量)

基本步骤:

  1. 构造器私有化 (防止 new)
  2. 类的内部创建对象
  3. 向外暴露一个静态的公共方法: getInstance()

代码实现

public class SingletonTest01 {

	public static void main(String[] args) {
		// 测试
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
	}

}

//饿汉式(静态变量)
class Singleton {

	// 1. 构造器私有化, 外部不能new
	private Singleton() {

	}

	// 2.本类内部创建对象实例
	private final static Singleton instance = new Singleton();

	// 3. 提供一个公有的静态方法,返回实例对象
	public static Singleton getInstance() {
		return instance;
	}

}

饿汉式(静态常量)的优缺点说明

优点:

  1. 这种方式基于 Classloder 机制避免了多线程的同步问题(一个类只会加载一次,初始化一次), 不过, instance 在类装载时就实例化, 在单例模式中大多数都是调用 getInstance() 方法获取单例对象, 但是导致类装载的原因有很多种, 因此不能确定有其他的方式(或者其他的静态方法) 导致类装载, 这时候初始化单例对象,就没有达到 lazy loading 的效果
  2. 这种写法比较简单, 就是在类装载的时候就完成实例化。 避免了线程同步问题。

缺点:

  1. 在类装载的时候就完成实例化, 没有达到 Lazy Loading 的效果。 如果从始至终从未使用过这个实例, 则会造成内存的浪费

结论: 这种单例模式可用, 可能造成内存浪费

3. 饿汉式(静态代码块)

步骤:

  1. 构造器私有化,外部不能 new
  2. 在本类内部的静态代码块中,创建单例对象
  3. 提供一个公有的静态方法,返回实例对象

代码实现:

public class SingletonTest02 {

	public static void main(String[] args) {
		// 测试
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
	}

}

//饿汉式(静态变量)
class Singleton {

	// 1. 构造器私有化, 外部不能new
	private Singleton() {

	}

	// 2.本类内部创建对象实例
	private static Singleton instance;

	static { // 在静态代码块中,创建单例对象
		instance = new Singleton();
	}

	// 3. 提供一个公有的静态方法,返回实例对象
	public static Singleton getInstance() {
		return instance;
	}

}

饿汉式(静态代码块)的优缺点说明

  1. 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
  2. 结论: 这种单例模式可用,但是可能造成内存浪费

4. 懒汉式(线程不安全)

步骤:

  1. 构造器私有化,外部不能 new
  2. 在本类内部的 getInstance() 静态方法中,判断单例对象是否为空
    • 如果为空,则创建单例对象并返回
    • 如果不为空,则直接返回此对象

代码实现:

public class SingletonTest03 {

	public static void main(String[] args) {
		System.out.println("懒汉式1 , 线程不安全~");
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
	}

}

class Singleton {
	private static Singleton instance;

	private Singleton() {
	}

	// 提供一个静态的公有方法,当使用到该方法时,才去创建 instance
	// 即懒汉式
	public static Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

懒汉式(线程不安全)的优缺点说明

  1. 优点:起到了 Lazy Loading 的效果, 但是只能在单线程下使用。
  2. 缺点: 如果在多线程下, 一个线程进入了 if (singleton == null) 判断语句块, 还未来得及往下执行, 另一个线程也通过了这个判断语句, 这时便会产生多个实例。 所以在多线程环境下不可使用这种方式
  3. 结论: 在实际开发中, 不要使用这种方式

5. 懒汉式(同步方法)

步骤:

  1. 构造器私有化,外部不能 new
  2. 在本类内部的 getInstance() 静态同步方法中,判断单例对象是否为空
    • 如果为空,则创建单例对象并返回
    • 如果不为空,则直接返回此对象

代码实现:

public class SingletonTest04 {

	public static void main(String[] args) {
		System.out.println("懒汉式2 , 线程安全~");
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
	}

}

// 懒汉式(线程安全,同步方法)
class Singleton {
	private static Singleton instance;

	private Singleton() {
	}

	// 提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
	// 即懒汉式
	public static synchronized Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

懒汉式(同步方法)的优缺点说明

  1. 优点: 解决了线程安全问题
  2. 缺点: 效率太低了, 每个线程在想获得类的实例时候, 执行 getInstance() 方法都要进行同步。 而其实这个方法只执行一次实例化代码就够了, 后面的想获得该类实例, 直接 return 就行了。 方法进行同步效率太低
  3. 结论: 在实际开发中, 不推荐使用这种方式

6. 懒汉式(同步代码块)

步骤:

  1. 构造器私有化,外部不能 new
  2. 在本类内部的 getInstance() 静态方法中,先判断对象是否为空
    • 如果为空,则加锁创建单例对象,并返回
    • 如果不为空,则直接返回此对象

代码实现:

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

此方式的问题

  1. 这种方式,本意是想在不为空时直接返回,为空时进行同步,
  2. 但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了 if (singleton == null) 判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例
  3. 结论:在实际开发中, 不能使用这种方式

7. 懒汉式(双重检查)

步骤 :

  1. 构造器私有化,外部不能 new
  2. 在本类内部的 getInstance() 静态方法中,先判断对象是否为空
    • 如果为空,则先加锁,再判断此单例对象是否为空,如果还为空,才创建对象
    • 如果不为空,则直接返回此对象
  3. 注意:单例变量需要使用 volatile 关键字进行修饰,保证内存可见性,以及防止指令重排序

代码实现:

public class SingletonTest06 {

	public static void main(String[] args) {
		System.out.println("双重检查");
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
	}

}

// 懒汉式(线程安全,同步方法)
class Singleton {
	private static volatile Singleton instance;

	private Singleton() {
	}

	// 提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
	// 同时保证了效率, 推荐使用
	public static Singleton getInstance() {
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

懒汉式(双重检查)的优点说明

  1. Double-Check 概念是多线程开发中常使用到的, 如代码中所示, 我们进行了两次 if (singleton == null) 检查, 这样就可以保证线程安全了
  2. 这样, 实例化代码只用执行一次, 后面再次访问时, 判断 if (singleton == null), 直接 return 实例化对象, 也避免的反复进行方法同步
  3. 线程安全; 延迟加载; 效率较高
  4. 结论: 在实际开发中, 推荐使用这种单例设计模式

volatile的作用

jvm在执行java指令时,有可能会发生指令重排, 那么在此例中,在第一个线程 对单例进行赋值时,因为指令重排的原因, 导致类的初始化和赋值操作颠倒, 那么如果此时 第二个线程在获取时,判断此时不为null,直接返回,但是此时返回的对象是还没有进行初始化的类,那么将有不可预期的后果,volatile的作用保证线程可见性,也防止了对instance 变量操作的 指令重排的可能性

8. 静态内部类

步骤:

  1. 构造器私有化,外部不能 new
  2. 在本类内部新增一个静态内部类,封装一个单例对象,用于实现单例模式
  3. 静态内部类的实现方式本质是利用类加载的同步机制,保证单例对象的线程安全,并且该方式能保证该单例对象的懒加载机制,因为只有用到静态内部类时,才会加载该静态内部类以及单例对象
  4. 在本类内部提供一个静态方法 getInstance() 用于返回静态内部类中的单例对象

代码实现:

public class SingletonTest07 {

	public static void main(String[] args) {
		System.out.println("使用静态内部类完成单例模式");
		Singleton instance = Singleton.getInstance();
		Singleton instance2 = Singleton.getInstance();
		System.out.println(instance == instance2); // true
		System.out.println("instance.hashCode=" + instance.hashCode());
		System.out.println("instance2.hashCode=" + instance2.hashCode());
	}

}

// 静态内部类完成, 推荐使用
class Singleton {	
	//构造器私有化
	private Singleton() {}
	
	//写一个静态内部类,该类中有一个静态属性 Singleton
	private static class SingletonInstance {
		private static final Singleton INSTANCE = new Singleton(); 
	}
	
	//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
	public static Singleton getInstance() {
		return SingletonInstance.INSTANCE;
	}
}

懒汉式(静态内部类)的优点说明

  1. 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
  2. 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance()方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
  3. 类的静态属性只会在第一次加载类的时候初始化,所以在这里, JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
  4. 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高。
  5. 结论:推荐使用。

9. 枚举

代码实现

public class SingletonTest08 {
	public static void main(String[] args) {
		Singleton instance = Singleton.INSTANCE;
		Singleton instance2 = Singleton.INSTANCE;
		System.out.println(instance == instance2);

		System.out.println(instance.hashCode());
		System.out.println(instance2.hashCode());

		instance.sayOK();
	}
}

//使用枚举,可以实现单例, 推荐
enum Singleton {
	INSTANCE; // 属性

	public void sayOK() {
		System.out.println("ok~");
	}
}

饿汉式(枚举)的优点说明

  1. 这借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化,反射重新创建新的对象。
  2. 这种方式是Effective Java作者Josh Bloch提倡的方式。如果用枚举去实现一个单例,属于饿汉模式。
  3. 结论:推荐使用

10. JDK中的单例模式

Rumtime 类单例模式

源码:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

11. 单例模式注意事项

  1. 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
  2. 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new
  3. 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即: 重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、 session 工厂等)
posted @ 2021-01-08 18:04  哈哈丶丶  阅读(48)  评论(0编辑  收藏  举报