单例设计模式

单例设计模式

饿汉式单例

饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线
程还没出现以前就是实例化了,不可能存在访问安全问题。

优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好,绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题
缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存,有可能占着茅
坑不拉屎。

普通饿汉式单例

public class HungrySingleton {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton(){}

    public static HungrySingleton getInstance(){
        return  hungrySingleton;
    }
}

静态饿汉式单例

public class HungryStaticSingleton {
    private static final HungryStaticSingleton hungrySingleton;
    static {
        hungrySingleton = new HungryStaticSingleton();
    }
    private HungryStaticSingleton(){}
    public static HungryStaticSingleton getInstance(){
        return  hungrySingleton;
    }
}

饿汉式适用于对象较少的情况。

懒汉式单例

懒汉式单例的特点是:被外部类调用的时候内部类才会加载

普通懒汉式

单锁
/懒汉式单例
//在外部需要使用的时候才进行实例化
public class LazySimpleSingleton {
    private LazySimpleSingleton(){}
    //静态块,公共内存区域
    private static LazySimpleSingleton lazy = null;
    public synchronized static LazySimpleSingleton getInstance(){
        if(lazy == null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }
}
双重锁
public class LazyDoubleCheckSingleton {
	private volatile static LazyDoubleCheckSingleton lazy = null;
	private LazyDoubleCheckSingleton(){}
	public static LazyDoubleCheckSingleton getInstance(){
		if(lazy == null){
			synchronized (LazyDoubleCheckSingleton.class){
				if(lazy == null){
				lazy = new LazyDoubleCheckSingleton();
                //1.分配内存给这个对象
                //2.初始化对象
                //3.设置 lazy 指向刚分配的内存地址
				}
			}
		}
		return lazy;
	}
}

双重检查模式,进行了两次判断,第一次是为了避免不要的示例,第二次是为了进行同步,避免线程安全问题。由于

lazy = new LazyDoubleCheckSingleton();对象的创建在jvm可能会进行排序,在多线程访问下存在风险,使用volatile修饰的LazyDoubleCheckSingleton实例变量有效,解决改问题

总结

懒汉式单例存在线程安全的问题,在多线程情况下有一点几率创建不同的对象,

如何来使得懒汉式单例在线程环境下安全呢?

给getInstance()加上synchronized关键字,使这个方法变成线程同步方法。

但是用synchronized加锁,在线程数量比较多情况下,如果CPU 分配压力上升,会导致大批
量线程出现阻塞,从而导致程序运行性能大幅下降。

有没有更好的解决方式?

—使用静态内部类的方式

静态内部类的方式创建单例

//这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题
//完美地屏蔽了这两个缺点
public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton(){}

