单例模式的常见写法

什么是单利模式?

单例类在整个程序中只能有一个实例,这个类负责创建自己的对象,并确保只有一个对象被创建

什么情况下会使用单例?

一般情况下,全局使用的类,我们要把它写成单例。
会消耗很多系统资源的类,要使用单例(eg:数据库连接池、工厂类、数据源等,这些创建和销毁都要消耗很多系统资源的对象)
比如在我们使用Spring框架的时候,Spring的Bean它的默认作用域就是单例的

单例模式的实现要点:

1、私有的构造器(当私有化构造器之后我们的对象就不能通过外界的构造方法去进行创建了);
2、持有该类的属性;
3、对外提供获取实例的静态方法;

接下来来看看实现单例模式的各种方式:
一、饿汉式

饿汉式:特点:线程安全、反射不安全、反序列化不安全
它是通过java的ClassLoader类加载机制去实现的,在类加载机制里面它默认是一个线程安全的。所以当类加载 成功的时候我们的静态属性就会被初始化。也就是说,这个对象它的初始化是没有延迟的,执行效率很高。 但是如果当前的对象在创建的时候要消耗很多资源的话,那么这个类加载的时候就会浪费掉很多的内存,并且对于反射和反序列化,它是不安全的。为了解决反序列化这个问题,我们可以在当前的这个类里面写一个readResolve()方法因为静态的变量在序列化过程中是不会被保存的,所以在反序列化的时候, 它会重新生成实例,这样就会破坏了我们的单例,通过readResolve()方法,在它反序列化的时候, 返回的是同一个实例对象,从而就能解决它获取多个实例的问题。

代码体现:
//饿汉式  通过实现Serializable这个接口,让这个对象支持序列化
public class Singleton_1 implements Serializable {
    //当前这个实例属性是静态的,因为静态的属性是全局的,不管这个类当前有多少个对象,永远共享的是同一个属性
    private static Singleton_1 instance = new Singleton_1();

    private Singleton_1() {
    }

    public static Singleton_1 getInstance() {
        return instance;
    }

    private Object readResolve() {
        return instance;
    }
}
测试代码:
public class Singleton_1Test {

    @Test
    public void test1() {
        Singleton_1 instance1 = Singleton_1.getInstance();
        Singleton_1 instance2 = Singleton_1.getInstance();
        //比较一下当前两个类的内存地址是不是一样的,经过测试,结果为true
        System.out.println(instance1 == instance2);
    }

    @Test
    //通过线程的方式来进行测试,经过比对,10次输出的内存地址都是一样的
    public void test2() throws Exception {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Singleton_1.getInstance());
                }
            }).start();
            //正常情况下Junit单元测试不支持多线程测试,
            //想要正常输出的话可以让主线程不要结束,等待子线程全部运行结束后再结束主线程,输出结果就会正常
            Thread.sleep(1);
        }
    }

    @Test
    //通过反射机制进行测试
    public void test3() throws Exception {
        //首先获取当前类的class对象
        Class c = Singleton_1.class;
        //获取构造方法
        Constructor constructor = c.getDeclaredConstructor();
        //因为构造方法是私有的,我们不能直接使用,所以我们需要通过反射去打开它的访问权限
        constructor.setAccessible(true);//这样我门就可以在外界去获取它了
        Singleton_1 instance1 = Singleton_1.getInstance();//标准的单例对象
        Singleton_1 instance2 = (Singleton_1) constructor.newInstance();//通过反射拿到的对象
        //比较一下两个对象的地址,其返回结果为false,说明我们拿到了两个不同的对象
        //这也就是饿汉式的缺点,对反射来讲它是不安全的
        System.out.println(instance1 == instance2);
    }

    @Test
    //通过序列化与反序列化进行测试
    public void test4() {
        //先执行一下序列化
        Singleton_1 instance1 = Singleton_1.getInstance();//先获取一个标准的单例对象
        SerializeUtil.serialize(instance1);//通过工具类序列化当前的对象
        //再通过反序列化拿到两个对象,通过对比两个对象的地址,返回结果为false
        //结论:通过反序列化去获得饿汉式的单例对象,发现这是不安全的,会拿到多个对象
        Singleton_1 s1 = (Singleton_1) SerializeUtil.unzerialize();
        Singleton_1 s2 = (Singleton_1) SerializeUtil.unzerialize();
        System.out.println(s1 == s2);
    }
}
二、登记式

