单例模式是什么
单例设计模式( Singleton Pattern)是一种创建型设计模式,它保证类只能有一个实例,并提供一个全局访问点。
在单例模式中,一般采用“懒加载”的方式来创建类实例,即只有在第一次访问时才会创建实例,此后每次访问都使用同一实例。
在实现单例模式时,需要注意以下几点:
1.私有化构造方法,以防止外部实例化;
2.提供一个私有静态变量来存储实例;
3.提供一个公共静态方法来获取实例,该方法一般是线程安全的;
4.如果需要支持序列化和反序列化,要实现 SERIALIZABLE接口和 readResolve方法。
为什么要有单例模式
从开发者本身来考虑的。比如日志类、配置类、连接池等,如果是一样的配置文件且不是单例的,就浪费了很多资源,而且也不知道是依哪一个为准。
当我们在应用中遇到功能性冲突的时候就需要使用单例模式。
懒汉式单例和饿汉式单例优缺点
1、时间和空间
懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。
2、线程安全
(1):饿汉式线程安全,静态修饰符修饰的属性,jvm类加载机制保证对象只会被创建一次
public class Singleton02 {
// 三部曲
//1.私有构造方法,不让外界进行创建对象。
private Singleton02() {
}
//2.静态变量实例
private static Singleton02 singleton = new Singleton02();
//3.通过提供一个静态方法来获取我们的单例
//该单例模式是饿汉式,及加载该类字节码的时候该单例就已经被创建。
public static Singleton02 getInstance() {
return singleton;
}
}
(2):懒汉式
public class Singleton2 {
// 1:私有的构造函数
private Singleton2() {
}
// 2:静态变量
private static Singleton2 instance;
// 3:静态方法
public static Singleton2 getInstance() {
// 4:读取instance的值
if (instance == null) {
// 5: 实例化instance
instance = new Singleton2();
}
return instance;
}
}
对于以上代码注释部分,如果此时有AB两个线程,线程A执行到4处,读取了instance为null,然后cpu就被线程B抢去了,此时,线程A还没有对instance进行实例化。因此,线程B读取instance时仍然为null,于是,它对instance进行实例化了。然后,cpu就被线程A抢去了。此时,线程A由于已经读取了instance的值并且认为它为null,所以,再次对instance进行实例化。所以,线程A和线程B返回的不是同一个实例。
(3):懒汉式加在方法前面加synchronized修饰
public class Singleton2 {
private Singleton2() {
}
private static Singleton2 instance;
public static synchronized Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
这种线程安全的解决方式,假如有100个线程同时执行,那么,每次去执行getInstance方法时都要先获得锁再去执行方法体,如果没有锁,就要等待,耗时长,变成了串行处理。因此,尝试其他更好的处理方式。
(4)懒汉式加同步代码块,减少锁的颗粒大小
public class Singleton2 {
private Singleton2() {
}
private static Singleton2 instance;
public static Singleton2 getInstance() {
if (instance == null) {
synchronized (Singleton2.class) {
instance = new Singleton2();
}
}
return instance;
}
}
只有第一次instance为null的时候,才去创建实例,而判断instance是否为null是读的操作,不可能存在线程安全问题,因此,我们只需要对创建实例的代码进行同步代码块的处理,也就是所谓的对可能出现线程安全的代码进行同步代码块的处理。
但是,这样处理其实还是有问题,同样的原理,线程A和线程B,线程A读取instance值为null,此时cpu被线程B抢去了,线程B再来判断instance值为null,于是,它开始执行同步代码块中的代码,对instance进行实例化。此时,线程A获得cpu,由于线程A之前已经判断instance值为null,于是开始执行它后面的同步代码块代码。它也会去对instance进行实例化。这样就导致了还是会创建两个不一样的实例。
(5)懒汉式双重检查加锁机制
同步代码块中instance实例化之前进行判断,如果instance为null,才对其进行实例化。这样,就能保证instance只会实例化一次了。
public class Singleton2 {
private Singleton2() {
}
private static Singleton2 instance;
public static Singleton2 getInstance() {
if (instance == null) {
synchronized (Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}
再次分析上面的场景:
线程A和线程B,线程A读取instance值为null,此时cpu被线程B抢去了,线程B再来判断instance值为null。于是,它开始执行同步代码块代码,对instance进行了实例化。这是线程A获得cpu执行权,当线程A去执行同步代码块中的代码时,它再去判断instance的值,由于线程B执行完后已经将这个共享资源instance实例化了,所以instance不再为null,所以,线程A就不会再次实行实例化代码了。
(5):懒汉式双重检查加锁机制+volatile关键字禁止指令重排序
双重检查加锁并不代码百分百一定没有线程安全问题了。因为,这里会涉及到一个指令重排序问题。instance = new Singleton2()其实可以分为下面的步骤:
1.申请一块内存空间;
2.在这块空间里实例化对象;
3.instance的引用指向这块空间地址;
指令重排序存在的问题是:
对于以上步骤,指令重排序很有可能不是按上面123步骤依次执行的。比如,先执行1申请一块内存空间,然后执行3步骤,instance的引用去指向刚刚申请的内存空间地址,那么,当它再去执行2步骤,判断instance时,由于instance已经指向了某一地址,它就不会再为null了,因此,也就不会实例化对象了。这就是所谓的指令重排序安全问题。那么,如何解决这个问题呢?
加上volatile关键字,因为volatile可以禁止指令重排序。volatile参考文章
public class Singleton2 {
private Singleton2() {
}
private static volatile Singleton2 instance;
public static Singleton2 getInstance() {
if (instance == null) {
synchronized (Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}
(6):静态内部类(线程安全,调用效率高,可以延时加载)
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
} //私有的构造方法
/**
* 获取单例对象的方法
*
* @return Singleton
*/
public Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。
INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
(7):枚举类(线程安全,调用效率高,不能延时加载,可以天然防止反射和序列化的调用)
静态内部类的方法虽然很好,但是存在着单例共同的问题:无法防止利用反射来重复构建对象。
//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));
枚举:本身是个类,且是静态类,就是一个单例的,默认被final修改类,不能被继承,枚举中只有ToString没有被final修饰,枚举是自己内部实例化对象,这种其实也是一种饿汉式。优点:代码简单,防止序列化。
public enum EnumSingleton {
INSTANCE;
// 单例的成员变量
private Object data;
// 单例的成员方法
// 调用方式是 EnumSingleton.getInstance().getData()
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
// 通过 getInstance() 获取单例对象
// 调用方式是 EnumSingleton.getInstance()
public static EnumSingleton getInstance(){
return INSTANCE;
}
}