当前位置:   article > 正文

单例模式

单例模式

定义:

确保一个类只有唯一的一个实例对象,并向整个系统提供方法访问这个实例。

单例模式的重点在于全局维持一个唯一的对象,一般是对使用共享资源的同步控制,同时可以避免重复创建对象以节约资源。

示例代码:

public class Singleton {
    /**
     * 静态私有的成员变量持有Singleton对象的引用
     */
    private static Singleton singleton;

    /**
     * 私有构造函数,只有类的内部可以使用
     */
    private Singleton() {

    }

    /**
     * 静态方法获取实例对象
     */
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

public void doSomething() {
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
public class Client {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        instance.doSomething();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

单例模式中的线程安全问题

在高并发多线程访问单例的情况下,上述代码是不安全的。为什么?假设第一个线程执行到getInstance()中的singleton = new Singleton()时,第二个线程也开始访问getInstance()方法,但是此时第一个线程要获取的实例对象还没完成,也就是说new Singleton()需要一定的时间,那么此时第二个线程访问判断singleton == null的条件还是成立的,那就会进行创建对象的过程。因此这时会出现创建了两个对象,两个线程访问的不是同一个对象,假如有若干线程同时访问,则产生对象的数量不唯一。

解决单例线程安全的方法:

1. 将访问方法变为同步方法(synchronized)

public class Singleton {
    private static Singleton singleton;

    private Singleton() {}
    
    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

使用synchronized关键字可以避免多线程访问的安全问题,这是java语言的特性,synchronized使得一个线程在访问此方法时必须等待另一个线程离开该方法,相当于给方法加锁,但是这样也不是最佳的方式,因为一旦加了synchronized关键字就意味着在每次访问时都要进行同步操作,而同步是影响性能的,假如同步造成的性能影响对系统来说可以忽略,那么你可以不必在意。

2.使用饿汉单例模式

上述代码所示例的单例模式又称为懒汉单例模式,与之对应的还有一种就是饿汉单例模式,懒汉是将对象初始化延时处理,而饿汉则是在定义成员变量的时候直接初始化对象:

public class Singleton {
    private static Singleton singleton = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return singleton;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这种方式是直接静态初始化一个实例对象,Java虚拟机JVM在加载类的时候就会创建该实例,这样就能保证在任何线程访问的时候该实例一定存在且唯一了。因为单例模式的目的就是确保存在一个唯一对象, 如果直接静态初始化带来的负担不是很大,可以考虑这种方式。但是如果new Singleton()需要的代价比较大,比如会占用极高的内存或者比较耗时,那么你需要慎重考虑了,因为假如实例对象并没有机会被访问到(假设你只用到了其中的静态方法),那么直接静态初始化了一个对象是不是浪费了资源呢。

饿汉和懒汉相比,还有一种极端的情况就是,假如对象长期未被使用,JVM是有可能回收掉该对象的,一旦被回收掉,懒汉模式下getInstance()会重新创建该对象,而饿汉模式下getInstance()则会直接返回null,但是这种情况现在基本不会存在,在java1.2以前会存在这个问题,后续的jdk版本中java修复了这个bug。

3.使用双重检查加锁减少同步次数(Double Check Lock)

为了减少同步的次数,我们可以将synchronized移到方法的内部,因为实际上一旦我们创建好了实例对象,实例对象已经存在的情况下,其它线程再来访问肯定是同一个对象。所以只需要在第一次访问的时候提供同步加锁就可以,第二次对象已存在就不需要再加锁了。

public class Singleton {
	 /**使用volatile保证对该变量的写入对另一个线程可见*/
    private volatile static Singleton mSingleton = null;
    
    private Singleton(){}
    
    public static Singleton getInstance() {
        if (mSingleton == null) {
            synchronized (Singleton.class) {
                if (mSingleton == null) {
                    mSingleton = new Singleton();
                }
            }
        }
        return mSingleton;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

可以看到代码中只有mSingleton == null的时候才对Singleton进行synchronized操作,这样当再次访问的时候就不会走这里了。

代码中静态实例使用了volatile关键字进行修饰,在JVM中使用volatile修饰的变量具有下面两个特性:

  1. 当对一个volatile变量写操作时,JMM会强制保证该变量对其他线程立即可见
  2. 这个写会操作会导致其他线程中该变量的缓存无效

关于volatile详细介绍可以参考这个文章:谈谈Java中的volatile

4.使用静态代码块或静态内部类优化单例模式

第3种方案虽然解决了多余的同步问题,但是在某些情况下仍然会出现失效的问题,该问题被称为双重检查锁定失效,有人认为它是“丑陋的优化”。那么有没有更加优雅的方式呢?
我们知道,在java当中使用静态内部类时,只有当静态内部类被调用的时候才会加载相应的代码,因此我们可以利用java这一特性来优化单例模式。

public class Singleton {
    private Singleton(){}
    
    /**
     * 静态内部类,内部类的实例与外部类的实例没有绑定关系,
     * 只有被调用到时才会加载,从而实现延迟加载
     */
    private static class SingletonHolder{
        /** 静态初始化器,由JVM来保证线程安全 */
        private static Singleton instance = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

当getInstance方法第一次被调用的时候,会导致SingletonHolder类得到初始化;而这个类在初始化的时候,会初始化它的静态成员变量,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。假如getInstance()方法没有被调用,那么SingletonHolder中的静态变量也不会初始化。

这种方式完全利用JVM的特性,避免了使用synchronized的方式进行加载同步,保证线程安全的同时也做到了延时实例化避免了饿汉模式的资源浪费,将对系统的影响降低到最低。


参考:
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/267570
推荐阅读