登记式(静态内部类形式):特点->线程安全、可防止反射攻击、反序列化不安全
它是对饿汉式的一种改进,跟饿汉式相比,它可以实现延迟加载。
它和饿汉式的区别在于:初始化对象的时机是不一样的,在静态内部类里面只有当我们调用getInstance()方法的时候,才会去触发当前静态内部类的加载,即实现了延迟加载。
相比于饿汉式,登记式还有一个好处,即可以进行一些改造,然后对反射是可以变成一个安全的。

代码体现:
//登记式
public class Singleton_2 {

    //在登记式中,使用的是静态内部类帮助我们获取当前类的一个属性
    private static class SingletonHolder {
        //在静态内部类中声明一个全局的属性
        private static Singleton_2 instance = new Singleton_2();
    }

    private Singleton_2(){
        System.out.println("singleton2 loaded.....");
        //在创建类的对象的时候我们可以判断一下它静态内部类中的属性是不是null,如果当前静态内部类中的属性不是null的话,
        //那么此时代表它已经被初始化了,就不应该再去调用这个构造方法了,所以此时我们可以抛出一个非法的状态异常
        //这样就可以达到一个禁用反射的效果,从而可以防御反射的攻击,保证安全性
        if (SingletonHolder.instance != null) {
            throw new IllegalSelectorException();
        }
    }

    public static Singleton_2 getInstance() {
        return SingletonHolder.instance;//返回的是静态内部类里面的属性
    }
}
测试代码:
public class Singleton_2Test {

    @Test
    public void test1() {
        Singleton_2 instance1 = Singleton_2.getInstance();
        Singleton_2 instance2 = Singleton_2.getInstance();
        //通过返回结果为true,可以看出两个对象是一致的
        System.out.println(instance1 == instance2);
    }

    @Test
    public void test2() throws Exception {
        Class  c = Singleton_2.class;
        Constructor constructor = c.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton_2 instance1 = Singleton_2.getInstance();
        Singleton_2 instance2 = (Singleton_2) constructor.newInstance();
        System.out.println(instance1 == instance2);
    }
}
三、枚举式

枚举式:是推荐实现单例的最佳方式,首先它是线程安全的,它不是延迟初始化的,是立即初始化的, 并且枚举式自动支持序列化,也能防止反序列化创建一个新的对象,并且枚举可直接防止反射攻击,但是如果需要用到继承的话,此时枚举式就不太恰当了。

代码体现:
//枚举式
public enum Singleton_3 {
    //该属性相当于一个全局的单例对象
    INSTANCE {
        @Override
        protected void doSomething() {
            System.out.println("doSomething run......");
        }
    };

    protected abstract void doSomething();
}
测试代码:
public class Singleton_3Test {

    @Test
    public void test1() {
        Singleton_3 instance1 = Singleton_3.INSTANCE;
        Singleton_3 instance2 = Singleton_3.INSTANCE;
        //其返回结果为true,说明这两个对象是一致的
        System.out.println(instance1 == instance2);
    }

    @Test
    public void test2() throws Exception {
        Class c = Singleton_3.class;
        Constructor constructor = c.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton_3 instance = (Singleton_3) constructor.newInstance();
        //运行后,发现反射一个枚举的时候抛出了一个异常:NoSuchMethodException
        //所以说想要通过反射得到一个枚举对象是不行的,从而就能防止反射的攻击
    }

    @Test
    public void test3() {
        Singleton_3 singleton_3 = Singleton_3.INSTANCE;
        SerializeUtil.serialize(singleton_3);
        Singleton_3 instance1 = (Singleton_3) SerializeUtil.unzerialize();
        Singleton_3 instance2 = (Singleton_3) SerializeUtil.unzerialize();
        System.out.println(instance1 == instance2);
        //通过比较,这两个对象的地址是一致的
    }
}
四、懒汉式

懒汉式:单例对象的延迟初始化,最明显的区别就是和饿汉式相比,饿汉式是在类加载的时候就去初始化对象,而懒汉式是在我们第一次使用到这个对象的时候才去加载。

代码体现:
//懒汉式
public class Singleton_4 {

    private static volatile Singleton_4 instance = null;//当前的对象不初始化

    private Singleton_4() {
    }

