从设计层面上讲,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。

比如在Hibernate的SessionFactory,它充当数据存储源的代理,并负责创建Session对象。SessionFactory并不是轻量的,一般情况下,一个项目通常只需要一个SessionFactory就够,这时就会使用到单例模式。

  • 某一个类只有一个实例;(构造器私有)

  • 它必须自行创建这个实例;(自己编写实例化逻辑)

  • 它必须自己向整个系统提供这实例;(对外提供实例化方法)

单例模式种类

静态常量饿汉式

1.代码实现

/**
 * 饿汉式(静态变量)
 *
 * @author caoweifeng
 * @date 2022年02月24日 5:05 PM
 * @email weistuday@qq.com
 * @description:
 */
public class Type1 {

    public static void main(String[] args) {
        //测试
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        System.out.println(instance == instance1); // true
        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());
    }
}

class Singleton {
    /**
     * 1.构造器私有化,外部不能new
     */
    private Singleton() {
    }

    /**
     * 2. 本类内部创建对象实例
     */
    private final static Singleton INSTANCE = new Singleton();

    /**
     * 3. 提供一个公有的静态方法,返回实例对象
     */
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

img

2.优缺点说明

  1. 优点 : 这种写法比较简单,就是在类装载点时候完成实例化,避免例线程同步问题。

  2. 缺点: 在类装载点时候就完成实例化,没有达成懒加载点效果,如果从始至终从未使用过这个实例,则会造成内存的浪费

  3. 这种方式基于classloder(类加载机制)避免例多线程的同步问题,不过,instance在类装载的时候就实例化,在单例模式中大多数都是调用getinstance方法,不过导致类装载的原因有很多,因此不能确定有其他的方式(或者静态方法)导致类装载,这时候初始化instance就没有达到懒加载的效果

静态代码块饿汉式

1. 代码实现

/**
 * 静态变量(饿汉式)
 *
 * @author caoweifeng
 * @date 2022年02月24日 5:24 PM
 * @email weistuday@qq.com
 * @description:
 */
public class Type2 {
    public static void main(String[] args) {
        //测试
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        System.out.println(instance == instance1); // true
        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());
    }
}

class Singleton {
    /**
     * 1.构造器私有化,外部不能new
     */
    private Singleton() {

    }

    /**
     * 2. 本类内部创建对象实例
     */
    private static final Singleton INSTANCE;

    /*
      静态代码块中,创建单例对象
     */
    static {
        INSTANCE = new Singleton();
    }

    /**
     * 3. 提供一个公有的静态方法,返回实例对象
     */
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

img

2. 优缺点说明

  1. 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面的是一样的。
  2. 结论: 这种单例模式可用,但是可能造成内存浪费

线程不安全懒汉式

1. 代码实现

/**
 * 懒汉式(线程不安全)
 *
 * @author caoweifeng
 * @date 2022年02月24日 5:24 PM
 * @email weistuday@qq.com
 * @description:
 */
public class Type3 {
    public static void main(String[] args) {
        //测试
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        System.out.println(instance == instance1); // true
        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());
    }
}

class Singleton {
    /**
     * 1.构造器私有化,外部不能new
     */
    private Singleton() {

    }

    /**
     * 2. 静态变量
     */
    private static Singleton INSTANCE;


