单例模式是一个很经典的设计模式,在java中无处不在。比如spring中的bean注入,工具类的编写等。
但是在刚接触单例模式时候,我总对这个饱汉式和饿汉式的称呼理解不好。甚至自己也一度加入自己理解,创建新的定义,比如饥饿营销,传统销售。其实本来是很直观的概念,不用再增加新定义的词汇来理解。
目录
一、初识
1)提前创建对象
class A{
private A(){}
private static A instance = new A();//这里更像传统销售产品,加载类时,先把商品(实例)准备好
public static A getInstance(){
return instance;//传统的销售模式,我已经准备好商品了,你需要,我直接给你。
}
}
!注意:该模式如果A类中的东西很多,那么创建A对象时所花时间更多(整个类的加载变慢了)
这里还可以用枚举来实现单例,推荐使用这种方法来实现单例模式,简单粗暴高效有没有。
public enum SingletonEnum {
INSTANCE1("实例1",1),INSTANCE2("实例2",2);
/**
* 实际上INSTANCE1变量的默认缺省等于是:
* public static final INSTANCE1 = new SingletonEnum("实例1",1);
* 所以,语法糖的味道你知道
*/
SingletonEnum(String name, Integer age) {
this.name = name;
this.age = age;
}
private String name;
private Integer age;
public String getName() {
return name;
}
public Integer getAge() {
return age;
}
@Override
public String toString() {
return "SingletonEnum{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
//测试类
public class Test {
public static void main(String[] args){
System.out.println(SingletonEnum.INSTANCE1);
System.out.println(SingletonEnum.INSTANCE2.getName());
}
}
2)需要时再创建对象
class A{
private A(){}
private static A instance;
public static A getInstance(){//在加载类的时候不创建A对象,不生产B商品,在有人预定的时候,再去生产(创建A对象)
if(instance == null){
//买家1(线程1) 买家2(线程2)
instance = new A();//这里就会有线程安全问题,如果买家1预定了,A对象还没生成完,买家2又开始预定,那么就会生成两个不同的A,破坏了单例。
}
return instance;
}
}
!!!注意:该模式存在线程安全问题
解决方式:添加synchronized关键字到静态方法,或者在if语句外,添加synchronized静态代码块,传入参数(A.class)
3)扩展,双重校验锁实现单例模式(线程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;
//因为 new 这个指令是非原子操作,底层是分成几条指令来执行的,加上 volatile 是禁止指令重排,保证别的线程读到的时候一定是状态和引用正常的、一个完整的对象,防止其他线程看到的是对象还没有完全实例化的内容。
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
-
为 uniqueInstance 分配内存空间
-
初始化 uniqueInstance
-
将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
二、好处
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
- 减轻 GC 压力,缩短 GC 停顿时间,使用单例模式由于 new 操作的次数减少,因而对系统内存的使用频率也会降低。
- 节省了内存开销