java单例模式看这一篇就够了

什么是单例模式

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式属于创建型模式

单例模式的常见写法

一、饿汉式单例

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

优点

没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好

缺点

类加载的时候就初始化,不管用与不用都占着空间,如果项目中有大量单例对象,则可能会浪费大量内存空间

示例

package com.zwx.design.pattern.singleton.hungry;

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

    private HungrySingleton() {
    }
    
    public static HungrySingleton getInstance(){
        return hungrySigleton;
    }
}

或者也可以利用静态代码块的方式实现饿汉式单例

package com.zwx.design.pattern.singleton.hungry;

public class HungryStaticSingleton {
    private static final HungryStaticSingleton hungrySigleton;
    static {
        hungrySigleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton() {
    }

    public static HungryStaticSingleton getInstance(){
        return hungrySigleton;
    }
}

这两种写法都非常的简单,也非常好理解,饿汉式适用在单例对象较少的情况

二、懒汉式单例

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

示例1(普通写法)

package com.zwx.design.pattern.singleton.lazy;

import com.zwx.design.pattern.singleton.hungry.HungrySingleton;

public class LazySingleton {
    private static LazySingleton lazySingleton = null;

    private LazySingleton() {
    }

    public static LazySingleton getInstance(){
        if(null == lazySingleton){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

}

上面的写法是最简单的一种懒汉式单例写法,但是存在线程安全问题,多线程情况下会有一定几率返回多个单例对象,这明显违背了单例对象原则,那么如何优化上面的代码呢?答案就是加上synchronized关键字

示例2(synchronized写法)

package com.zwx.design.pattern.singleton.lazy;

import com.zwx.design.pattern.singleton.hungry.HungrySingleton;

public class LazySingleton {
    private static LazySingleton lazySingleton = null;

    private LazySingleton() {
    }

    public synchronized static LazySingleton getInstance(){
        if(null == lazySingleton){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

示例2的写法仅仅是在getInstance()方法上面加了synchronized关键字,其他地方没有任何变化。用 synchronized 加锁,在线程数量比较多情况下,如果CPU分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。那么,有没有一种更好的方式,既 兼顾线程安全又提升程序性能呢?答案是肯定的。接下来就在介绍一种双重检查锁(double-checked locking)单例写法

扫描二维码关注公众号,回复: 11258314 查看本文章

示例3(DCL写法)

package com.zwx.design.pattern.singleton.lazy;

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazySingleton = null;

    private LazyDoubleCheckSingleton() {
    }

    public static LazyDoubleCheckSingleton getInstance(){
        if(null == lazySingleton){//1
            synchronized (LazyDoubleCheckSingleton.class){//2
                if(null == lazySingleton){//3
                    lazySingleton = new LazyDoubleCheckSingleton();//4
                }
            }
        }
        return lazySingleton;//5
    }
}

这里的写法将同步放在了方法里面的第一个非空判断之后,这样可以确保对象不为空的时候不会被阻塞,但是第二个非空判断的意义是什么呢?我们假设线程A首先获得锁,进入了第3行,还没有释放锁的时候,线程B又进来了,这时候因为线程还没有执行对象初始化,所以判空成立,会进入第2行等待获得锁,这时候当线程A释放锁之后,线程B会进入到第3行,这时候因为第二个判空判断对象不为空了,所以就会直接返回,如果没有第2个判空,这时候就会产生新的对象了,所以需要两次判空!

大家可能注意到这里的变量定义上加了volatile关键字,为什么呢?这是因为DCL在可能会存在失效的情况:
第4行代码:lazySingleton = new LazyDoubleCheckSingleton();
大致存在以下三步:
(1)、分配内存给对象
(2)、初始化对象
(3)、将初始化好的对象和内存地址建立关联(赋值)
而这3步由于CPU指令重排序,不能保证一定按顺序执行,假如线程A正在执行new的操作,第1步和第3步都执行完了,但是第2步还没执行完,这时候线程B进入到方法中的第1行代码,判空不成立,所以直接返回了对象,而这时候对象并没有初始化完全,所以就会报错了,解决这个问题的办法就是使用volatile关键字,禁止指令重排序(jdk1.5之后),保证按顺序执行上面的三个步骤

示例4(内部类写法)

package com.zwx.design.pattern.singleton.lazy;

public class LazyInnerClassSingleton {

    private LazyInnerClassSingleton(){
    }
    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.LAZY;
    }

    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

上面的写法巧妙的利用了内部类的特性,LazyHolder里面的逻辑需要等到外面方法调用时才执行。
这种写法看起来很完美,没有加锁,也保证了懒加载,但是这种单例模式也有问题,那就是可以被反射或者序列化破坏单例,下面我们写一个反射破坏单例的例子

package com.zwx.design.pattern.singleton.lazy;

import java.lang.reflect.Constructor;

public class LazyInnerClassSingletonTest {

    public static void main(String[] args) throws Exception {
        Class<?> clazz = LazyInnerClassSingleton.class;
        Constructor constructor = clazz.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        Object o1 = constructor.newInstance();
        Object o2 = LazyInnerClassSingleton.getInstance();
        System.out.println(o1 == o2);//false
    }
}

上面这个结果输出的结果为false,说明产生了2个对象,当然,要防止反射破坏单例很简单,我们可以把上面例子中的构造方法加一个判断就可以了:

 private LazyInnerClassSingleton(){
        //防止反射攻击
       if(null != LazyHolder.LAZY){
           throw new RuntimeException("不允许构造多个实例");
       }
    }

这样虽然防止了反射破坏单例,但是依然可以被序列化破坏单例,下面就让我们验证一下序列化是如何破坏单例的!
首先对上面的类实现序列化接口

public class LazyInnerClassSingleton implements Serializable

接下来开始对单例对象类进行序列化和反序列化测试:

package com.zwx.design.pattern.singleton.lazy;

import com.zwx.design.pattern.singleton.seriable.SeriableSingleton;

import java.io.*;
import java.lang.reflect.Constructor;

public class LazyInnerClassSingletonTest {

    public static void main(String[] args) throws Exception {
        LazyInnerClassSingleton s1 = null;
        LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();

        FileOutputStream fos = null;

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

            FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (LazyInnerClassSingleton)ois.readObject();
            ois.close();
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);//false
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

这时候输出结果为false,说明产生了2个对象,那么我们应该如何防止序列化破坏单例呢?我们可以对LazyInnerClassSingleton类加上readResolve方法就可以防止序列化破坏单例

package com.zwx.design.pattern.singleton.lazy;

import java.io.Serializable;

public class LazyInnerClassSingleton implements Serializable {

    private LazyInnerClassSingleton(){
        //防止反射攻击
       if(null != LazyHolder.LAZY){
           throw new RuntimeException("不允许构造多个实例");
       }
    }
    //防止序列化破坏单例
    private Object readResolve(){
        return LazyHolder.LAZY;
    }
    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.LAZY;
    }

    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

这是因为JDK源码中会检验一个类中是否存在一个readResolve()方法,如果存在,则会放弃通过序列化产生的对象,而返回原本的对象,也就是说,在校验是否存在readResolve()方法前产生了一个对象,只不过这个对象会在发现类中存在readResolve()方法后丢掉,然后返回原本的单例对象,保证了单例的唯一性,这种写法虽然保证了单例唯一,但是过程中类也是会被实例化两次,假如创建对象的频率增大,就意味着内存分配的开销也随之增大,那么有没有办法从根本上解决问题呢?那么下面就让继续介绍一下注册式单例

三、注册式单例

注册式单例就是将每一个实例都保存到某一个地方,然后使用唯一的标识获取实例

示例1(容器式)

package com.zwx.design.pattern.singleton.register;

public class ContainerSingleton {
    private ContainerSingleton(){
    }

    private static Map<String,Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(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;
            }
            return ioc.get(className);
        }
    }
}

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

示例2(枚举式)

package com.zwx.design.pattern.singleton.register;

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;
    }
   
}

枚举式单例是《Effective java》一书中推荐的写法,这种写法避免了上面的内部类写法中存在的问题(虽然结果唯一,但是过程产生了多个实例对象),是一种效率较高的写法

四、ThreadLocal式单例

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

示例

package com.zwx.design.pattern.singleton.threadlocal;

public class ThreadLocalSingleton {
    private ThreadLocalSingleton() {

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

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

测试

package com.zwx.design.pattern.singleton.threadlocal;

import com.zwx.design.pattern.singleton.ExectorThread;
import com.zwx.design.pattern.singleton.ExectorThread3;

public class ThreadLocalSingletonTest {

    public static void main(String[] args) {
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance();
                System.out.println(Thread.currentThread().getName() + ":" + singleton);
            }
        });
        t1.start();
    }
}

在这里插入图片描述
反复测试可以发现同一个线程获得的对象是唯一的,不同对象则不唯一

总结

单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用,单例模式的写法很多,大家可以根据自己的业务需求选择合适自己的单例方式

猜你喜欢

转载自blog.csdn.net/zwx900102/article/details/90171066