设计模式学习笔记---单例模式(Java版)

时间:2021-08-22 10:29:47

GOF23(Group of  four)

创建型模式

单例模式,工厂模式,抽象工厂模式,建造者模式,原型模式。

结构型模式

适配器模式,桥接模式,装饰模式,组合模式,外观模式,享元模式,代理模式。

行为型模式

模板方法模式,命令模式,迭代器模式,观察者模式,中介者模式,备忘录模式,解释器模式,状态模式,策略模式,职责链模式,访问者模式。


单例模式

核心作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。

优点:

--由于只产生一个实例,减少了系统实例开销,当一个对象的产生需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式解决。

--可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。


常见的五种单例模式的实现方式:

1 俄汉式(线程安全,调用效率高。但是,不能延时加载。)

2 懒汉式(线程安全,调用效率不高。但是,可以延时加载。)

3 双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题,不建议使用)

4 静态内部类式(线程安全,调用效率高,但是,可以延时加载)

5 枚举单例(线程安全,调用效率高,不能延时加载。并且可以天然的防止反射和反序列化漏洞!)


1 俄汉式实现(单例对象立即加载)


package com.lgd.gof.singleton;

/**
* 测试俄汉式单例
* @author liguodong
*
*/
public class Demo01 {
//2 提供静态属性,类初始化,立即加载对象(没有延时加载的优势)。
//由于加载类时,自然是线程安全的!
private static Demo01 instance = new Demo01();
//1 私有构造器。
private Demo01()
{
}
//3 提供方法 方法没有同步,调用效率高。
public static Demo01 getInstance()
{
return instance;
}
}

俄汉式单例设计模式代码中,static变量会在类加载时初始化,此时不会设计多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,可以省略synchronized关键字。
问题:如果是加载本类,而不是调用getInstance(),甚至永远没有调用,则会造成资源浪费。


2 懒汉式(单例对象延时加载)


package com.lgd.gof.singleton;

/**
* 测试懒汉式单例
* @author liguodong
*/
public class Demo02 {
//2 提供静态属性,类初始化,不加载对象,真正用到再创建。
private static Demo02 instance;
//1 私有构造器。
private Demo02()
{
}
//3 提供方法 方法同步,调用效率低。
public static synchronized Demo02 getInstance()
{
if(instance == null)
{
instance = new Demo02();
}
return instance;
}
}

延时加载,懒加载,真正用的时候才加载。

问题:资源利用率高了,由于可能有多个线程调用,每次调用getInstance()方法都要同步,并发效率较低。


3 双重检测锁实现


package com.lgd.gof.singleton;

/**
* 测试双重检测锁单例
* @author liguodong
*/
public class Demo03 {
//2 提供静态属性,类初始化,不加载对象,真正用到再创建。
private static Demo03 instance;
//1 私有构造器。
private Demo03()
{
}
//3 提供方法
public static Demo03 getInstance()
{
if(instance == null)
{
Demo03 sc;
synchronized(Demo03.class)
{
sc = instance;
if(sc == null)
{
synchronized (Demo03.class)
{
if(sc==null)
{
sc = new Demo03();
}
}
instance = sc;
}
}
}
return instance;
}
}


优点:

将同步内容下放到if内部,提高了执行i效率,不必每次获取对象时进行同步,只有第一次才同步创建以后就没有必要了。

问题:

由于编译器优化和JVM底层内部模型原因,偶尔会出问题,不建议使用


4 静态内部类实现方式

package com.lgd.gof.singleton;

/**
* 测试静态类实现单例模式
* 线程安全,调用效率高,懒加载。
* @author liguodong
*
*/
public class Demo04 {
public static class Demo04ClassInstance
{
private static final Demo04 instance = new Demo04();
}

private Demo04()
{
}
//方法没同步,调用效率高。
public static Demo04 getInstance()
{
return Demo04ClassInstance.instance;
}
}



要点:

外部类没有static属性,则不会像俄汉式那样立即加载对象

只有真正调用getInstance(),才会加载静态内部类。加载类时是线程安全的。instance是static final 类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证线程安全性。

兼备了并发高效调用和延时加载的优势。


单例模式:线程安全,调用效率尽量高,要有懒加载。


5 枚举实现单例

package com.lgd.gof.singleton;

/**
* 测试枚举式实现单例模式(没有延时加载)
* @author liguodong
*
*/
public enum Demo05 {
//这个枚举元素,本身就是单例对象!
INSTANCE;

//添加自己需要的操作
public void Demo05Operation()
{
//功能处理
}
}


优点:

实现简单,枚举本身就是单例模式。由于JVM提供保障避免通过反射和反序列化的漏洞。

缺点:

无延时加载



测试:

package com.lgd.gof.singleton;

public class Client {
public static void main(String[] args) {
Demo01 s1 = Demo01.getInstance();
Demo01 s2 = Demo01.getInstance();

System.out.println(s1);
System.out.println(s2);
System.out.println(Demo05.INSTANCE==Demo05.INSTANCE);
 }
}


运行结果:

设计模式学习笔记---单例模式(Java版)



如何选用?

单例对象  占用资源少,不需要延时加载:

枚举式 好与 俄汉式

单例对象  占用资源大,需要延时加载:

静态内部类式好于懒汗式


问题1

通过反射破解上面几种(不包含枚举式(天然的单例))实现方式!(可以在构造方法中手动抛出异常控制)

package com.lgd.gof.singleton;

