单例设计模式是最简单的一种创建型设计模式,其提供了创建对象的最佳实现。该模式涉及到一个单一的类,且该类负责创建自己的对象,同时确保只有一个对象被创建。该类提供了一种访问其唯一对象的方式,访问方式的实现是单例设计模式的核心,涉及到线程安全等问题。
- 为何使用单例模式?
通常来说,我们获取到一个对象实例,当只使用其内部提供的方法,而不关注其成员变量时,可以使用单例模式。- 节省内存空间:单例在内存中只有一个实例对象,节省内存空间,避免大量创建及销毁
- 高性能:减少高质量资源重复占用,可以进行全部访问
单例模式理论
单例模式
- 什么时候使用单例模式?
- 重复对象需要频繁实例化及销毁的对象
- 有状态化的工具对象
- 频繁访问数据库或文件的对象
单例角色:单例模式只有一个单例角色,在单例的内部生成一个对象实例,同时提供一个方法用于获取这个对象实例。为了避免外界直接创建对象实例从而破坏单例模式,通常构造函数都是设置为私有的,外部只能通过方法获取到唯一的单例对象。
单例模式的实现方式
比较常见的单例实现方式有如下:
- 饿汉式
- 懒汉式
- 静态内部类
代码实现
- 饿汉式
package singleton.hungry;
/**
* 单例模式:饿汉式实现方式,类在加载的时候就创建单例对象
*/
public class HungrySingleton {
/**
* 定义类静态变量,类加载的时候就已经创建好单例对象
*/
private static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return INSTANCE;
}
}
饿汉式在类加载的时候就已经将单例对象创建好了,但是如果没有使用它,一定程度上造成浪费。不过,由于在类加载的时候单例对象就创建好了,因此它是线程安全的。
- 懒汉式
package singleton.lazy;
/**
* 单例模式:懒汉式实现方式
*/
public class LazySingleton {
private static LazySingleton INSTANCE = null;
private LazySingleton(){}
public static LazySingleton getInstance(){
if (INSTANCE == null){
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
懒汉式是在外部调用类的方法获取单例对象时才会创建单例对象,它不会想饿汉式那样造成浪费。但是,会导致线程安全问题。在高并发情况下,可能多个线程都调用了构造方法来创建对象。为了解决这个线程并发问题,通常需要加锁。
- 线程安全版懒汉式
package singleton.safeLazy;
/**
* 单例模式:线程安全的懒汉式实现
*/
public class SafeLazySingleton {
/**
* 加volatile关键字,防止重排序
*/
private static volatile SafeLazySingleton INSTANCE = null;
private SafeLazySingleton(){}
public static SafeLazySingleton getInstance(){
if(INSTANCE == null){
synchronized (SafeLazySingleton.class){
// 双重校验
if(INSTANCE == null){
INSTANCE = new SafeLazySingleton();
}
}
}
return INSTANCE;
}
}
线程安全版的懒汉式实现要点是:1. volatile关键字 2. synchronized锁 3. 双重校验判空
1. volatile关键字可以防止初始化对象时由于编译器的作用导致指令重排序
一个对象初始化的步骤包括:
- 给对象实例分配一块内存空间
instance = allocate(SafeLazySingleton.class)
- 针对分配好的内存空间的对象实例,执行构造方法,对这个对象实例进行初始化操作,对各个成员变量赋值,执行初始化逻辑。
invokeConstructor(instance)
- 经过上面两个步骤,一个对象实例完成;此时将
instance
指针指向这块内存空间,赋值给我们引用类型的变量,让它指向SafeLazySingleton
对象的内存地址。
以上是正常的步骤,然而在编译器重排序的作用下,可能真正执行的指令顺序是 1-> 3-> 2。
所以当A线程去创建单例对象实例,进行到3步骤,由于重排序,此时的对象是残缺的;而此时B线程判断INSTANCE
对象为空,于是进行了某些操作,由于是残缺对象,因此会出错。
2. 双重校验
第二重校验是避免A、B两个线程依次进入了同步代码块,重复创建单例对象。
3. synchronized同步锁
其实synchronized
可以直接加在方法上,这样就不用双重校验了,但是这样的锁粒度比较大。
- 静态内部类
package singleton.staticClass;
/**
* 单例模式:静态内部类
*/
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton(){}
/**
* 通过静态内部类加载时创建单例对象
*/
private static class Inner{
final static StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
/**
* 第一次调用此方法时,触发静态内部类加载从而创建单例对象
* @return
*/
public static StaticInnerClassSingleton getInstance(){
return Inner.INSTANCE;
}
}
此方法利用了JVM类加载的线程安全实现单例。
网友评论