单例模式
什么是单例模式?
单例模式顾名思义就是整个类中有且仅有一个的实例对象。
单例模式的三个要点:
- 有且仅有一个私有实例
- 构造方法是私有的,以保证不会被多次创建
- 提供一个访问单例对象的公有静态方法
单例模式分很多种,常见的有饿汉模式、懒汉模式、静态内部类单例模式和双重锁模式…
饿汉模式
饿汉模式,这个比喻可以理解成人饥饿的时候立马就想要吃东西,放到代码里,可以说想要获取一个对象的时候立马就可以得到。饿汉模式中的Singleton对象作为静态成员变量,它在类加载时就已经初始化了,所以想要使用此对象的时候可以立即获取。
代码示例:
public class Singleton{
//1.静态成员:有且仅有一个实例
private static Singleton singleton = new Singleton();
//2.私有的构造方法
private Singleton(){};
//3.获取单例对象的静态方法
public static Singleton getInstance(){
return singleton;
}
}
饿汉模式的特点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存)。
- 避免对资源的多重占用(比如写文件操作)。
- 线程安全。
- 可能造成资源的浪费,类初始化的时候已经创建单例对象实例了,但是它可能不会被使用到。(使用静态内部类单例模式或懒汉式可以解决)
静态内部类单例模式
对饿汉模式进行改进,由于饿汉模式的单例对象初始化不能延迟到它真正被使用的时候,那就想个办法来延迟初始化,静态内部类就可以做到这一点。
代码示例:
public class Singleton{
//静态内部类
public static class Inner{
//1.静态成员
public static final Singleton INSTANCE = new Singleton();
}
//2.私有的构造方法
private Singleton(){};
//3.获取该单例对象的静态方法
public static Singleton getInstance(){
return Inner.INSTANCE; //获取静态内部类中的静态成员
}
}
这种方法的优势在于,Singleton加载时并不会初始化 INSTANCE。只有等到getInstance()方法被显式调用时,才会触发静态内部类Inner进行加载,从而初始化Singleton对象。
只有一个线程时,可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。目前此方式是所有单例模式中最推荐的模式,但具体还是根据项目选择。
特点:
- 延迟初始化,避免资源浪费
- 线程安全
懒汉模式
懒汉模式与饿汉模式的区别在于:懒汉模式将单例对象的初始化延迟到其真正被使用时。
代码示例:
public class Singleton{
//1.静态成员
private static Singleton singleton;
//2.私有的构造方法
private Singleton(){};
//3.访问此对象的公有静态方法
public static getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
特点:
- 延迟初始化。
- 线程不安全。两个线程在单例对象还没创建时,同时执行
if (singleton == null)
语句,那么就会造成线程问题。所以懒汉模式实际上不能算是单例模式。
线程安全的懒汉模式
既然懒汉模式线程不安全,那就让获取实例的静态方法加上synchronized
关键字,它就是线程安全的了。
但是这样就造成一个问题,就是资源的浪费,因为这样的方法效率比较低,绝大多数情况下此方法并不需要加锁。在单例对象已经存在时去获取这个对象,其实不需要线程同步的,可以直接返回instance。
public class Singleton {
//1.定义实例
private static Singleton instance;
//2.私有构造方法
private Singleton(){};
//3.对外提供获取实例的静态方法,对该方法加锁
public static synchronized Singleton getInstance() {
//在对象被使用的时候才实例化
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重锁模式实现懒汉式
针对线程安全的懒汉模式,消除了不必要的加锁步骤,提高其效率;并使用双重校验彻底解决多线程问题。
public class Singleton {
//1.静态成员
private volatile static Singleton singleton; // 使用volatile限制singleton
//2.私有的构造方法
private Singleton (){};
//3.访问单例对象的静态方法
public static Singleton getSingleton() {
//第1次检查
if (singleton == null) {
synchronized (Singleton.class) {
//第2次检查
if (singleton == null) {
singleton = new Singleton(); //保证创建对象时前面的语句已经都执行了
}
}
}
return singleton; //最后执行返回对象语句
}
}
第1重检查:检查实例是否已经存在,避免重复上锁。
第2重检查:同步后避免多线程问题,如果没有加上volatile进行限制,有可能造成先执行了对象返回,然后再进行对象的初始化,这样就导致实际返回的单例是一个空对象。
关键字volatile可以说是java虚拟机中提供的最轻量级的同步机制。volatile类型的变量保证对所有线程的可见性;volatile类型的变量禁止指令重排序优化。
Singleton对象的创建在JVM中可能会进行重排序。用volatile关键字修饰instance变量,使得instance在读、写操作前后都会插入内存屏障,避免重排序。
枚举单例模式
在1.5之后,还有另外一种实现单例的方式,那就是使用枚举。枚举单例模式在《Effective Java》中推荐的单例模式之一。
public enum Singleton{
//枚举单例对象
INSTANCE;
//访问单例对象的静态方法
// public static Singleton getInstance(){
// return INSTANCE;
// }
}
其中,访问单例对象的静态方法可以省略,然后通过Singleton.INSTANCE进行操作。其次,枚举类是没有构造方法的,因此通过反射也不能创建其实例,所以是线程安全的。
它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
在以上所有的单例模式中,推荐静态内部类单例模式。主要是非常直观,即保证线程安全又保证唯一性。
序列化
由于序列化可以破坏单例。要想防止序列化对单例的破坏,只要在Singleton类中定义readResolve,以让实例唯一,就可以解决该问题:
private Object readResolve() throws ObjectStreamException{
return singleton;
}
参考链接:
https://blog.csdn.net/jisuanji198509/article/details/81260487 Java中的静态成员何时被初始化?
https://www.jianshu.com/p/15106e9c4bf3 Java内存模型
https://blog.csdn.net/hla199106/article/details/44965315?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.control 静态变量,成员变量,局部变量
很详细的介绍
https://www.hollischuang.com/archives/1373
https://www.jianshu.com/p/3bfd916f2bb2
https://www.cnblogs.com/crazy-wang-android/p/9054771.html
https://www.jianshu.com/p/c08ab7115f6d Volatile关键字理解