单例模式大致可以分为两类,懒汉模式和饿汉模式,但是不必在意是懒还是饿,还是要明白他们的原理和区别。(什么是懒汉,就是类加载了之后,并没有实例化单例,而是延后到第一次使用的时候;什么事恶汉,就是类加载了,就实例化单例了。)
本文所举例均为线程安全的单例模式。
直接看代码和注释:
1、双重加锁的单例模式(懒汉模式)
这种模式也是作者之前最常使用的模式,因为代码好理解,并且是lazyload模式,不会产生垃圾对象。基本面试的时候也会首先回答这种模式,这种模式可以延伸出好多知识点,包括volatile的作用,synchronized锁。
/**
* 双层加锁的单例模式
* */
public class SingletonTest3 implements Test{
/**
* instance使用volatile修饰,防止线程之间出现脏数据,导致线程A已经执行完instance = new SingletonTest3() ;
* 后线程B拿到锁,但是判断instance还是null,而导致instance初始化两次。
*/
private volatile static SingletonTest3 instance = null ;
private SingletonTest3(){
Log.d("gggl" , "SingleTest3 初始化了") ;
}
public static SingletonTest3 getInstance(){
//首先进行instance的null判断,为了在没有线程竞争的情况下提高速度不必进行锁的竞争。
if (instance == null) {
//加锁防止线程竞争
synchronized (SingletonTest3.class) {
//二次判断,这里是必须的,因为第一个null判断不是线程安全的。
if (instance == null) {
instance = new SingletonTest3() ;
}
}
}
return instance ;
}
@Override
public void test() {
Log.d("gggl" , "SingletonTest3 初始化了") ;
}
}
2、static final 修饰成员变量instance(恶汉模式)
这种模式下,当我们的class被加载到内存中后,就会调用构造函数,如果你的单例项目启动后就一直需要的话,采用这种方式没有任何问题,简单方便。但是如果你的单例是项目启动后需要某些动作触发才需要的话,不建议采用这种方式,浪费一丢丢内存。其实也有点吹毛求疵了哈,不过这是面试,认真点。-^-
import com.example.interview.Test;
public class SingletonTest1 implements Test {
private SingletonTest1(){
Log.d("gggl" , "SingletonTest1 初始化了") ;
}
private static final SingletonTest1 instance = new SingletonTest1() ;
public static SingletonTest1 getInstance() {
return instance ;
}
public void test(){
Log.d("gggl" , "SingletonTest1 say hello!!!!") ;
}
}
3、静态内部类模式(懒汉模式)
静态内部类模式。这种模式略显负责,作者也是不喜欢使用,因为我基本都是采用第一种的双重null判断模式。但是没办法出去面试,都是看看。
public class SingletonTest1 implements Test {
private SingletonTest1(){
Log.d("gggl" , "SingletonTest1 初始化了") ;
}
public static SingletonTest1 getInstance() {
return SingletonHander.singleton ;
}
public void test(){
Log.d("gggl" , "SingletonTest1 say hello!!!!") ;
}
private static class SingletonHander{
private static final SingletonTest1 singleton = new SingletonTest1 ();
}
}
测试代码:
try {
Class.forName("com.example.interview.designpattern.SingletonTest1" , true , getClassLoader()) ;
Log.d("gggl" , "SingletonTest1加载完毕") ;
Class.forName("com.example.interview.designpattern.SingletonTest2" , true , getClassLoader()) ;
Log.d("gggl" , "SingletonTest2加载完毕") ;
Class.forName("com.example.interview.designpattern.SingletonTest3" , true , getClassLoader()) ;
Log.d("gggl" , "SingletonTest3加载完毕") ;
SingletonTest1.getInstance().test();
SingletonTest2.getInstance().test();
SingletonTest3.getInstance().test();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
这里首先对SingletonTest的几个类进行了加载(注:Class.forName进行加载类的时候顺便执行了初始化的一些任务,具体执行到哪些步骤作者没有具体看jdk代码,但是static成员变量和代码块都是在这个阶段执行的)。然后分别调用SingletonTest.getInstance().test()方法。下面我们来看执行结果。
2022-09-16 10:16:01.081 11710-11710/com.example.interview D/gggl: SingletonTest1 初始化了
2022-09-16 10:16:01.081 11710-11710/com.example.interview D/gggl: SingletonTest1加载完毕
2022-09-16 10:16:01.082 11710-11710/com.example.interview D/gggl: SingletonTest2加载完毕
2022-09-16 10:16:01.082 11710-11710/com.example.interview D/gggl: SingletonTest3加载完毕
2022-09-16 10:16:01.082 11710-11710/com.example.interview D/gggl: SingletonTest1 say hello!!!!
2022-09-16 10:16:01.082 11710-11710/com.example.interview D/gggl: SingletonTest2 初始化了
2022-09-16 10:16:01.082 11710-11710/com.example.interview D/gggl: SingletonTest2 say hello!!!!
2022-09-16 10:16:01.082 11710-11710/com.example.interview D/gggl: SingleTest3 初始化了
2022-09-16 10:16:01.082 11710-11710/com.example.interview D/gggl: SingletonTest3 初始化了
大家看到了SingletonTest1也就是饿汉模式的第一种(private static final SingletonInstance1 = new SingletonInstance1())在类加载的时候就执行了构造函数,这就是区别,面试知识点。
总结:
其实以上三种模式大家在开发的时候只需固定化使用1或者3即可,第二种因为不是lazyload模式所以尽量不要使用,好的编码习惯也是需要点滴养成的。而针对面试知道上面三种模式足够了,至于那种线程不安全的最简单的模式就不要往出写了,没任何意义。