目录

单例模式

为什么要使用单例模式

有时候系统只需要一个对象,比如 数据库连接对象、线程池对象

线程池这种对象如果建立多个就会失去它的意义,因为线程池本来就是用来管理多个线程的只需要一个即可

数据库连接对象使用单例并不是指只有一个数据库连接,数据库连接对象里面保存的只是数据库连接相关的信息,比如数据库类型、用户名、密码、URL、字符集等

实际创建个多少TCP连接和实际的并发量有关,因此数据库连接对象也不必每次查询SQL的时候去创建对象,这样就显得多余和麻烦了

这种为了让系统中只存在一个对象的模式就叫 单例模式

单例模式实现方法

  • 饿汉式
  • 方法级别锁、双重检查锁
  • 静态内部类
  • 枚举

饿汉式单例

饿汉式单例指的就是在系统初始化的时候就创建对象

优点是无需考虑多线程问题

缺点是无法懒加载,万一对象始终用不到那就白加载了

Java类成员变量初始化实现

public class DataBaseManager {
    //初始化时就创建
    private final static DataBaseManager db = new DataBaseManager("lyer","55555");
    
    private String username;
    private String password;
    
    //必须隐藏构造方法 外界无法创建
    private DataBaseManager(String username,String password){
        this.username = username;
        this.password = password;
    }
    
    public static DataBaseManager getInstance(){
        return db;
    }
}

Go全局变量实现

go中可以通过 全局变量 或则init函数实现,不过这不是最佳实现方式

var database1 = &DataBase{}
func main() {
	database1.Ping()
}

懒汉单例

懒汉单例就是在需要时候再进行创建,只有在第一次需要的时候再创建对象,懒汉模式唯一需要确定的就是 并发安全问题 如果在判断是否为null的过程中触发了线程调度那么就会破坏单例,因此就需要加锁、双重检查等方式来保证单例

Java方法锁

这种方式每次进入都需要加锁,锁的粒度太大

synchronized  public static LoadBalancer getInstance() {
     if (lb == null) {
          lb = new LoadBalancer();
     }
     return lb;
}

Java双重检查锁

这种方式的锁粒度更细,但是需要两次检查是否为null ,可能第一次if判断由于线程调度可能会进入两个多个线程,最终只有一个线程能抢到锁并且创建对象,进入第一次if的线程会继续抢锁然后创建对象,因此需要在一次判断对象是否为null 防止创建多个对象,如果后面抢到锁的线程则判断对象不为null就不会再创建对象了

指令重排可以参照这篇文章一个单例模式中volatile关键字引发的思考

public class LoadBalancer {
//    阻止变量访问前后的指令重排
    private static volatile LoadBalancer lb;
    //Server name collection
    private List<String> serverList;
    private Integer counter;
    private LoadBalancer() {
        this.serverList = new ArrayList<String>();
        this.counter = 0;
    }
    //double check 双重检查
    public static LoadBalancer getInstance(){
        if(lb==null){
            synchronized (LoadBalancer.class){
                if (lb==null){
                    lb = new LoadBalancer();
                }
            }
        }
        return lb;
    }
}

Java静态内部类实现

内部静态类不会自动初始化,只有调用静态内部类的方法,静态域,或者构造方法的时候才会加载静态内部类,这种也可以实现懒汉式单例,同时也是并发安全的

public class DataBaseManagerStatic {
    private String username;
    private String password;
    private DataBaseManagerStatic(String username, String password){
        this.username = username;
        this.password = password;
    }

    //static inner class
    private static class DataBaseManager{
        private final static DataBaseManagerStatic db = new DataBaseManagerStatic("lyer","55555");
    }
    public static DataBaseManagerStatic getInstance(){
        return DataBaseManager.db; //如果第一次调用,此时静态内部类才会被初始化
    }
}

Go Once实现 (最佳模式)

go可以使用标准库的once对象,此函数能保证只执行一次代码

type DataBase struct{}

func (db *DataBase) Ping() {
}

var (
	database *DataBase
	once     sync.Once
)

func DataBaseInstance() *DataBase {
	once.Do(func() {
		database = &DataBase{}
	})
	return database
}

Java枚举实现单例(最佳模式)

下面是一个负载均衡器的单例模式实现,使用Java枚举的特性,该实现方式是Java单例的最佳实现方式,枚举其实是一种 饿汉式单例enum类型里面其实是用 静态类+静态代码块 来实现的,在枚举类被加载的时候就会立即创建对象

enum LoadBalancerEnum {
    LB;
    //Server name collection
    private List<String> serverList = new ArrayList<String>();
    private Integer counter = 0;
    public void addServer(String serverName) {
        //if contain then skip
        if (this.serverList.contains(serverName)) {
            return;
        }
        this.serverList.add(serverName);
    }
    public void delServer(String serverName) {
        this.serverList.remove(serverName);
    }
    //RoundRobin
    public String getServer() {
        String result = this.serverList.get(this.counter);
        this.counter = (this.counter + 1) % this.serverList.size();
        return result;
    }
}
//3个都是同一个对象
LoadBalancerEnum lb1 = LoadBalancerEnum.LB;
LoadBalancerEnum lb2 = LoadBalancerEnum.LB;
LoadBalancerEnum lb3 = LoadBalancerEnum.LB;
lb1.addServer("localhost:9090");
lb1.delServer("localhost:9090");
lb1.getServer();

单例在Spring中的应用

Spring中所有的Bean都是单例的,如果显式声明需要多个对象则会使用原型模式创建多个对象