单例有如下几个特点:
- 在Java应用中,单例模式能保证在一个JVM中,该对象只有一个实例存在
- 构造器必须是私有的,外部类无法通过调用构造器方法创建该实例
- 没有公开的set方法,外部类无法调用set方法创建该实例
- 提供一个公开的get方法获取唯一的这个实例
单例模式的好处:
- 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销
- 省去了new操作符,降低了系统内存的使用频率,减轻GC压力
- 系统中某些类,如spring里的controller,控制着处理流程,如果该类可以创建多个的话,系统完全乱了
- 避免了对资源的重复占用
写法:
- 饿汉
public class Singleton {
// 创建一个实例对象
private static Singleton instance = new Singleton();
/**
* 私有构造方法,防止被实例化
*/
private Singleton(){}
/**
* 静态get方法
*/
public static Singleton getInstance(){
return instance;
}
}
之所以叫饿汉式字面意思,就是饿,着急new对象,他想提前把对象new出来,这样别人哪怕是第一次获取这个类对象的时候直接就存在这个类了,省去了创建类这一步的开销。
- 懒汉
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式大家可以理解为他懒。就是懒得吃,等别人问他吃了没,他再想想自己吃没吃,别人第一次调用的时候他发现自己的实例是空的,然后去初始化了,再赋值,后面的调用就和饿汉没区别了。
懒汉和饿汉的对比:大家可以发现两者的区别基本上就是第一次创作时候的开销问题,以及线程安全问题(线程不安全模式的懒汉)。
那有了这个对比,那他们的场景好理解了,在很多电商场景,如果这个数据是经常访问的热点数据,那我就可以在系统启动的时候使用饿汉模式提前加载(类似缓存的预热)这样哪怕是第一个用户调用都不会存在创建开销,而且调用频繁也不存在内存浪费了。
而懒汉式呢我们可以用在不怎么热的地方,比如那个数据你不确定很长一段时间是不是有人会调用,那就用懒汉,如果你使用了饿汉,但是过了几个月还没人调用,提前加载的类在内存中是有资源浪费的。
怎么解决懒汉线程安全问题?
- 1.双重检查
public class Singleton2 {
private static volatile Singleton2 instance;
private Singleton2(){}
public static Singleton2 getInstance(){
if (instance == null){
synchronized (Singleton2.class){
if (instance == null){
instance = new Singleton2();
}
}
}
return instance;
}
}
用两次判断加同步代码块实现线程安全
用双重检查实现的懒汉式,在多线程场景中getIntance时,首先要进行一次对象是否已创建的判断,如果已创建就直接返回实例,当首次加载需要创建对象时,假设有线程A和线程B两个线程同时通过第一层判断,那么它们需要排队进入同步代码块,假设线程A先进入同步代码块,那么实例由线程A创建,那么当线程B进入同代码块时便不能通过第二层检查,即直接返回实例。这样便实现了线程安全的懒加载。
关于volatile关键字:
volatile有两个作用:保证可见性和防止指令重排
什么是保证可见性呢,就是当一个线程在对主内存的某一份数据进行更改时,改完之后会立刻刷新到主内存。并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。意思就是所有线程都可以得到最新的数据,这样一来就保证了可见性。
什么是指令重排呢,当new一个对象时,用字节码指令分析是三条指令(new、dup、invokespecial),这三条指令可能会发生重排序,引用指向分配地址,但对象还未创建,导致判空校验不准确。
因为创建对象的过程都在同步代码块中,所以此处使用volatile的作用主要是保证可见性。
- 2.静态内部类
public class Singleton3 {
private Singleton3(){}
private static class SingletonInstance{
private static final Singleton3 INSTANCE = new Singleton3();
}
public static Singleton3 getInstance(){
return SingletonInstance.INSTANCE;
}
}
静态内部类:1.当外部类被装载时,内部类并不会被装载 当使用到时才会被装载,且只装载一次。
- 3.枚举
enum Singleton4 {
INSTANCE;
public void method(){
System.out.println("枚举实现单例");
}
}
使用:
Singleton4.INSTANCE.method();
枚举是最简单也是最好用的实现方式,枚举的实际是用final修饰的实现enum接口的类,因为枚举构造只能私有,所以枚举是天生的单例模式
因为枚举类是在第一次访问时才被实例化,所以它也是懒加载的。