    /**
     * 3. 提供一个公有的静态方法,当使用该方法时,才去创建instance
     */
    public static Singleton getInstance() {
        if (null == INSTANCE) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

img

2. 优缺点

  1. 起到了懒加载的效果,但只能在单线程下使用
  2. 如果在多线程下,一个线程进入了if(null = singleton)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时就会产生多个实例。所以在多线程环境下不可使用这种方式。

img

  1. 结论: 在实际开发种,不要使用这种方式。

线程安全懒汉式

1. 代码实现

package cn.weifeng.creatation.singleton.type4;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 懒汉式(线程安全)
 *
 * @author caoweifeng
 * @date 2022年02月24日 5:24 PM
 * @email weistuday@qq.com
 * @description:
 */
public class Type4 {
    public static void main(String[] args) {
        //测试
//        Singleton instance = Singleton.getInstance();
//        Singleton instance1 = Singleton.getInstance();
//        System.out.println(instance == instance1); // true
//        System.out.println(instance.hashCode());
//        System.out.println(instance1.hashCode());

        //多线程情况下测试
        ExecutorService pool = Executors.newWorkStealingPool(150);
        for (int i = 0; i < 150; i++) {
            pool.execute(() -> {
                System.out.println(Thread.currentThread().getName());
                Singleton instance = Singleton.getInstance();
                System.out.println(instance.hashCode());
            });
        }

    }
}

class Singleton {
    /**
     * 1.构造器私有化,外部不能new
     */
    private Singleton() {}

    /**
     * 2. 静态变量
     */
    private static Singleton INSTANCE;


    /**
     * 3. 提供一个公有的静态方法,当使用该方法时,才去创建instance
     * 加入同步代码块
     */
    public static synchronized Singleton getInstance() {
        if (null == INSTANCE) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

img

2. 优缺点

  • 解决了线程不安全的问题

  • 效率太低了,每一个线程要获取实例对象的时候都需要执行同步代码块操作,其实这个只要执行一次获取实例的办法,后面直接返回就行

  • 实际开发不推荐使用

同步代码块懒汉式

1. 代码实现

package cn.weifeng.creatation.singleton.type5;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 懒汉式(线程安全)
 *
 * @author caoweifeng
 * @date 2022年02月24日 5:24 PM
 * @email weistuday@qq.com
 * @description:
 */
public class Type5 {
    public static void main(String[] args) {
        //测试
//        Singleton instance = Singleton.getInstance();
//        Singleton instance1 = Singleton.getInstance();
//        System.out.println(instance == instance1); // true
//        System.out.println(instance.hashCode());
//        System.out.println(instance1.hashCode());

        //多线程情况下测试
        ExecutorService pool = Executors.newWorkStealingPool(150);
        for (int i = 0; i < 150; i++) {
            pool.execute(() -> {
                System.out.println(Thread.currentThread().getName());
                Singleton instance = Singleton.getInstance();
                System.out.println(instance.hashCode());
            });
        }

    }
}

class Singleton {
    /**
     * 1.构造器私有化,外部不能new
     */
    private Singleton() {}

    /**
     * 2. 静态变量
     */
    private static Singleton INSTANCE;


    /**
     * 3. 提供一个公有的静态方法,当使用该方法时,才去创建instance
     * 加入同步代码块
     */
    public static  Singleton getInstance() {
        if (null == INSTANCE) {
            synchronized (Singleton.class){
                INSTANCE = new Singleton();
            }
        }
        return INSTANCE;
    }
}

img

2 .优缺点

  • 本意是想改进前面的那种同步代码效率低的问题

  • 但是这种方式并不能改变线程同步不安全的问题

  • 实体开发不推荐这种实现方式

DoubleCheck

1. 代码实现

package cn.weifeng.creatation.singleton.type6;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 双重检查
 *
 * @author caoweifeng
 * @date 2022年02月24日 5:24 PM
 * @email weistuday@qq.com
 * @description:
 */
public class Type6 {
    public static void main(String[] args) {
        //测试
//        Singleton instance = Singleton.getInstance();
//        Singleton instance1 = Singleton.getInstance();
//        System.out.println(instance == instance1); // true
//        System.out.println(instance.hashCode());
//        System.out.println(instance1.hashCode());

        //多线程情况下测试
        ExecutorService pool = Executors.newWorkStealingPool(150);
        for (int i = 0; i < 150; i++) {
            pool.execute(() -> {
                System.out.println(Thread.currentThread().getName());
                Singleton instance = Singleton.getInstance();
                System.out.println(instance.hashCode());
            });
        }

    }
}

class Singleton {
    /**
     * 1.构造器私有化,外部不能new
     */
    private Singleton() {}

    /**
     * 2. 静态变量
     */
    private static volatile Singleton INSTANCE;


    /**
     * 3. 提供一个公有的静态方法,当使用该方法时,才去创建instance
     * 加入双重检查机制,解决线程同步问题,同时解决懒加载
     */
    public static  Singleton getInstance() {
        if (null == INSTANCE) {
            synchronized (Singleton.class){
                if (null == INSTANCE) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

img

2. 优缺点

  • 双重检查概念是多线程开发中常使用的,如代码所示,我们进行了两次 if(singleton ==null)检查,这样就可以保证线程安全了

  • 实例化代码只用执行一次,后面再次访问的时,判断if的时候直接return实例化对象,也避免反复进行方法同步

  • 线程安全;延迟加载;效率较高

  • 结论: 在实际开发中,推荐使用这种单例设计模式

静态内部类

1. 代码实现

package cn.weifeng.creatation.singleton.type7;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 静态内部类
 *
 * @author caoweifeng
 * @date 2022年02月24日 5:24 PM
 * @email weistuday@qq.com
 * @description:
 */
public class Type7 {
    public static void main(String[] args) {
        //测试
//        Singleton instance = Singleton.getInstance();
//        Singleton instance1 = Singleton.getInstance();
//        System.out.println(instance == instance1); // true
//        System.out.println(instance.hashCode());
//        System.out.println(instance1.hashCode());

        //多线程情况下测试
        ExecutorService pool = Executors.newWorkStealingPool(150);
        for (int i = 0; i < 150; i++) {
            pool.execute(() -> {
                System.out.println(Thread.currentThread().getName());
                Singleton instance = Singleton.getInstance();
                System.out.println(instance.hashCode());
            });
        }

    }
}

class Singleton {
    /**
     * 1.构造器私有化,外部不能new
     */
    private Singleton() {
    }

    /**
     * 2. 添加静态内部类
     */
    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    /**
     * 3. 提供一个公有的静态方法
     */
    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

img

2. 优缺点

  • 这种方式采用了类装载机制来保证初始化实例时只有一个线程

  • 静态内部类方式在Singleto类被装载的时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingLetonInstance类,从而完成SingLeton的实例化

  • 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证类线程的安全性,在类初始化时,别的线程时无法进入的

  • 优点: 避免了线程不安全,利用静态内部类特点实现延迟加载,效率高

  • 推荐使用

枚举方式

1. 代码方式

package cn.weifeng.creatation.singleton.type8;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 枚举
 *
 * @author caoweifeng
 * @date 2022年02月24日 5:24 PM
 * @email weistuday@qq.com
 * @description:
 */
public class Type8 {
    public static void main(String[] args) {
        //测试
//        Singleton instance = Singleton.getInstance();
//        Singleton instance1 = Singleton.getInstance();
//        System.out.println(instance == instance1); // true
//        System.out.println(instance.hashCode());
//        System.out.println(instance1.hashCode());

        //多线程情况下测试
        ExecutorService pool = Executors.newWorkStealingPool(150);
        for (int i = 0; i < 150; i++) {
            pool.execute(() -> {
                System.out.println(Thread.currentThread().getName());
                Singleton instance = Singleton.INSTANCE;
                System.out.println(instance.hashCode());
            });
        }

    }
}

enum Singleton {
    INSTANCE;

    public void sayOK() {
        System.out.println("OK!");
    }
}

img

2. 优缺点

  • 借助在JKD1.5中添加点枚举来实现单例对象,不仅能避免多线程同步问题,还能防止反序列化重新创建对象

  • 这种方式对Effective Java作者Josh Bloch提倡对方式

  • 结论: 推荐使用

应用场景

什么场景用到?

  • 多线程中的连接池

  • 数据库中的连接池

  • 系统环境信息

  • 上下午(ServletContext)

面试问题

  • 系统环境信息

  • Spring怎么保证组件单例的

  • ServletContext是什么?是单例的吗?怎么保证?

  • ApplicationContext是什么?是单例的吗?怎么保证?

    • ApplicationContext: tomcat: 一个应用(部署的一个war包)会有一个应用上下文
    • ApplicationContext: Spring: 表示整个IOC容器(怎么保证单例),IOC容器中有很多组件(怎么保证单例)
  • 数据库连接池是怎么创建出来的,怎么保证单例?