单例模式
核心概念
单例模式包装一个类只有一个实例,并且提供一个全局访问点来访问该实例
要求 | 优点 |
---|---|
确保一个类只有一个实例 | 避免重复创建对象,避免资源浪费和冲突 |
提供了一个全局访问点来访问该实例。 | 只有一个全局访问点,控制了访问权限 |
饿汉式
- 确保只有一个实例
- 在加载类的时候就创建一个静态实例(与懒汉式的不同)
- 避免创建实例
- 提供了全局唯一访问点来获取对象
public class Hungry {
//1. 用private修饰,确保只能通过public方法来访问
//2. 用static修饰,HUNGRY属于这个类,保证全局只有一个实例
//3. 用final修饰,保证HUNGRY初始化后无法修改
private static Hungry HUNGRY = new Hungry();
//因为默认的构造方法为共有的
//为了避免重复创建对象,这里的构造方法必须显式的定义为private
private Hungry(){}
//这里的static是提供全局唯一访问点
public static Hungry getInstance(){
return HUNGRY;
}
}
懒汉式(内部类)
利用JVM在加载类时并不会加载内部类,但调用时,则开始加载内部类来创建实例
public class Lazy1 {
//内部静态类
private static class InnerClass{
private static Lazy1 instance = new Lazy1();
}
//私有防止创建对象
private Lazy1(){}
//public和static是为了让外部调用
public static Lazy1 getInstance(){
return InnerClass.instance;
}
}
懒汉式(双重检查锁)
版本一
public class Lazy {
private static Lazy lazy;
private Lazy(){}
public static Lazy getInstance(){
//对lazy进行判断,如果为空就创建,不为空就直接返回
if(lazy == null){
lazy = new Lazy();
}
return lazy;
}
}
缺点:如果多个并发线程同时进入getInstance()
,此时多个线程同时判断lazy==null
,还是会创建多个对象
版本二
public class Lazy {
private static Lazy lazy;
private Lazy(){}
//加了个synchronized修饰,保证每次只有一个线程才可以进入该方法
public static synchronized Lazy getInstance(){
//对lazy进行判断,如果为空就创建,不为空就直接返回
if(lazy == null){
lazy = new Lazy();
}
return lazy;
}
}
每次只有一个线程进入getInstance()
,保证不会创建多个对象
缺点:锁的粒度太大了,每次获得lazy都要进行加锁,效率太低
版本三
public class Lazy {
//volatile是保证lazy的可见性
private volatile static Lazy lazy;
private Lazy(){}
//这里避免了每一次加锁,如果有实例,那么只需要进行一次判断即可,不用加锁
public static Lazy getInstance(){
if(lazy == null){
//如果lazy为null,就锁住Lazy这个类
synchronized (Lazy.class){
//为什么还要判断一次呢,因为可能在synchronized加锁的过程中,其他线程创建了lazy实例
//这里再判断一次,防止重复创建
if(lazy == null){
lazy = new Lazy();
}
}
}
return lazy;
}
}
这样即不会重复创建对象,又不会使锁的粒度过大
双重检查锁
双重检查锁是一种优化多线程实例化单例模式的技巧。它分为两次判断 instance == null
,这样做的目的是为了避免每次获取实例时都进行同步,从而提高性能。具体的步骤如下:
- 第一次判断:检查
lazy
是否为null
,如果不为null
,说明实例已经创建,直接返回已有实例,不需要进入同步代码块,避免了同步的性能开销。 - 第二次判断:如果进入了同步代码块,其他线程已经可能在等待或者进入该同步块执行实例化的操作。为了确保在高并发情况下只会有一个线程实例化
Lzay
,需要再次判断lazy == null
,避免多个线程同时创建实例。
为什么要加锁?
在判断lazy==null
后进行加锁,保证只有一个线程可以进入代码块,其他线程会被阻塞,直到实例化完成。
为什么第一次判断不加锁,第二次判断加锁?
- 第一次判断不加锁:如果实例已经创建,直接返回实例,这样可以避免每次访问
getInstance()
都需要进行加锁,提高性能。 - 第二次判断加锁:只有在第一次判断为
null
时,才会进入加锁区域。在加锁区域里,再次判断instance == null
是为了确保只有一个线程创建实例。即使多个线程进入同步块,也会通过第二次判断避免重复创建实例。
枚举(天然的单例模式)
public enum Singleton {
INSTANCE;
}