你的位置:首页 > 信息动态 > 新闻中心
信息动态
联系我们

手把手教你学会使用设计模式之单例模式

2022/1/1 9:08:07

文章目录

  • 设计模式之单例模式
    • 定义
    • 特点
    • 优缺点
      • 优点
      • 缺点
    • 结构与实现
      • 结构
      • 实现
        • 懒汉式单例
          • 1. 懒汉式单例(线程不安全)
            • 单线程调用
            • 多线程调用
          • 2. 懒汉式单例(线程安全)
          • 3. 懒汉式单例(双重校验锁)
            • `volatile`和`synchronized`的区别
          • 4. 懒汉式单例(静态内部类 推荐*)
        • 饿汉式单例
          • 1. 饿汉式单例(静态成员变量)
          • 2. 饿汉式单例(静态代码块)
          • 3. 饿汉式单例(枚举类 推荐*)
        • 总结
      • 破坏单例模式
        • 序列化反序列化方式破坏
        • 序列化反序列化方式破坏的解决方案
          • 源码查看
        • 反射破坏
        • 反射破坏的解决方案
    • 实例
      • 1. JDK源码中的单例模式
      • 2.Spring源码中的单例模式
          • 示例
          • 结论
        • Spring的bean注入是懒汉式单例还是饿汉式单例?
    • 扩展
      • 多例实现
        • `@Scope`有哪些

设计模式之单例模式

定义

一个类只有一个实例,该类提供一个全局的访问点供外部获取该实例。

如windows只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费。

特点

  1. 单例类只有一个实例对象;
  2. 该单例对象必须由单例类自行创建;
  3. 单例类对外提供一个访问该单例的全局访问点。

优缺点

优点

  • 单例模式可以保证内存里只有一个实例,减少了内存的开销。
  • 可以避免对资源的多重占用。
  • 单例模式设置全局访问点,可以优化和共享资源的访问。

缺点

  • 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
  • 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  • 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

结构与实现

一般地,普通类的构造函数是公有的,外部类可以通过**new 构造函数()**来生成多个实例。

使用Spring之前就是通过new来进行调用的。

如果将类的构造函数设为私有的,外部类就无法调用该构造函数,更无法生成多个实例。

这时,该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。

结构

主要角色有:单例类和访问类。

  1. 单例类:包含一个实例并且能自行创建这个实例的类。
  2. 访问类:使用单例的类。

在这里插入图片描述


实现

单例模式分为两种:懒汉式和饿汉式。

懒汉式是指:首次使用才会进行该实例对象的创建。

饿汉式是指:类加载时该单实例对象就会被创建。


懒汉式单例

1. 懒汉式单例(线程不安全)
public class Singleton {
    //私有构造方法
    private Singleton() {
    }
    //声明变量
    private static Singleton instance;
	//外部调用方法
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
单线程调用
public static void main(String[] args) {
    System.out.println(Singleton.getInstance());
    System.out.println(Singleton.getInstance());
    System.out.println(Singleton.getInstance());
}

在进行第一次调用时,会去new一个实例,第二次则直接可以获取到上一次new的实例。

输出结果:

com.ssw.config.thread.Singleton@59a6e353
com.ssw.config.thread.Singleton@59a6e353
com.ssw.config.thread.Singleton@59a6e353

上面的代码在单线程情况下是没有问题的,当多个线程并行调用getInstance()时,就会创建多个实例

多线程调用

多线程异步调用,肯定不能使用main方法来测试,这里我新写一个接口,然后通过地址进行调用。

新建一个TestService类,里面有t1、t2、t3方法,分别调用getInstance()方法,输出实例。

3个方法改为异步执行。

@Service
public class TestService {
    private static final Logger log = LoggerFactory.getLogger(TestService.class);

    @Async
    public void t1() {
        log.error("t1实例:{}", Singleton.getInstance());
    }

