单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其(唯一)的对象的方式,可以直接访问,不需要实例化该类的对象(不需要每次new)。
温馨提示:
- 单例类只能有一个实例。
- 单例类必须自己创建自己唯一的实例。
- 单例类必须给所有其他对象提供这一实例。
- 保证一个类只能有一个实例,并提供它的全局访问点。
主要来解决一个全局使用的类频繁的创建和销毁,当你想控制实例数目,节约系统资源的时候。
判断系统是否有了这个单例,如果有则返回,没有则创建。在实现单例的共同特点就是构造函数是私有的。
个人不太恰当的理解(如果错了请指正):
把普通模式和单例模式理解为一根火柴(普通)和一个打火机(单例)的区别,火柴的燃烧要燃尽自己为代价,而打火机可以持续的供给,重复的销毁和创建,如果类占用内存空间较大,耗时也长,如果频繁的创建会产生不必要的消耗的时候,我们就可以建一个单例类供我们使用就好了。
举例:
一个班级只能有一个班主任,只有当这个班主任走了,才能有新的班主任过来。
一个电脑有两台打印机设备,在输出的时候就不能两台打印机同时打印一个文件。
优点:
在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁(比如管理学院首页页面缓存)。
避免对资源的多重占用
缺点:
没有接口,不能继承,与单一职责原则冲突,只关心内部逻辑,而不管外面怎么实例化。
注意事项:
getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
单例模式又分为四种(也可以叫五种,枚举用的相对来说比较少)
- 饿汉式
- 懒汉式
- 双检锁/双重校验锁(DCL,即 double-checked locking)
- 登记式/静态内部类
- 枚举
接下来就来一个个介绍........
单例模式创建要求:
- 只能有一个实例化(构造方法私有化)
- 它必须自己创建这个实例(含有一个该类的静态变量来保存这个唯一的实例)
- 自己向整个系统提供这个实例(直接暴露或使用静态变量的get方法)
饿汉式
直接创建对象,不存在线程安全问题。比较容易实现,类似的思想的很多。
举例说明更加清晰:
就好比女人最喜欢的双十一,你一下把你家里不管(需要)还是(不需要)的都买了,认为以后会用的到的也买了。这样就会导致占用内存了。
代码展示:
也可以使用枚举式(最简洁)和 静态代码块饿汉式(适合复杂实例化)
//饿汉式 不管需不需要这个对象都创建
public class Singleton
{
private static Singleton singleton=new Singleton();
private Singleton(){}
public static Singleton getInstance()
{
return singleton;
}
}
懒汉式
从上面我们可以了解到饿汉式早早的就把对象创建好了,在执行一些逻辑的时候不存在线程安全问题,但是懒汉式会延迟创建对象可能会导致线程安全问题
线程不安全的源码展示(适合单线程):
//不支持多线程,适用单线程。
//懒汉式:构造器私有化
//静态变量保存实例
//提供一个静态方法,获取这个实例对象
public class Singleton
{
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance()
{
if (singleton==null)
{
singleton=new Singleton();
}
return singleton;
}
}
对于这种单线程没问题,但是如果如果两个或多个线程来同时访问instance如果都为null同时申请的时候会对系统数据造成错误
线程安全的源码展示(适合多线程):
既然上面那种不能实现多线称,那要这个单例干嘛?所以既然不允许多个线程同时访问,我们就给他加个锁(synchronized)不就可以了。
//支持多线程。
//懒汉式:构造器私有化
//静态变量保存实例
//提供一个静态方法,获取这个实例对象
public class Singleton
{
private static Singleton singleton;
private Singleton(){}
public static synchronized Singleton getInstance()
{
if (singleton==null)
{
singleton=new Singleton();
}
return singleton;
}
}
但是这样有啥问题呢?就是我获取这个单例对象的时候,被上锁了,你气不气?我们就是在创建这个单例时候多线程可能出现点问题,咱么获取的时候不存在线程安全问题啊。。你为啥获取也要锁?? 就好比我们去食堂吃饭,吃饭是不是要一个一个的排队去打饭,但是不妨碍你看着里面的菜流口水《干饭人》
双检锁/双重校验锁:
相对来说实现比较复杂,这种方式采用了双锁(volatile 轻量级锁)(synchronized 重量级锁)机制,安全且在多线程情况下能保持高性能。后面我们就顺便把这两种锁扩展了。
getInstance() 的性能对应用程序很关键。
public class Singleton
{
private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getInstance()
{
if (singleton==null)
{
synchronized (Singleton.class)
{
if (singleton==null)
{
singleton=new Singleton();
}
}
}
return singleton;
}
}
对于上述存在的问题,我们只要双重判定就行了
- 首先判断instance是不是为null。如果不是null说明被实例化过直接返回即可!nice!
- 如果instance为null?上锁之后再判空实例化
或许有人会问了两个null,为什么要两个?
- 第一个:用来判断是否为空需要上锁new 实例。
- 第二个:上锁后虽然只有一个会执行当前方法,但是很可能都为null的时候两个或多个都为null上锁的想构造。然后后面线程在等待同时前面线程构造好了,那么它就不需要再去构造这个
singleton
啦!直接不操作就行了。
这样就相对完美了
登记式/静态内部类(适用于多线程):
这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。利用了 classloader 机制来保证初始化 singleton
时只有一个线程
静态内部类是个好方式。主要是静态内部类和外部类不是一起加载的,并且你去调用它的时候他就会初始化,并且类加载是线程安全的,这个不需要考虑线程安全问题。当加载完之后你就可以直接拿啦。这样也能达到延迟加载的作用。
public class Singleton
{
private static class SingletonHolder
{
private static final Singleton INSTANCE=new Singleton();
}
private Singleton(){}
public static final Singleton getInstance()
{
return SingletonHolder.INSTANCE;
}
}
枚举:
种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
public enum Singleton
{
INSTANCE;
public void whateverMethod() {}
}
懒汉式和饿汉式的区别:
这里举个例子加深一下印象:大家平时都玩过游戏和下过游戏吧,就拿下游戏来举例,拿(我的世界)来举例吧。饿汉式就是一次性把所有的模块全部下完,不管你后续玩不玩都下完了,这样子等待和成本太大,懒汉式就是你要玩什么模块就下什么模块,避免过多的消耗。
volatile和synchronized的区别:
看完上面是不是对这两个锁有了一些的认识,接下来我们就来扩展一下他们的区别。
首先需要理解线程安全的两个方面:
- 执行控制。(执行控制:的目的是控制代码执行(顺序)及是否可以并发执行。)
- 内存可见。(内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。)
区别在于:
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
如果哪里写的不对的地方,还望指教,感谢支持。