    //一个对外暴露此属性的公有方法
    /*存在的问题:这种方式不是线程安全的,当线程A和线程B都来访问getInstance()方法的时候,如果A和B同时进行了if判断的话,
    因为线程是具有随机性的,如果两个线程比如说A先判断出当前的对象是null,接着在没有初始化对象的时候,它的时间片用完了,
    此时B也进行了一个判断,但因为A还没有把对象初始化,所以B也会进入到当前的if判断里,对当前的对象进行一个初始化,
    从而导致有多个对象出现,所以从严格的意义上来讲,这一种简单的懒汉式并不是一个标注的单例模式。*/
    /*public static Singleton_4 getInstance() {
        if (instance == null) {
            instance = new Singleton_4();
        }
        return instance;
    }*/
    //解决方式1:同步方法,在方法上添加synchronized关键字
    /*public static synchronized Singleton_4 getInstance() {
        if (instance == null) {
            instance = new Singleton_4();
        }
        return instance;
    }*/
    //解决方式2:同步代码块
    /*public static Singleton_4 getInstance() {
        synchronized (Singleton_4.class) {
            if (instance == null) {
                instance = new Singleton_4();
            }
        }
        return instance;
    }*/
    /*这两种方式都能实现线程同步,但是效率比较低,因为我们的对象已经初始过了,后续每一次去拿当前的单例对象的话,所有的线程
    都是处于一个同步的状态,也就是说必须得等其他线程跳出同步代码块之后才能去执行,那么此时它的执行效率是很低的,为了解决
    这个问题,于是就出现了双检锁这种模式,它不需要每一次在拿单例对象的时候都去进行同步,双检锁的形式主要是在同步代码块的
    基础上进行的改变,如下所示
    解决方式4:双检锁,双检锁的形式就是在同步代码块外面再加一步判断
    这样的判断方式好处在于:比如说当前有AB两个线程,都进入到了第一层的if判断,那么当他们都发现了instance是null的时候
    都会去执行初始化的方法,那么此时当前两个线程必须是处于一个同步的状态,假设A线程先去执行,那么A把我们的对象进行了一个初始化
    当A跳出了同步块之后,B接着去执行,B要进行第二次判断,因为A已经把instance进行了一个初始化,所以B就不再会去进行初始化了,
    就会直接返回了,后续其他的线程再来访问的时候,它们会在第一重检索的时候就会发现instance不为null了,此时就不会去进行同步了
    也就是说,双重检索的形式,它会减少同步的次数,只是在单例一开始初始化的时候去进行同步,后面的线程来访问的时候是不需要进行
    同步的,那么就会比我们前面的几种方式效率要高很多*/
    public static Singleton_4 getInstance() {
        if (instance == null) {
            synchronized (Singleton_4.class) {
                if (instance == null) {
                    instance = new Singleton_4();
                }
            }
        }
        return instance;
    }

    /*双检锁的形式是否能100%保证是没有问题的呢?
    答案肯定是无法保证的,原因如下:
    instance = new Singleton_4()会执行以下操作:①分配对象内存空间;②初始化对象;③instance指向①中分配的空间
    在某些编译器上可能会出现指令重排:①分配对象内存空间;②instance指向①中分配的空间(但此时对象还没有初始化);③初始化对象;
    在程序的运行中,指令重排不会影响最终的结果,但是在多线程中会存在极小的概率下出现一些问题
    解决办法: 在当前的对象前面加volatile关键字,它可以保证我们对于instance这个变量它的所有操作不会进行指令重排,就避免了
    我们上面说的这种问题,所以大家如果写的是双检锁形式的懒汉式的话,要记得把volatile这个关键字加进去*/

}
测试代码:
public class Singleton_4Test {

    @Test
    public void test1() {
        Singleton_4 instance1 = Singleton_4.getInstance();
        Singleton_4 instance2 = Singleton_4.getInstance();
        System.out.println(instance1 == instance2);
        //通过对比,在单线程下,通过饿汉式得到的对象是一致的
    }

    @Test
    public void test2() throws Exception {
        for (int i = 0; i < 20; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Singleton_4.getInstance());
                }
            }).start();
            Thread.sleep(1);
        }
    }
}
五、ThreadLocal

ThreadLocal:它和双检锁模式不同,ThreadLocal本身不加锁,它会为每一个线程提供一个变量的独立副本,也就是说它是一个空间换时间的操作,它可以保证在每一个线程中类的对象都是单例的,但是在不同的线程之间它是不能保证单例的。

