单例模式是一种比较常见的设计模式,但是在Java中要用好单例模式,并不是一件简单的事。在整个系统中,单例类只能有一个实例对象,且需要自行完成示例,并始终对外提供同一实例对象。
因为单例模式只允许创建一个单例类的实例对象,避免了频繁地创建对象,所以可以减少GC的次数,也比较节省内存资源,加快对象访问速度。例如,数据库连接池、应用配置等一般都是单例的。
单例模式有很多种写法,但是有些写法在特定的场景下,尤其是多线程条件下,无法满足实现单一实例对象的要求, 从而导致错误。首先我们来介绍比较经典的懒汉模式和饿汉模式,具体实现如下:
//饿汉模式
public class Singleton{
//饿汉模式是最简单的实现方式,在类加载的时候就创建了单例类的对象
private static final Singleton instance = new Singleton();
//单例类的构造方法都是私有的,防止外部创建单例类的对象
private Singleton();
public static Singleton newInstance(){
return instance;//返回唯一的到单例对象
}
}
//懒汉模式
public class Singleton{
private static Singleton instance = null;
private Singleton();
public static Singleton newInstance(){
//在需要的时候才去创建单例对象,如果单例对象已经创建,再次调用newTnstance()方法时
//将不会重新创建新的单例对象,而是直接返回之前创建的单例对象
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
懒汉模式有延迟加载的意思,如果创建单例对象会消耗大量资源的情况下,在真正使用单例对象时创建其实例是- - -个不错的选择。但是懒汉模式有一个明显的问题,就是没有考虑线程安全的问题,在多线程的场景中,可能会有多个线程并发调用newInstance()方法创建单例对象,从而导致系统中同时存在多个单例类的实例,这显然不符合需求。我们可以通过给newInstance()方法加锁解决该问题,得到如下代码:
//懒汉模式
public class Singleton{
private static Singleton instance = null;
private Singleton();
//使用synchronized为newInstance()方法加锁
public static synchronized Singleton newInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
虽然这种修改方式可以保证线程安全,但是每次访问newInstance()方法时, 都会进行一次加锁和解锁操作,而单例类是全局唯的,该锁就可能成为系统的瓶颈。为了解决问题,可以就有人提出了“双重检查锁定"的方式,但请读者注意,这是错误的写法,具体代码如下:
public class Singleton{
private static Singleton instance = null;
private Singleton();
//使用synchronized为newInstance()方法加锁
public static synchronized Singleton newInstance(){
if(null == instance){//第一次检测
synchronized(Singleton.class){//加锁
if(null == instance){//第二次检测
instance = new Singleton();
}
}
}
return instance;
}
}
由于指令重排优化,可能会导致初始化单例对象和将该对象地址赋值给instance字段的顺序与上面Java代码中书写的顺序不同。例如,线程A在创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时线程A就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。线程B来调用newInstance(方法,得到的就是未初始化完全的单例对象,这就会导致系统出现异常行为。
为了解决该问题,我们可以使用volatile 关键字修饰instance字段。volatile 关键字的一个语义就是禁止指令的重排序优化,从而保证instance字段被初始化时,单例对象已经被完全初始化。最终得到的代码如下所示。
public class Singleton{
//使用volatile关键字修饰instance字段
private static volatile Singleton instance = null;
private Singleton();
public static Singleton newInstance(){
if(null == instance){//依旧是双重检测
synchronized(Singleton.class){
if(null == instance){
instance = new Singleton();
}
}
}
return instance;
}
}
相对于双重检测 的写法,推荐使用静态内部类的单例模式写法,这种写法也可以实现延迟加载的相关,且通过类加载机制保证只创建一个单例对象,具体写法如下:
public class Singleton{
//私有的静态内部类,该静态内部类只会在newInstance()方法中被使用
private static class SingletonHolder{
//静态字段
public static Singleton instance = new Singleton();
}
private Singleton();
public static Singleton newInstance(){
return SingletonHolder.instance;//访问静态内部类中的静态方法
}
}
熟悉Java类加载机制的读者知道,当第一次访问类中的静态字段时;会触发类加载,并且同一个类只加载一次。静态内部类也是如此,类加载过程由类加载器负责加锁,从而保证线程安全。这种写法相比较于创双重检测锁的方法,更加简洁明了,更不容易出错。
来源:CSDN
作者:China.wyb
链接:https://blog.csdn.net/qq_37839971/article/details/103873505