单例模式
Singleton Pattern,单例模式。Java中最简单设计模式之一,也是Java程序员接触最多使用最多的模式之一。常见的应用场景,例如Spring中管理的bean实例默认情况下是单例的。
单例模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。有如下特征:
- 只能有一个实例。
- 必须自己创建自己的唯一实例。
- 必须给所有其他对象提供这一实例。
任何设计模式的出现,本质上都为某一个可重复的设计问题提供了一套解决方案。我们使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。单例模式的出现是为了一个全局使用类只有一个实例,并提供一个访问它的全局访问点,避免频繁地创建与销毁,造成不必要的系统资源消耗。其本质是判断程序是否已经有这个单例,如果有则返回,如果没有则创建。
以下是一个单例模式的简单实现:
1.先创建一个对象
public class SingleObjectTest {
/**
* 静态对象,作为外部共享的唯一实例
* (静态成员变量存储在方法区的静态区,在类加载初始化后就可以使用)
*/
private static SingleObjectTest instance = new SingleObjectTest();
/**
* 构造函数私有化,为了防止在外部对其实例化
*/
private SingleObjectTest() { }
/**
* 返回唯一可以的实例
* @return
*/
public static SingleObjectTest getInstance() {
return instance;
}
/**
* 测试方法
*/
public void test() {
System.out.println("这是单例模式。");
}
}
2.获取该类的实例
public class TestMain {
public static void main(String[] args) {
//这样new对象,编译错误:SingleObjectTest() has private access in SingleObjectTest
//SingleObjectTest test = new SingleObjectTest();
/* 获取可用实例 */
SingleObjectTest singleObjectTest = SingleObjectTest.getInstance();
/* 使用 */
singleObjectTest.test();
}
}
单例模式的实现方式
单例模式分饿汉式单例与懒汉式单例,实现的方式有多种。
1.饿汉式单例类
饿汉式单例类是实现起来最简单的单例类。线程安全,但没有延迟加载(Lazy Load)
,是常用的实现方式,但容易产生垃圾对象。
实现代码:
public class SingleObjectTest {
private static SingleObjectTest instance = new SingleObjectTest();
private SingleObjectTest() { }
public static SingleObjectTest getInstance() {
return instance;
}
}
这代码和前面的代码没什么区别。当SingleObjectTest类被加载时,静态变量instance会被初始化,此时SingleObjectTest类的私有构造函数会被调用,单例类的唯一实例将被创建。
弊端:类加载时就初始化,浪费内存。为了解决这个问题,于是引出了懒汉式单例类。
2.懒汉式单例类
懒汉式单例类是结合线程锁来实现的单例类,是为了第一次调用才实例化,避免内存浪费。这种方式具备很好的 lazy loading,线程安全。
实现代码:
public class SingleObjectTest {
/**
* 避免类加载就实例对象,节省系统资源
*/
private static SingleObjectTest instance = null;
private SingleObjectTest() { }
public static synchronized SingleObjectTest getInstance() {
//当程序需要该实例时,才创建
if (instance == null) {
instance = new SingleObjectTest();
}
return instance;
}
}
上面代码虽然解决了线程安全问题,但是每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会很消耗系统资源。那么,如何既解决线程安全问题又不影响系统性能?懒汉式单例类还有一种实现方式,双检锁/双重校验锁(DCL,即 double-checked locking)。
实现代码:
public class SingleObjectTest {
private static volatile SingleObjectTest instance = null;
private SingleObjectTest() { }
public static SingleObjectTest getInstance() {
// 第一重判断
if (instance == null) {
synchronized (SingleObjectTest.class) {
//第二重判断
if (instance == null) {
instance = new SingleObjectTest1();
}
}
}
return instance;
}
}
为什么要双重校验锁呢?举个例子,假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过instance == null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在synchronized中再进行一次(instance == null)判断。
这样的方式,既实现了线程安全与延迟加载,又能在多线程情况下能保持高性能。在这里,要注意的是, 双重校验锁方式实现的SingleObjectTest 单例类,instance 静态变量需要加关键字volatile 。
然而双重校验锁方式也并非十全十美,要求JDK 1.5及以上版本。volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低。
3.饿汉式与懒汉式比较
饿汉式单例在类加载的时候就实例化,线程安全,可以确保唯一实例。但由于没有实现延迟加载(Lazy Load),假如实例化所需要的时间不短,这将会浪费系统资源。不过从调用速度和反应时间角度来讲,由于类在加载的时候就实例化,这点是优于懒汉式的。
懒汉式则在单例类第一次调用时,才开始实例化,实现了延迟加载(Lazy Load),采用双重校验锁方式保证了线程安全。和饿汉式单例相比,系统资源利用得更加合理更加高效。但由于加锁的原因,在单例类尚没有实例化时,多线程同时进入,系统运行效率可能降低。
对于我们来说,使用哪种方式的单例模式,要结合具体的业务来选择。