单例模式属于设计模式三大分类中的第一类——创建型模式
这个模式在创建对象的同时,还致力于控制创建对象的数量,是的,只能创建一个实例
每个Java程序员都知道,Java中的对象都是使用new关键字来加载类并在堆内存中开辟空间创建对象,这是平时用到最多创建对象的方式。也知道每次new都会产生一个全新的对象。
那么问题来了,到底我们为什么要控制对象创建的个数?直接new一下多省事啊
既然这个模式存在并且大量使用,说明有些场景下,没它还真不行。那么什么场景下会没它不行呢?我举个栗子,比如我们平时使用的Windows上的回收站,是不是只有一个?要是有多个,会发生什么?我刚把回收站清空了,换到另一个回收站看垃圾还在,那这垃圾到底是在,还是不在?是不是很诡异了?另外比如博客上会有一个博客访问人数统计,这个东西要是不是单例的会有啥问题?今天统计了流量有100个,第二天用了一个新的计数器,又回到0了重新开始统计,那这个统计还有意义吗?
也就是说,有些场景下,不使用单例模式,会导致系统同一时刻出现多个状态缺乏同步,用户自然无法判断当前处于什么状态
首先给单例下一个定义:在当前进程中,通过单例模式创建的类有且只有一个实例。
单例有如下几个特点:
-
在Java应用中,单例模式能保证在一个JVM中,该对象只有一个实例存在
-
构造器必须是私有的,外部类无法通过调用构造器方法创建该实例
-
没有公开的set方法,外部类无法调用set方法创建该实例
-
提供一个公开的get方法获取唯一的这个实例
那单例模式有什么好处呢?
-
某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销
-
省去了new操作符,降低了系统内存的使用频率,减轻GC压力
-
系统中某些类,如spring里的controller,控制着处理流程,如果该类可以创建多个的话,系统完全乱了
-
避免了对资源的重复占用
饿汉式
当类一初始化,该类的对象就立刻会被实例化
public class Hungry {
//构造器用private修饰,防止外部手动通过new创建
private Hungry(){
}
//HUNGRY使用static修饰,然后调用new创建对象,我们知道static修饰的东西都属于类,而且在类加载阶段就已经被加载,并且只能被加载一次。就是类加载这种特性很好的保证了单例的特性,也天然防止了并发的问题。
private static Hungry hugry = new Hungry();
public static Hungry getHungry(){
return hugry;
}
}
问题:如果创建这个对象极其耗费时间和资源呢?这样必然会造成巨大的性能损耗。有的时候我只是想单纯的加载一下类,但并不想去用该对象,那这个时候这种模式就属于浪费内存了
当去访问一个类的静态属性的时候会触发该类初始化,这就导致,我明明只是想使用一下属性,并不想用
Hungry对象,但由于你访问了静态属性导致Hungry的初始化,从而导致HUNGRY被实例化,造成内存泄露。
懒汉式
需要用该对象才去创建对象
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName());
}
private static LazyMan lazyMan;
//问题 单线程没有问题,多线程有问题
public static LazyMan getInstance(){
if (lazyMan==null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
问题:多线程并发
public static void main(String[] args) {
for (int i = 0; i < 10; i++){
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
解决方法:
双重检测锁模式 DCL懒汉式
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan = new LazyMan();
/*
1.申请内存空间
2.初始化默认值
3.执行构造器初始化
4.将lazyMan指向创建的对象
*/
}
}
}
return lazyMan;
}
问题:不是原子性操作, 指令重排
些编译器会对代码做指令重排序,因为3和4本身相互并不存在依赖,指令重排序的存在可能会导致3和4顺序发生颠倒。
这会有什么问题?
首先在单线程下并不会有什么问题,为什么?因为指令重排序的前提就是不改变在单线程下的结果,无论先执行3还是4,最后返回的对象都是初始化好后的。
但是在多线程下呢?设想一种极端场景,现在假设A线程拿到锁进入到处,然后它完成了上面4步的1和2,因为现在指令重排序了,下面A线程会将instance指向创建的对象,也就是说,此时instance != null了!然后正当A要去执行构造器初始化对象时,巧得很,这时候B线程来到处,判断instance == null不成立了,直接返回,独留A线程在原地骂娘“尼玛,我还没初始化对象呢……”,因为返回了一个没有经过初始化的对象,后续操作自然会有问题。
正是因为这个原因,所以处volatile不可省略,主要原因就在于防止指令重排序,避免上述问题。那是不是这样就万无一失了呢?很遗憾,上述如此严密的控制,还是不能完全保证出问题。What?
那就是上述的做法有个前提,JDK必须是JDK5或更高版本,因为从JDK5才开始使用新的JSR-133内存模型规范,而在这个规范中才增强了volatile这个语义……
private volatile static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan = new LazyMan();// 不是一个原子性操作
}
}
}
return lazyMan;
}
就算不考虑JDK版本这个问题,这种方案的实现代码太过丑陋,本身看着就不是很爽,而且考虑的东西太多,稍有闪失就GG了。所以,这种方案虽然分析了这么多,但是其实没有实际意义,实际工作中强烈不建议使用。
静态内部类
根据类加载机制,外部类的初始化并不会导致静态内部类的初始化。
这种方式是懒汉式的另一种方式,即做到了延迟加载,又避免了双重检测锁
public class Holder {
//1.构造器私有化,外部不能new
private Holder(){}
//2.写一个静态内部类,直接返回Holder
private static class InnerClass{
private static final Holder HOLDER = new Holder();
}
//3.提供一个静态的公有方法,直接返回InnerClass.HOLDER
public static Holder getInstance(){
return InnerClass.HOLDER;
}
}
优缺点说明:
1) 这种方式采用了类装载的机制来保证初始化实例式只有一个线程。
2 ) 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
3) 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程时无法进入的。
4) 优点: 避免了线程不安全,利用静态内部类特点实现延迟加载,效率高。
5) 结论:推荐使用
使用枚举
//enum 本身也是一个class类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
public static void main(String[] args) {
EnumSingle instance1 = EnumSingle.INSTANCE;
EnumSingle instance2 = EnumSingle.INSTANCE;
System.out.println(instance1);
System.out.println(instance2);
}
/*
输出结果
INSTANCE
INSTANCE
*/
唯一遗憾的是,这个方案和饿汉式一样,没法延迟加载。枚举类加载自然就会初始化INSTANCE
如何选择
从安全性角度考虑,枚举显然是最安全的,保证绝对的单例,因为可以天然防止反射和反序列化的破解手段。而其它方案一定场合下全部可以被破解。
从延迟加载考虑,懒汉式、双重检测锁、静态内部类方案都可以实现,然而双重检测锁方案代码实现复杂,而且还有对JDK版本的要求,首先排除。懒汉式加锁性能较差,而静态内部类实现方法既能够延迟加载节约资源,另外也不需要加锁,性能较好,所以这方面考虑静态内部类方案最佳。
一般选用原则
单例对象占用资源少,不需要延时加载:枚举式好于饿汉式。
单例对象占用资源大,需要延时加载:静态内部类式好于懒汉式。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/95931.html