    //static 是为了使单例的空间共享,保证这个方法不会被重写,重载
    //jvm优先加载静态内部类,只加载一次,利用jvm底层执行逻辑,完美的避免了线程安全的问题
    public static final LazyInnerClassSingleton getInstance(){
        //在返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }

    //内部类默认不加载,使用到了内部类才加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题。内部类一定是要在方
法调用之前初始化,巧妙地避免了线程安全问题

反射破坏单例

但是,上述的构造方法只用private之外,没有做任何处理,如果我们使用反射来调用其构造方法,然后,再调用getInstance()方法,应该就会两个不同的实例。我们看一下测试代码

cpublic class LazyInnerClassSingletonTest {
    public static void main(String[] args) {
        try{
            //想搞进行破坏
            Class<?> clazz = LazyInnerClassSingleton.class;
            //通过反射拿到私有的构造方法
            Constructor c = clazz.getDeclaredConstructor(null);
            //强制访问,跳过安全检查,必须得从了
            c.setAccessible(true);
            //暴力初始化
            Object o1 = c.newInstance();
            //调用了两次构造方法,相当于new了两次,犯了原则性问题
            Object o2 = c.newInstance();

            System.out.println(o1 == o2);//运行结果为false
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行结果是false

很明显,创建了的两个不同的实例。现在我们在构造方法中进行一些限制,一旦重复创建,就直接抛出异常,来看一下优化后的代码

public class LazyInnerClassSingleton {
    //默认使用LazyInnerClassGeneral的时候,会先初始化内部类
    //如果没使用的话,内部类是不加载的
    private LazyInnerClassSingleton(){
        if(LazyHolder.LAZY != null){//防止反射暴力破坏单例
            throw new RuntimeException("不允许创建多个实例");
        }
    }
    //static 是为了使单例的空间共享,保证这个方法不会被重写,重载
    //jvm优先加载静态内部类,只加载一次,利用jvm底层执行逻辑,完美的避免了线程安全的问题
    public static final LazyInnerClassSingleton getInstance(){
        //在返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }
    //内部类默认不加载,使用到了内部类才加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

再次运行的结果:

...
Caused by: java.lang.RuntimeException: 不允许创建多个实例
	at com.gupaoedu.vip.pattern.singleton.lazy.LazyInnerClassSingleton.<init>(LazyInnerClassSingleton.java:17)
	... 5 more

至此,得到的最好用的单例创建方式!!

序列破坏单例

有时候,创建出来的单例对象需要序列化写到磁盘中,下次使用的时候,再从磁盘读取到对象,反序列化为实体对象,反序列化后的对象会重新分配内存,即重新创建,违背了单例模式的初衷,来看一段代码

//序列化:把内存中的状态通过转换成字节码的形式,通过一个IO流,写入到其他地方
//反序列化:将已经持久化的字节码内容,通过IO流的读取,进而将读取的内容转换为Java对象,转换的过程中需要重新创建对象new
public class SeriableSingleton implements Serializable {    
	public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}
    
    public static SeriableSingleton getInstance(){
        return INSTANCE;
    } 
}

编写测试代码:

public class SeriableSingletonTest {
    public static void main(String[] args) {

        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();

            System.out.println(s1 == s2);//运行结果是false

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果是false

可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例的设计初衷。

我们如何保证序列化的情况下也能够实现单例?其
实很简单,只需要增加readResolve()方法即可。来看优化代码:

public class SeriableSingleton implements Serializable {


    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

    private  Object readResolve(){
        return  INSTANCE;
    }

}

为什么增加了readResolve()方法就能解决这个问题?

因为ObjectInputStream类的readObject()方法,调用了该类中的readOrdinaryObject()方法,通过反射找到一个无参的readResolve()方法,并且保存下来,通过readResolve()方法返回实例,解决了单例被破坏的问题。但是实际上实例化了两次,只不过新创建的对象没有被返回而已。

注册式单例

注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。

枚举登记

先来看枚举式单例的写法,来看代码,创建EnumSingleton类:

public enum EnumSingleton {
    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

测试:

public static void main(String[] args) {
        try {
            EnumSingleton instance1 = null;

            EnumSingleton instance2 = EnumSingleton.getInstance();
            instance2.setData(new Object());

            FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");            			 ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance2);
            oos.flush();
            oos.close();
            
            FileInputStream fis = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            instance1 = (EnumSingleton) ois.readObject();
            ois.close();

            System.out.println(instance1.getData());
            System.out.println(instance2.getData());
            System.out.println(instance1.getData() == instance2.getData());

       }catch (Exception e){
            e.printStackTrace();
        }
    }

运行结果为true

为什么?

反编译EnumSingleton.java文件得到

static 
{ 
	INSTANCE = new EnumSingleton("INSTANCE", 0); 
	$VALUES = (new EnumSingleton[] { INSTANCE }); 
}

枚举式单例在静态代码块中就给INSTANCE进行了赋值,是饿汉式单例的实现。

我们再来测试一些反射能否破坏单例

public static void main(String[] args) {
        try {
            Class clazz = EnumSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
            c.setAccessible(true);
            EnumSingleton enumSingleton = (EnumSingleton)c.newInstance("123",666);

        }catch (Exception e){
            e.printStackTrace();
        }
    }

运行报错:

java.lang.IllegalArgumentException: Cannot reflectively create enum objects

不能用反射来创建枚举类型

来看一下jdk的Constructor 的newInstance()方法源码:

  @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

可以看到 , 在newInstance()方法中做了强制性的判断,如果修饰符是Modifier.ENUM枚举类型,直接抛出异常。

容器缓存

public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
    public static Object getInstance(String className){
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            } else {
                return ioc.get(className);
            }
        }
    }
}

来看看Spring中的容器式单例的实现代码:

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory { 
    /** Cache of unfinished FactoryBean instances: FactoryBean name --> BeanWrapper */ 		private final Map<String, BeanWrapper> factoryBeanInstanceCache = new ConcurrentHashMap<>(16);
    ...
} 

容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的。

ThreadLocal单例

线程单例的实现ThreadLocal:

ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。

我们来看代码:

public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
            new ThreadLocal<ThreadLocalSingleton>(){
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };

    private ThreadLocalSingleton(){}

    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}

测试代码:

class ExectorThread implements Runnable{
    @Override
    public void run() {
        ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance();
    }
}
public class ThreadLocalSingletonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
    }
}

main中无论调用多少次,获取到的实例都是同一个,都在两个子线程中分别获取到了不同的实例。

ThreadLocal是如果实现这样的效果的呢?

我们知道上面的单例模式为了达到线程安全的目的,给方法上锁,以时间换空间。ThreadLocal将所有的对象全部放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值