设计模式之单例模式详解
文章目录
一、什么单例模式
单例模式(Singleton Pattern) 是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。隐藏其所有构造方法,单例模式是创建型模式。
二、饿汉式单例模式
饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题
代码示例一
public class HungrySingletonOne {
//隐藏其构造方法
private HungrySingletonOne(){
}
//保证单实例
private static final HungrySingletonOne hungrySingletonOne = new HungrySingletonOne();
//提供全局访问点
public static HungrySingletonOne getInstance(){
return hungrySingletonOne;
}
}
代码示例二
public class HungrySingletonTwo {
//利用静态代码块在类初始化的时候来创建对象
static {
hungrySingletonTwo = new HungrySingletonTwo();
}
private HungrySingletonTwo(){}
private static final HungrySingletonTwo hungrySingletonTwo;
public static HungrySingletonTwo getInstance(){
return hungrySingletonTwo;
}
}
以上两种都是饿汉式单例的写法,我们再看总结一下饿汉式的优缺点
三、懒汉式单例模式
为了解决饿汉式单例可能带来的内存浪费问题,于是出现了懒汉式单例的写法。懒汉式单例模式的特点是,单例对象会在要被使用时才初始化
懒汉式代码示例
public class LazySingletonOne {
private LazySingletonOne(){}
private static LazySingletonOne lazySingletonOne = null;
public static LazySingletonOne getInstance(){
if(lazySingletonOne == null){
lazySingletonOne = new LazySingletonOne();
}
return lazySingletonOne;
}
}
以上就是一个最简单的饿汉式单例实现,但是在多线程情况下并不能保证单例。我们来测试看一下
public class SingleOneTest {
public static void main(String[] args) {
new Thread(() ->{
LazySingletonOne instanceOne = LazySingletonOne.getInstance();
System.err.println(Thread.currentThread().getName() + "....." + instanceOne);
}, "Thread-01").start();
new Thread(() ->{
LazySingletonOne instanceTwo = LazySingletonOne.getInstance();
System.err.println(Thread.currentThread().getName() + "....." + instanceTwo);
},"Thread-02").start();
}
}
从控制台输出可以看出实例化的对象并不是单例的,也就是当两个线程同时去判断 lazySingletonOne == null
的条件的时候,就会实例化两次
为了解决上面问题我们对其优化,保证在判断 lazySingletonOne == null
条件或者执行该方法的时候只能有一个线程进来,我们来看优化后的代码,在获取实例的方法上加了 synchronized
关键字,让其变成同步方法
public class LazySingletonTwo {
private LazySingletonTwo(){}
private static LazySingletonTwo lazySingletonTwo = null;
public static synchronized LazySingletonTwo getInstance(){
if(lazySingletonTwo == null){
lazySingletonTwo = new LazySingletonTwo();
}
return lazySingletonTwo;
}
}
上面我们在方法上加了synchronized
,对性能影响较大,我们对其做一下优化,将锁的粒度缩小
public class LazySingletonTwo {
private LazySingletonTwo(){}
private static LazySingletonTwo lazySingletonTwo = null;
public static LazySingletonTwo getInstance(){
if(lazySingletonTwo == null){
synchronized (){
lazySingletonTwo = new LazySingletonTwo();
}
}
return lazySingletonTwo;
}
}
一、双重检查锁单例模式
为了解决上面懒汉式的线程安全问题,我们将获取实例方法变成了同步方法,但是在线程数量比较多的情况下,CPU的分配压力上升,会导致大批量的线程阻塞,会导致程序性能大幅度下降。为了解决这一问题,可以使用双重检查锁的单例模式,我们来看具体代码
public class LazySingletonThree {
private LazySingletonThree(){}
private static LazySingletonThree lazySingletonThree = null;
public static LazySingletonThree getInstance(){
if(lazySingletonThree == null){
synchronized (LazySingletonThree.class){
if(lazySingletonThree == null){
lazySingletonThree = new LazySingletonThree();
}
}
}
return lazySingletonThree;
}
}
现在可能有人会想了哈,为什么要对实例做两次非空校验呢?
- 第一次校验
这里的话第一次非空校验是为了提高程序性能避免线程阻塞,试想如果我们不加外层非空校验,那么程序是不是不管lazySingletonThree
是否已经被实例化都会被阻塞,所以这里只在没有实例化的时候进行阻塞,在一定程度上提升了程序性能。 - 第二次校验
第二次校验是为了保证实例化对象的唯一性。
if(lazySingletonThree == null){
synchronized (LazySingletonThree.class){
lazySingletonThree = new LazySingletonThree();
}
}
return lazySingletonThree;
如果不加这一层判断的话,试想现在有两个线程同时在执行,当线程1执行到 return lazySingletonThree;
这一行代码的时候CPU时间片切换由线程2去执行,此时,线程2会再一次实例化LazySingletonThree
,最后返回的还是两个不同实例。
至此,乍一看好像是没什么问题了,但是这里在分配lazySingletonThree
变量和 new LazySingletonThree()
内存空间的时候可能会有指令重排序的问题,最后为lazySingletonThree
变量加上volatile
关键字,解决指令重排序的问题,最后看下完整的双重检查锁的单例代码:
public class LazySingletonThree {
private LazySingletonThree(){}
private volatile static LazySingletonThree lazySingletonThree = null;
public static LazySingletonThree getInstance(){
if(lazySingletonThree == null){
synchronized (LazySingletonThree.class){
if(lazySingletonThree == null){
lazySingletonThree = new LazySingletonThree();
}
}
}
return lazySingletonThree;
}
}
二、静态内部类单例模式
为了兼顾饿汉式单例模式内存浪费的问题以及
synchronized
带来的性能问题。静态内部类利用Java语法巧妙的避免了线程安全问题,还起到了一个延时加载的作用,只有在调用getInstance()
方法的时候,静态内部类才会被加载。
public class LazySingletonFour {
private LazySingletonFour(){
}
public static LazySingletonFour getInstance(){
return innerClass.INSTANCE;
}
private static class innerClass{
private static final LazySingletonFour INSTANCE = new LazySingletonFour();
}
}
那么看似完美的静态内部类单例模式真的没有缺陷了么?我们来看一个例子:
public class ReflectTest {
public static void main(String[] args) {
try {
Class<LazySingletonFour> classFour = LazySingletonFour.class;
Constructor<LazySingletonFour> fourConstructor = classFour.getDeclaredConstructor(null);
fourConstructor.setAccessible(true);
LazySingletonFour objOne = fourConstructor.newInstance();
LazySingletonFour objTwo = fourConstructor.newInstance();
System.err.println("objOne = " + objOne);
System.err.println("objTwo = " + objTwo);
} catch (Exception e) {
e.printStackTrace();
}
}
}
从控制台输出可以看出,静态内部类单例模式也可以通过反射来破坏
至此,我们可以对静态内部类进行一个优化,通过手动抛异常来避免反射创建,来看静态内部类单例模式最终代码:
public class LazySingletonFour {
private LazySingletonFour(){
if(innerClass.INSTANCE != null){
throw new RuntimeException("不允许非法创建....");
}
}
public static LazySingletonFour getInstance(){
return innerClass.INSTANCE;
}
private static class innerClass{
private static final LazySingletonFour INSTANCE = new LazySingletonFour();
}
}
我们再次测试看一下,可以看出已经避免了反射破坏
到这里,这中单例模式的写法似乎已经非常的完美了,但是还有一个黑科技,我们将LazySingletonFour
实现Serializable
接口后,再接着看一个测试代码:
public class LazySingletonFourTest {
public static void main(String[] args) {
LazySingletonFour instance1 = null;
LazySingletonFour instance2 = LazySingletonFour.getInstance();
FileOutputStream fos = null;
try {
//序列化静态内部类获取的实例
fos = new FileOutputStream("LazySingletonFour.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();
//反序列化
FileInputStream fis = new FileInputStream("LazySingletonFour.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1 = (LazySingletonFour)ois.readObject();
ois.close();
System.err.println("反序列化获得的实例 instance1: " + instance1);
System.err.println("静态内部类获取实例 instance2: " + instance2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
我们从这个测试代码可以看出,看似完美的静态内部类单例模式也能够通过序列化的方式破坏掉,这样违背了我们单例模式的初衷。那有没有啥更好的方式能够解决该情况呢?我们接着看注册式单例模式!
三、注册式单例模式
为了解决反射、以及序列化破坏单例的情况,出现了注册式单例模式。注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。
一、枚举式单例模式
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){return INSTANCE;}
}
以上就是一个简单的枚举式单例模式实现,我们先来测试一下反射是否能破坏:
public class EnumSingletonTest {
public static void main(String[] args) {
EnumSingleton instance = EnumSingleton.getInstance();
instance.setData(new Object());
try {
Class<EnumSingleton> clazz = EnumSingleton.class;
Constructor<EnumSingleton> c = clazz.getDeclaredConstructor(String.class,int.class);
c.setAccessible(true);
Object obj1 = c.newInstance();
Object obj2 = c.newInstance();
System.err.println("obj1 " + obj1);
System.err.println("obj2 " + obj2);
}catch (Exception e){
e.printStackTrace();
}
}
}
从结果来看,枚举类型从源码中就限制了通过反射来实例化对象
我们再来测试一下,通过序列化的方式能否将其破坏:
public static void main(String[] args) {
EnumSingleton instance1 = null;
EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(new Object());
FileOutputStream fos = null;
try {
fos = new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1 = (EnumSingleton)ois.readObject();
ois.close();
System.err.println("反序列化获得的实例 instance1: " + instance1.getData());
System.err.println("通过枚举类获取实例 instance2: " + instance2.getData());
} catch (Exception e) {
e.printStackTrace();
}
}
从测试结果来看,枚举类单例模式解决了反射以及序列化破坏单例的问题
-
优点
- 枚举类单例模式利用了枚举类自身特点避免了反射破坏,性能高,写法优雅,没有加任何锁
-
缺点
- 枚举类单例底层实现和饿汉式单例模式有点像,在初始化的时候就将其放在了一个Map容器中,在某些情况下,可能会造成内存浪费。同时,不适合大批量的创建对象。
二、容器式单例模式
public class ContainerSingleton {
private ContainerSingleton(){}
private static final Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className){
synchronized (ioc){
if(!ioc.containsKey(className)){
Object instance = null;
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (Exception e){
e.printStackTrace();
}
return instance;
}else{
return ioc.get(className);
}
}
}
}
先创建一个实体类来做测试:
@Data
@ToString(callSuper = true)
public class ContainerEntity {
private String name;
private String nickName;
private Integer age;
}
我们写个测试代码测试一下:
public class ContainerSingletonTest {
public static void main(String[] args) {
Object instance1 = ContainerSingleton.getInstance("com.zdp.entity.ContainerEntity");
Object instance2 = ContainerSingleton.getInstance("com.zdp.entity.ContainerEntity");
System.err.println("instance1---> " + instance1);
System.err.println("instance2---> " + instance2);
}
}
从结果可以看出,容器式单例模式也是没问题的。
四、总结
单例模式可以保证内存中只有一个实例,减少了内存的开销,还可以避免对资源的多重占用。单例模式实现起来也非常简单,以上就是我对单例模式的介绍啦,希望能够对大家能够有点帮助!码字不易,如果觉得对你有帮助就帮我留个赞吧!