单例模式原来有这几种写法

一、简介

单例模式(Singleton Pattern),就是采取一定的方法保证在整个软件系统内,一个类只有一个对象实例,并且该类只提供一个取得其对象实例的方法。

单例模式的类也可以和一般的类一样,具有一般的数据和方法。一般的单例模式写法分为这三个步骤:

  • 私有化构造方法
  • 在类内部创建类的实例(私有的)
  • 提供唯一一个获取唯一实例的方法

二、不同写法

1.线程不安全的懒汉式

懒汉式,即延迟实例化(lazy instantiaze),当我们需要时才进行创建,我们不需要某个类的实例时,它就永远不会产生。具体代码如下:

class Singleton {
	// 声明一个静态变量来记录Singleton的唯一实例
	private static Singleton instance;

	// 构造方法私有化,防止类的外部使用new创建该类的对象
	private Singleton() {
	}
	// 提供公有的静态方法,当对象为null才实例化对象,并返回该实例
	public static Singleton getInstance() {
		// 如果instance不存在,则使用私用的构造器创建
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
//其他的方法
}

为什么说这种写法是线程不安全的呢?这是因为当有两个线程同时进行Singleton类的创建时,就会出现如下图的情况:
在这里插入图片描述
总结:这种情况下,一个线程进入了if(instance==null)语句块,还没来得及完成实例创建,另一个线程也进入到了这个语句块,这样就会产生两个实例。所以在多线程环境下这种写法是不可用的。

2.线程安全的懒汉式(同步方法)

要解决线程安全问题,只要把getInstance()方法改成同步(synchronized)方法就可以轻易解决。

class Singleton {
	// 声明一个静态变量来记录Singleton的唯一实例
	private static Singleton instance;

	// 构造方法私有化,防止类的外部使用new创建该类的对象
	private Singleton() {
	}
	// 提供公有的静态方法,当对象为null才实例化对象,并返回该实例
	public static synchronized Singleton getInstance() {
		// 如果instance不存在,则使用私用的构造器创建
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
//其他的方法
}

这里通过增加synchronized关键字到getInstance()方法中,迫使每个线程在进入这个方法前,需要等候别的线程离开该方法。(不会有多个线程同时进入该方法)

总结:这种写法虽然解决了线程安全问题,但是这会降低性能,因为每个线程要获得该类的实例时,都需要在getInstance()方法进行同步,然而实际上,只有第一次执行此方法时才真正需要同步。

为了符合大多数Java应用程序,我们需要确保单例模式在多线程环境下正常工作,但是上面的同步getInstance()方法的做法又会降低性能。

那么下面的写法都可以用于比较有效地解决多线程访问问题。

3.饿汉式(静态常量)

当Java应用程序总是需要创建并使用单例的实例,或者创建和运行方面的负担不太繁重时,就可以使用饿汉式(在类装载时就完成实例化)。

class Singleton {
	// 1.构造方法私有化,防止类的外部使用new创建该类的对象
	private Singleton() {}
	// 2.类内部创建对象,该实例对象作为静态常量
	private final static Singleton INSTANCE = new Singleton();

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

总结:利用这个写法,优点在于我们依赖JVM在加载这个类时就创建了唯一的实例对象,这就避免了线程同步问题。而缺点在于若在程序创建到销毁的过程都没用到该类,就造成了内存浪费。

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

这种写法将类的实例化过程放在静态代码块中,同样是在类被JVM加载时就完成实例化。同样是饿汉式,优缺点与上一个写法相同。

class Singleton {
	// 1.构造方法私有化,防止类的外部使用new创建该类的对象
	private Singleton() {}
	// 2.此处声明变量,该实例对象作为静态变量
	private static Singleton instance;
	static {//在静态代码块中完成实例化
		instance = new Singleton();
	}
	// 3.提供公有的静态方法,返回实例对象
	public static Singleton getInstance() {
		return instance;
	}
}

5.双重检查锁(DCL)

利用双重检查加锁(double checked locking) ,将锁的范围从方法移到方法内部。首先检查实例是否已经创建,当没有创建才进行线程同步 。这样一来,就只会在第一次创建实例时进行同步。

class Singleton {
	// 1.构造方法私有化,防止类的外部使用new创建该类的对象
	private Singleton() {}
	