代码体现:
//ThreadLocal
public class Singleton_5 {
    private static Singleton_5 instance = null;

    private Singleton_5() {
    }

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

    //提供一个对外访问此变量的方法
    public static Singleton_5 getInstance() {
        return THREAD_LOCAL_SINGLETON.get();
    }
}
测试代码:
public class Singleton_5Test {

    //先在单线程的模式下进行测试
    @Test
    public void test1() {
        Singleton_5 instance1 = Singleton_5.getInstance();
        Singleton_5 instance2 = Singleton_5.getInstance();
        System.out.println(instance1 == instance2);
        //其返回结果为true,说明这两个对象是一致的
    }

    //在多线程下再次进行测试
    @Test
    public void test2() throws Exception {
        for (int i = 0; i < 20; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Singleton_5 instance1 = Singleton_5.getInstance();
                    Singleton_5 instance2 = Singleton_5.getInstance();
                    System.out.println(Thread.currentThread().getName() + "------"
                            + (instance1 == instance2) + "-----" + instance1);
                }
            }).start();
            Thread.sleep(1);
        }
        /*通过观察打印结果可知:在每一个线程里面我们获取到的对象都是相等的,也就是说在独立的一个线程里不管获取几个对象,它都是单例的
        但是在不同的线程里面我们看到拿到的对象是不一样的,也就是说用ThreadLocal这种形式的话你不能保证其它的线程之间它是单例的
        而只能保证在一个线程里面它是单例的。*/
    }
}
六、CAS(比较交换技术)

CAS(比较交换技术):它的设计思想是一种无锁的乐观策略,是线程安全的。它假设当前线程在访问资源的时候不会出现冲突,如果出现冲突的话就不是当前的操作,直到没有冲突为止。

代码体现:
//CAS(比较交换技术)
public class Singleton_6 {
    /*这个对当前对象的引用用的是java里面的原子类AtomicReference,它是一个常量,它在引用的时候不是直接引用的当前类的对象
    而是对它进行了一个原子类的封装*/
    private static final AtomicReference<Singleton_6> INSTANCE = new AtomicReference<>();

    private Singleton_6() {
        System.out.println("singleton_6 run.....");
    }

    //在对外获取这个方法的时候要对这个被原子类修饰过的引用进行一个操作,在这个方法里面要先拿到这个原子类引用它所指向的对象
    public static final Singleton_6 getInstance() {
        while (true) {
            Singleton_6 currentInstance = INSTANCE.get();//通过get方法拿到它所指向的那个真正对象
            //如果当前的对象不是null的话,我们就把它给返回
            if (currentInstance != null) {
                return currentInstance;
            }
            /*如果是null的话,说明这个对象是没有被初始化的,从这就可以看到它是一个延迟加载的一个过程,那此时这个current要替换掉
            instance里面的对象,但是为了在多线程之间保证线程安全,我们需要调用原子类的一个compareAndSet方法进行一个判断
            如果说当前instance对象指向的是一个null,我们在用current对它进行一个替换,接着把current返回*/
            currentInstance = new Singleton_6();
            if (INSTANCE.compareAndSet(null,currentInstance)) {
                return currentInstance;
            }
        }
    }
}
/*
总结:如果有两个线程对getInstance()方法进行访问的话,由于没有加锁,所以这两个线程有可能拿到的都是null,然后这两个线程都
会new一个对象出来,但是对instance的操作的话是一个原子性的,因为它是一个原子类的操作,所以只会有一个线程对其进行替换的操作
比如说A线程先进行替换,此时它会把instance指向的null的对象替换成A线程new出来的current对象,B线程再来的时候,因为A已经把
它指向的实际对象替换掉了,所以B线程的替换是无法进行的,此时B没有获得对象会进入到下一次循环,在下一次循环的时候,它就不会拿到
空的对象了,因为A已经替换过了,所以就会返回一个单例的对象
存在一个问题,第一次进来的时候两个线程都初始化了一个对象,所以用这种方式的时候它会产生一定的垃圾对象,就是你可以保证你最后
拿到的instance它是一个,但是你不能保证系统里面只初始化了一个对象,并且这种形式由于存在一个死循环,所以在一开始竞争的时候
比如说竞争到了instance的使用权的时候这个线程卡顿了的话,有可能会对CPU造成一定的负荷。
*/
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值