单例模式的写法
什么是单例模式 ?
单例模式数以Java 中的设计模式之一, 这种模式设计到一个单一的类, 该类负责创建自己的对象, 并且保证只有单个对象被实例化 , 并且该类只提供一种获得该类实例的方法,(构造设置为私有)
- 该单例类只允许存在一个类实例,
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一事例
为什么要设计单例模式?
如果一个类的实际大小由100G(假设很大) 但是内存只有200G 此时 我们必须保证该类只能实例化一个对象, 如果程序员不小心多创建了一个该类, 那么此时内存就会被占满, 从而出现问题. 因此我们设计出单例模式来避免该问题, 提供一个唯一的获取实例的方法,这样就不会出现创建出两个实例的情况了.
饿汉模式
单例模式的第一种方法, 也就是当创建一个对象的时候, 直接初始化该对象的构造(对唯一的实例的初始化比较着急,类加载的时候会直接创建实例)
class Singleton {
//也就是在一个Java程序中,一个类对象只存在一份(JVM保证的)
//进一步也就保证了类的static成员也是只有一份 唯一的.
//1. 使用static 创建一个实例 并且立即实例化.
// 这个instance 对应的实例, 就是该类的唯一实例.
private static Singleton instance = new Singleton();
//2. 为了防止程序员不小心在其他地方new了一个对象 因此 构造方法创private
private Singleton(){}
//3. 提供一个方法 让外面能拿到唯一实例
public static Singleton getInstance(){
return instance;
}
}
外面拿到该实例只能通过 调用 getInstance 方法 来获取该对象的实例
public static void main(String[] args) {
//保证有唯一实例 对象不能new出来
Singleton singleton = Singleton.getInstance();
}
懒汉模式
单例模式的第二种方法,在外部调getInstance 方法的时候在开始对唯一的实例进行初始化,
好处是: 节省了整体的资源
坏处是: 需要考虑在多线程的情况, 创建实例是否安全
class Singleton2{
//1.就不是立即初始化实例
private static Singleton2 instance = null;
//2.把构造方法设为private
private Singleton2 (){}
//3.提供一个方法来获取到上述单例的实例
// 只有当真正需要用这个实例的时候,才会真正的去创建这个实例.
public static Singleton2 getInstance(){
if (instance == null) {
return new Singleton2();
}
return instance;
}
}
public static void main(String[] args) {
//保证有唯一实例 对象不能new出来
Singleton2 singleton = Singleton2.getInstance();
}
懒汉模式在多线程中的安全问题(重点)
在多线程中, 因为线程的调度是随机的, 因此可能会多个线程同时调用 getInstance 方法, 如果不进行处理, 可能就会出现该类被多个线程创建了实例,此时该模式就不能成为单例模式了.
因此我们需要对instance方法进行加锁, 保证该方法的原子性
这样就可以保证if内的两个方向 (一个判断成功创建实例,一个判断失败返回实例) 成为原子性.
class Singleton2{
private static Singleton2 instance = null;
private Singleton2 (){}
public static Singleton2 getInstance(){
synchronized (Singleton2.class) {
if (instance == null) {
return new Singleton2();
}
}
return instance;
}
}
仔细看这个代码 我们就会产生出问题, 单例模式的实例的创建只会创建一次, 在实例创建完成之后, 每个线程都会继续进行判断if 可是此时明明实例以及被创建 , 剩下的只是读取操作 而不是修改操作, 我们之前说的多个线程读取同一份变量的时候不会引发线程安全的问题. 因此 我们可以在synchronized 外面再加一层判断, 如果说 实例已经被创建 那么我们就不会进行判断 ,加锁, 从而降低效率 , 而是直接进行返回实例操作. 并对变量加入volatile 关键字 保证内存可见性.
class Singleton2{
//1.就不是立即初始化实例
//增加 volatile 保证内存可见性
private static volatile Singleton2 instance = null;
//2.把构造方法设为private
private Singleton2 (){}
//3.提供一个方法来获取到上述单例的实例
// 只有当真正需要用这个实例的时候,才会真正的去创建这个实例.
public static Singleton2 getInstance(){
if (instance == null) {
//未初始化过的单例的才会出现线程安全问题.
synchronized (Singleton2.class){
if (instance == null) {
return new Singleton2();
}
}
}
return instance;
}
}
当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁,其中竞争成功的线程, 再完成创建实例的操作.
当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例
此时又有读者会有疑问了,如果此时我们直接在第一个if内直接进行加锁 ,返回实例不就可以了?
即:
if (instance == null) {
synchronized (Singleton2.class) {
return new Singleton2();
}
}
return instance;
答案是不可以的, 因为之间我们说了, if 判断 是分为两个操作的, 因此,为了保证原子性, 需要将内层的if 进行包裹, 因为两个if 的作用是完全不一样的.
总结:
-
需要加两层if ,第一层是判断线程是否已经实例完对象, 如果完成就不需要进行锁竞争, 直接获取instance对象,
第二层是当线程还没有创建实例, 防止多个线程进行创建实例,而导致实例被创建了多份,
-
需要加volatile关键字, 如果实例创建完成, 此时只剩下读取操作, 不会再修改instance 的值, 为了保证内存可见性, 加上volatile 关键字.
-
加synchronized关键字, 保证内层if 语句的原子性, 就不会再出现实例被创建了多份.