	// 2.声明Singleton类型的静态变量
	private static volatile Singleton instance;

	// 3.提供公有的静态方法,当对象为null才实例化该类(加入synchronized关键字)
	public static Singleton getInstance() {
		if (instance == null) {// 如果对象为null,才进入同步区块
			synchronized (Singleton.class) {
				if (instance == null) {// 再检查一次,对象仍为null才初始化为Singleton实例。
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

这里复习一下volatile:
首先需要明确的是,volatile不能说是轻量级的synchronized,因为volatile不保证原子性(线程安全),只保证可见性,volatile只是轻量级的线程可见方式。

volatile关键字的两大特性:

  • 保证内存(线程)的可见性。当被volatile修饰的变量被修改时,JMM(Java内存模型)会把该线程本地内存(本地内存保存了主内存中的共享变量副本)中的变量强制刷新到主内存中去,并使其他线程中的缓存无效,需要重新在主内存中读取。(保证了变量被修改后,其他线程立刻知道)
  • 禁止指令重排。重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。

以上面的单例模式的双重检查为例,这条语句:instance = new Singleton(); 的执行,可以分为3条伪代码:

(a) memory = allocate() //分配内存

(b) ctorInstanc(memory) //初始化对象

(c) instance = memory //设置instance指向刚分配的地址

如果没有volatile关键字修饰该变量,上面的代码在编译运行时,可能会出现重排序:从a-b-c排序为a-c-b。那么在多线程的情况下会出现以下问题:当线程A在执行第13行代码:instance=new Singleton()时,B线程进来执行到第10行代码:(if (install==null))。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。

所以这里声明变量使用到的volatile关键字确保了当instance变量被初始化为Singleton实例时,多个线程正确地处理instance变量。

总结:这种写法既实现了懒汉式的延迟加载和线程安全的保证,又大大减少了getInstance()的时间消耗。在实际开发中,推荐使用这种单例模式写法。

volatile关键字学习:Java中volatile关键字的最全总结

6.静态内部类

静态内部类的写法采用了类装载的机制来保证初始化实例时只有一个线程。

class Singleton {
	// 1.构造方法私有化,防止类的外部使用new创建该类的对象
	private Singleton() {}
	
	// 2.写一个静态内部类,其中有一个静态属性Singleton
	private static class SingletonInstance {
		private static final Singleton instance = new Singleton();
	}
	// 3.提供公有的静态方法获取内部类中的属性
	public static Singleton getInstance() {
		return SingletonInstance.instance;
	}
}

采用静态内部类方式,使得instance在Singleton类被装载时不会立即初始化,而是在需要使用时,调用getInstance()方法后才会装载静态内部类SingleInstance,这才完成Singleton类的对象instance的初始化。

总结:使用静态内部类方式既避免了线程不安全,又利用静态内部类特点(产生了对该类的引用,才会装载到内存中)实现延迟加载,所以在开发中推荐使用。

7.枚举

借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

enum Singleton {
	INSTANCE;
}

这种方式是《Effective Java》的作者Josh Bloch提倡的方式。

三、具体应用

在JDK中的java.lang.Runtime就是经典的单例模式,采用的是饿汉式,类一装载就创建该类的实例。

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() {}
    
//省略
}

四、总结

  1. 作用:单例模式保证了系统内存中一个类只存在一个对象,节省了系统资源,对于一些需要频繁创建和销毁的对象,使用单例模式可以提高系统性能。
  2. 使用场景:需要频繁的进行创建和销毁的对象、创建对象耗时过长或耗费资源过多的对象但又经常使用的对象。如:工具类对象、频繁访问数据库或文件的对象。
  3. 注意:每个类加载器都定义了一个命名空间,如果有两个以上的类加载器,不同的类加载器可能会加载同一个类。这种情况下,使用单例模式需要自动指定同一个类加载器。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章