/**
* 测试俄汉式单例(如何防止反射和反序列化漏洞)
* @author liguodong
*/
public class Demo06 {
//2 提供静态属性,类初始化,立即加载对象(没有延时加载的优势)。
//由于加载类时,自然是县城安全的!
private static Demo06 instance = new Demo06();
//1 私有构造器。
private Demo06()
{
}
//3 提供方法 方法没有同步,调用效率高。
public static Demo06 getInstance()
{
return instance;
}
}



package com.lgd.gof.singleton;

import java.lang.reflect.Constructor;

/**
* 测试反射和反序列化破解单例模式
* @author liguodong
*
*/
public class ClientDemo {
public static void main(String[] args) throws Exception {
Demo06 s1 = Demo06.getInstance();
Demo06 s2 = Demo06.getInstance();
System.out.println(s1);
System.out.println(s2);

Class<Demo06> clazz = (Class<Demo06>)Class.forName("com.lgd.gof.singleton.Demo06");
Constructor< Demo06> c = clazz.getDeclaredConstructor(null);
//反射不能访问私有的成员,可以跳过权限的检查,可以访问私有的。
c.setAccessible(true);
Demo06 s3 = c.newInstance();
Demo06 s4 = c.newInstance();
System.out.println(s3);
System.out.println(s4);
}
}


运行结果:

设计模式学习笔记---单例模式(Java版)


以上是通过反射破解单例,怎么样来防止?


解决方法:

    private Demo06()
    {    
        if(instance != null)
        {
            throw new RuntimeException();
        }

    }

再次实例的时候抛出运行时异常。





问题2

反序列化可以破解上面几种实现方式

package com.lgd.gof.singleton;

import java.io.Serializable;

/**
* 测试俄汉式单例(如何防止反射和反序列化漏洞)
* @author liguodong
*/
public class Demo06 implements Serializable{
//2 提供静态属性,类初始化,立即加载对象(没有延时加载的优势)。
//由于加载类时,自然是县城安全的!
private static Demo06 instance = new Demo06();
//1 私有构造器。
private Demo06()
{
if(instance != null)
{
throw new RuntimeException();
}
}
//3 提供方法 方法没有同步,调用效率高。
public static Demo06 getInstance()
{
return instance;
}
}


package com.lgd.gof.singleton;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
* 测试反射和反序列化破解单例模式
* @author liguodong
*
*/
public class ClientDemo {
@SuppressWarnings("all")
public static void main(String[] args) throws Exception {
Demo06 s1 = Demo06.getInstance();
Demo06 s2 = Demo06.getInstance();
System.out.println(s1);
System.out.println(s2);

//通过反序列化的方式构造多个对象
FileOutputStream fos = new FileOutputStream("/usr/Code/a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.close();
fos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/usr/Code/a.txt"));
Demo06 s3 = (Demo06)ois.readObject();
System.out.println(s3);
}
}

运行结果:

设计模式学习笔记---单例模式(Java版)

以上是通过反序列化破解单例,怎么样来防止?


解决方法:

(可以通过定义readResolve() 防止获得不同对象。反序列化时,如果对象所在类定义了readReslove(),实际上是一种回调),定义返回那个对象。

package com.lgd.gof.singleton;

import java.io.ObjectStreamException;
import java.io.Serializable;

/**
* 测试俄汉式单例(如何防止反射和反序列化漏洞)
* @author liguodong
*/
public class Demo06 implements Serializable{
//2 提供静态属性,类初始化,立即加载对象(没有延时加载的优势)。
//由于加载类时,自然是县城安全的!
private static Demo06 instance = new Demo06();
//1 私有构造器。
private Demo06()
{
if(instance != null)
{
throw new RuntimeException();
}
}
//3 提供方法 方法没有同步,调用效率高。
public static Demo06 getInstance()
{
return instance;
}
//在反序列时,如果定义了readResolve()
//直接返回此方法指定的对象,而不需要把反序列化那个新对象返回。
private Object readResolve() throws ObjectStreamException
{
return instance;
}
}


private Object readResolve() throws ObjectStreamException
{
   return instance;
}



常见的五种单例模式在多线程环境下的效率测试


CountDownLatch

同步辅组类,在完成一组正在其他线程中执行的操作前,它允许一个或多个线程等待。

countDown()  当前线程调用方法,则计数减一(建立放在finally里执行)

await()  调用此方法会一直阻塞当前线程,直到计时器的值为0

package com.lgd.gof.singleton;

import java.util.concurrent.CountDownLatch;

/**
* 测试多线程环境下五种创建单例模式的效率
* @author liguodong
*
*/
public class ClientTest {
public static void main(String[] args) throws InterruptedException {

long start = System.currentTimeMillis();
int threadNum = 10;
final CountDownLatch count = new CountDownLatch(threadNum);

for(int i=0;i<10;i++)
{
new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<100000;i++)
{
//Object o = Demo01.getInstance();
Object o = Demo05.INSTANCE;
}
count.countDown();

}
}).start();
}
count.await();//main线程阻塞,直到计数器变为0,才会继续往下执行。
long end = System.currentTimeMillis();
System.out.println("总耗时:"+(end-start));
}
}

在这里测试每一种单例模式

for(int i=0;i<100000;i++)
{
    //Object o = Demo01.getInstance();
    Object o = Demo05.INSTANCE;

}



单例模式优缺点

主要优点:

1、提供了对唯一实例的受控访问。

2、由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。

3、允许可变数目的实例。

主要缺点:

1、由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。

2、单例类的职责过重,在一定程度上违背了“单一职责原则”。

3、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。