    @Async
    public void t2() {
        log.error("t2实例:{}", Singleton.getInstance());
    }

    @Async
    public void t3() {
        log.error("t3实例:{}", Singleton.getInstance());
    }
}

控制层,注入TestService类,写一个地址为/a1的方法,调用t1、t2、t3。

@Autowired
private TestService testService;

@PostMapping("/a1")
public void a1() {
    testService.t1();
    testService.t2();
    testService.t3();
}

输出结果:

2021-12-30 10:47:09 ERROR c.s.c.b.c.TestService.t1(20): t1实例:com.ssw.config.thread.Singleton@7a15471c
2021-12-30 10:47:09 ERROR c.s.c.b.c.TestService.t3(30): t3实例:com.ssw.config.thread.Singleton@423eaa58
2021-12-30 10:47:09 ERROR c.s.c.b.c.TestService.t2(25): t2实例:com.ssw.config.thread.Singleton@37cb9672

可以看到每个实例都是不一样的。

这属于是并发问题,我本来需要的是一个结果,现在变成了3个结果。

问题:再执行/a1方法,结果是什么?


2. 懒汉式单例(线程安全)

为了解决上面线程不安全的问题,最简单的方法是将整个getInstance()方法设置为同步(synchronized

public class Singleton {
    private static Singleton instance;
	//私有构造方法
    private Singleton() {
    }
	//外部调用方法
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

只是在getInstance()上增加了synchronized

重启项目,调用/a1方法,查看结果:

2021-12-30 10:55:53 ERROR c.s.c.b.c.TestService.t2(25): t2实例:com.ssw.config.thread.Singleton@651dea56
2021-12-30 10:55:53 ERROR c.s.c.b.c.TestService.t1(20): t1实例:com.ssw.config.thread.Singleton@651dea56
2021-12-30 10:55:53 ERROR c.s.c.b.c.TestService.t3(30): t3实例:com.ssw.config.thread.Singleton@651dea56

可以看出,设置为同步就解决多线程会创建多个实例的问题。


3. 懒汉式单例(双重校验锁)

上面增加了synchronized同步,解决了多线程情况下创建了多个实例的问题,但是效率不高,因为在调用getInstance()方法时,只能有一个线程调用,其他线程必须等待。

但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。(就是我第一次调用进行创建,后面的调用我不需要再进行同步操作,直接返回结果就好了),没必要让每个线程必须持有锁才能进行操作。

这就引出了双重检验锁。

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。

之所以称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。

为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        //第一次检验
        if (instance == null) {
            synchronized (Singleton.class) {
                //第二次检验
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这段代码看起来很完美,但是instance = new Singleton()会存在问题。

问题:在多线程情况下可能会出现空指针问题。

这句话在jvm中做了3件事情:

  1. instance分配内存
  2. 调用Singleton的构造函数来初始化成员变量
  3. instance对象指向分配的内存空间(执行完这步instance就为非null了)

在JVM的即时编译器中存在指令重排序的优化,就是说第二步和第三步的顺序不能保证,有可能执行的顺序是1-2-3或者1-3-2。

如果是后者,3执行完毕,2未执行之前,被线程2抢占了,这时instance已经非null了(但没有进行初始化),线程2会直接返回instance

我们只需要将instance变量声明成volatile就可以了。

public class Singleton {
    private volatile static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        //第一次检验
        if (instance == null) {
            synchronized (Singleton.class) {
                //第二次检验
                if (instance == null) {
                    System.out.println("执行new");
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

使用volatile的主要原因是它有一个特性:禁止指令重排优化,保证可见性和有序性。

volatilesynchronized的区别
  • volatile告诉jvm当前变量在寄存区中的值是不确定的,需要从主内存中读取。synchronized是锁定当前变量,只有当前线程可以访问该变量,其他线程处于阻塞状态。
  • volatile仅能使用在变量级别;synchronized可以使用在变量、方法和类级别中。
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

4. 懒汉式单例(静态内部类 推荐*)

静态内部类单例模式实例由内部类创建,由于JVM在加载外部类的过程中,不会加载静态内部类,只有内部类的方法/属性被调用时才会被加载,静态属性被static修饰,保证只被实例化一次,并且严格保证实例化顺序,解决了指令重排序的问题

public class Singleton {
    //私有构造方法
    private Singleton() {
    }
    //定义静态内部类
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    //提供公共访问
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

输出结果:

2021-12-30 11:50:39 ERROR c.s.c.b.c.TestService.t2(25): t2实例:com.ssw.config.thread.Singleton@598414b8
2021-12-30 11:50:39 ERROR c.s.c.b.c.TestService.t3(30): t3实例:com.ssw.config.thread.Singleton@598414b8
2021-12-30 11:50:39 ERROR c.s.c.b.c.TestService.t1(20): t1实例:com.ssw.config.thread.Singleton@598414b8

静态内部类使用了JVM本身机制保证了线程安全问题;

SingletonHolder是私有的,除了getInstance()方法之外没有办法可以访问它,所以它是懒汉式的


饿汉式单例

1. 饿汉式单例(静态成员变量)

特点是类一旦加载就创建一个单例,保证在调用getInstance方法之前单例就存在了。

public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }
}

在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程也不会出现问题。

但这种方式不属于懒加载,因为只要程序启动,不管是否使用都会进行创建。


2. 饿汉式单例(静态代码块)
public class Singleton {
    //私有构造函数
    private Singleton() {
    }
    private static Singleton instance;
    //静态代码块
    static {
        instance = new Singleton();
    }
    public static Singleton getInstance() {
        return instance;
    }
}

3. 饿汉式单例(枚举类 推荐*)

创建枚举默认是线程安全的,还能防止反序列化导致重新创建新建的对象。

public enum SingletonEnum {
    INSTANCE;
}

调用方:

public static void main(String[] args) {
    SingletonEnum singletonEnum = SingletonEnum.INSTANCE;
    SingletonEnum singletonEnum1 = SingletonEnum.INSTANCE;
    System.out.println(singletonEnum == singletonEnum1);
}

返回结果:

true

总结

  1. 在不考虑浪费内存空间的情况下,推荐使用饿汉式的枚举方式

  2. 在需要时才进行加载时,推荐使用懒汉式的静态内部类方式


破坏单例模式

破坏单例模式是指:使上面定义的单例类可以创建多个实例,枚举方式除外

破坏的方式有两种:序列化反序列化破坏和反射破坏

序列化反序列化方式破坏

示例:

如我们使用静态内部类方式

public class Singleton implements Serializable {
    //私有构造方法
    private Singleton() {
    }

    //定义静态内部类
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    //提供公共访问
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

定义将获取的实例写入文件中

/**
 * 向文件中写数据(对象)
 * @throws Exception
 */
public static void wirte() throws Exception {
    //1.获取对象
    Singleton singleton = Singleton.getInstance();
    //2.创建对象输出流对象
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\a.txt"));
    //3.写对象
    objectOutputStream.writeObject(singleton);
    //4.释放对象
    objectOutputStream.close();
}

读取文件中的实例

/**
 * 从文件读取数据(对象)
 * @throws Exception
 */
public static void read() throws Exception {
    //1.创建对象输入流对象
    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\a.txt"));
    //2.读取对象
    Singleton singleton = (Singleton) objectInputStream.readObject();
    System.out.println(singleton);
    //释放资源
    objectInputStream.close();
}

调用方:

先调用写方法,再进行两次读取,看结果是否一致。

public static void main(String[] args) throws Exception {
    //1.写文件
    wirte();
    //2.读文件
    read();
    read();
}

输出结果:

com.ssw.config.thread.Singleton@239963d8
com.ssw.config.thread.Singleton@3abbfa04

序列化反序列化方式破坏的解决方案

序列化、反序列化方式破坏单例模式的解决方法。

Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,没有定义则返回新new 出来的对象。

静态内部类增加readResolve()方法

//当进行反序列化时,会自动调用该方法,将该方法的返回值直接返回。
public Object readResolve() {
    return SingletonHolder.INSTANCE;
}

再次进行一次写、两次读操作。

输出结果:

com.ssw.config.thread.Singleton@36f6e879
com.ssw.config.thread.Singleton@36f6e879

问题解决。

源码查看
Singleton singleton = (Singleton) objectInputStream.readObject();

查看readObject()方法,

在这里插入图片描述

readObject0()方法,tc=TC_OBJECT

case TC_OBJECT:
    return checkResolve(readOrdinaryObject(unshared));

readOrdinaryObject()方法中

在这里插入图片描述

如果有readResolve方法,则执行readResolve方法。


反射破坏

示例:

还是用静态内部类

public class Singleton implements Serializable {
    //私有构造方法
    private Singleton() {
    }

    //定义静态内部类
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    //提供公共访问
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

调用方:

public static void main(String[] args) throws Exception {
    //获取字节码对象
    Class<Singleton> singletonClass = Singleton.class;
    //获取无参构造对象
    Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor();
    //取消访问检查
    constructor.setAccessible(true);
    //创建对象
    Singleton s1 = constructor.newInstance();
    Singleton s2 = constructor.newInstance();
    System.out.println(s1 == s2);
}

创建了两个对象,调用结果:

false

反射破坏的解决方案

反射是通过获取无参构造对象,来进行创建对象。

所以静态内部类,需要修改构造方法

增加标识符,,当第一次创建时设置为true,否则抛出运行时异常。

private static boolean flag = false;

//私有构造方法
private Singleton() {
    synchronized (Singleton.class) {
        //如果为ture,说明已经创建过。抛出运行时异常。
        if (flag) {
            throw new RuntimeException("不能创建多个对象");
        }
        //如果为false,第一次创建,设置flag=true
        flag = true;
    }
}

静态内部类完整代码

public class Singleton implements Serializable {

    private static boolean flag = false;

    //私有构造方法
    private Singleton() {
        synchronized (Singleton.class) {
            //如果为ture,说明已经创建过。抛出运行时异常。
            if (flag) {
                throw new RuntimeException("不能创建多个对象");
            }
            //如果为false,第一次创建,设置flag=true
            flag = true;
        }
    }

    //定义静态内部类
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    //提供公共访问
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

调用结果:

Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.ssw.core.blog.controller.Test2.main(Test2.java:22)
Caused by: java.lang.RuntimeException: 不能创建多个对象
	at com.ssw.config.thread.Singleton.<init>(Singleton.java:17)
	... 5 more

实例

1. JDK源码中的单例模式

java.lang.Runtime就是使用了饿汉式单例(静态成员变量)

在这里插入图片描述

供外部调用方法

public static Runtime getRuntime() {
        return currentRuntime;
    }

2.Spring源码中的单例模式

在Spring依赖注入Bean实例,默认是单例的。Bean有两种模式,单例(singleton)和多例(prototype)。

  • singleton(单例):只有一个共享的实例存在,所有对这个 bean 的请求都会返回唯一的实例。
  • prototype(多例):对这个 bean 的每次请求都会创建一个新的 bean 实例,类似于 new。

@Autowired@Resource@Service@Mapper@Component,默认都属于单例模式。

这里拿@Autowired举例

示例

新建一个users接口

public interface IUsers {
    /**
     * 获取信息
     * @return
     */
    boolean getInfo();
}

新建users接口的实现类

@Service
public class UsersService implements IUsers {
    @Override
    public boolean getInfo() {
        return true;
    }
}

控制层调用(为了对比,写了两个控制层)

@RestController
@RequestMapping("users")
public class Users1Controller {
    private static final Logger log = LoggerFactory.getLogger(Users1Controller.class);

    @Autowired
    private IUsers users;

    @RequestMapping("a1")
    public void a1() {
        log.info("a1: users={},返回结果:{}", users, users.getInfo());
        IUsers iUsers = new UsersService();
        log.info("a1 new 出来的: users={},返回结果:{}", iUsers, iUsers.getInfo());
    }
}
@RestController
@RequestMapping("users1")
public class Users2Controller {
    private static final Logger log = LoggerFactory.getLogger(Users2Controller.class);

    @Autowired
    private IUsers users;

    @RequestMapping("a2")
    public void a2() {
        log.info("a2: users={},返回结果:{}", users, users.getInfo());
        IUsers iUsers = new UsersService();
        log.info("a2 new 出来的: users={},返回结果:{}", iUsers, iUsers.getInfo());
    }
}

说明:两个控制层分别进行注入IUsers接口,并new一个服务层,日志输出进行比较。

结果

: a1: users=com.demo.service.UsersService@3ae1a7f3,返回结果:true
: a1 new 出来的: users=com.demo.service.UsersService@235bfd56,返回结果:true
: a2: users=com.demo.service.UsersService@3ae1a7f3,返回结果:true
: a2 new 出来的: users=com.demo.service.UsersService@66ee65ad,返回结果:true
结论
  1. 可以看出多次注入的实例是同一个实例。
  2. 每次new出来的实例都是新建,所以每个实例都不一样,会造成资源浪费。

Spring的bean注入是懒汉式单例还是饿汉式单例?


扩展

上面说到,在Spring依赖注入Bean实例,默认是单例的。Bean有两种模式,单例(singleton)和多例(prototype)。

  • singleton(单例):只有一个共享的实例存在,所有对这个 bean 的请求都会返回唯一的实例。
  • prototype(多例):对这个 bean 的每次请求都会创建一个新的 bean 实例,类似于 new。

多例实现

上面的示例,都是使用UsersService这个bean,我们只需要增加@Scope注解就可以将这个bean变为多例模式。

@Scope("prototype")
@Service
@Scope("prototype")
public class UsersService implements IUsers {
    @Override
    public boolean getInfo() {
        return true;
    }
}

其他类不需要修改

测试结果

a1: users=com.demo.service.UsersService@5c8847c1,返回结果:true
a1 new 出来的: users=com.demo.service.UsersService@405b8b1e,返回结果:true
a2: users=com.demo.service.UsersService@6a7b3cfc,返回结果:true
a2 new 出来的: users=com.demo.service.UsersService@b529dd6,返回结果:true

可以看到每个实例都不一样,对这个bean的每次请求都是创建一个新的实例。

@Scope有哪些

  1. singleton 单例模式,也就是容器中只存在一个实例,不管怎么获取Bean都是只存在一个实例,singleton类型的bean定义从容器启动到第一次被请求而实例化开始,只要容器不销毁或退出,该类型的bean的单一实例就会一直存活,典型单例模式。
  2. prototype 多例模式,spring容器每次都会重新生成一个新的Bean实例给请求方,选择该类型的对象的实例化和属性设置都由容器负责,但准备工作完成后,对象实例给到请求方后,容器不再拥有对该对象的引用,拿到该对象实例的这一方需要对该对象实例后续的生命周期负责(比如说销毁)。
  3. request 同一次请求创建一个实例,bean在当前请求内有效,该可以理解成一种特殊的多例模式。当请求结束后,该对象的生命周期即告结束,他们之间相互不干扰。
  4. session 代表同一个session创建一个实例,一个session id对应一个session。
  5. global session 基于porlet的web程序中才有用,它映射到porlet的global session,普通的servlet中使用了这个模式,容器会将其作为普通的session的scope对待。

最常用的是:单例singleton和多例prototype