
本文共 4614 字,大约阅读时间需要 15 分钟。
目录
1.单例模式是什么
顾名思义,单例模式就是一个类中,他只能够创建一个对象,不能创建多个对象。当多个程序共享一个类时,就可以用单例模式。
2.饿汉模式的单例模式
思路:为了不让外部空间随意创建对象,所以首先要把构造函数私有化。接着必须创建一个本类对象,使得类在加载时就创建了这个唯一的对象。
如下代码中的:private static SingleClass s = new SingleClass();用private修饰是因为这个对象要用自己写的方法返回,用static修饰是因为这个 s 变量要被
GetSingle()函数调用,由于这个函数也是static修饰的,所以只能访问同样是用static修饰的变量。而GetSingle()用static修饰的原因在于要让外部空间在没有创建SingleClass对象的情况下可以调用 SingleClass 类中的GetSingle()函数,从而得到本类对象。
public class Main { public static void main(String[] args) { SingleClass s1 = SingleClass.GetSingle(); SingleClass s2 = SingleClass.GetSingle(); s1.num = 10; //先把s1的num改成10,后再把s2的num改成20,再打印s2的num,看s1和s2是否指向同一个对象。 s2.num = 20; System.out.println(s1.num); //打印结果:20 ,证明s1和s2指向同一个对象 }}/*饿汉式单例写法*/class SingleClass{ /*创建一个本类对象*/ private static SingleClass s = new SingleClass(); /*私有化构造函数,使外部不能调用构造函数*/ private SingleClass(){} /*给出接口让外部调用*/ public static SingleClass GetSingle() { return s; } public int num;}
3.懒汉模式的单例模式
思路:先不创建本类对象,只定义一个对象引用。同样要把构造函数私有化,使得外部空间不能随意的创建对象。GetSingle()函数中,先判断本类引用 s是否为空,要是为空就创建一个对象,并返回,要是不为空就直接返回对象。
public class Main { public static void main(String[] args) { SingleClass s1 = SingleClass.GetSingle(); SingleClass s2 = SingleClass.GetSingle(); s1.num = 10; //先把s1的num改成10,后再把s2的num改成20,再打印s2的num,看s1和s2是否指向同一个对象。 s2.num = 20; System.out.println(s1.num); //打印结果:20 ,证明s1和s2指向同一个对象 }}/*懒汉式单例的写法*/class SingleClass{ /*创建一个本类对象引用*/ private static SingleClass s; /*私有化构造函数,使外部不能调用构造函数*/ private SingleClass(){} /*给出接口让外部调用 若s 为null时,才创建对象,如果不为null,则直接返回已有的对象s*/ public static SingleClass GetSingle() { if(s ==null) s = new SingleClass(); return s; } public int num;}
4.两种模式的区别
一般来说,懒汉式用得比较多。他们的区别是,第一种在类加载的时候,就已经创建了对象了。而第二种是外部空间第一次调用 GetSingle()函数时,才会创建唯一的单例对象。
5.多线程并发访问单例模式的问题
用多线程并发访问的单例模式最好是用:饿汉式。
//饿汉式代码class SingleClass{ private static SingleClass s = new SingleClss(); private SingleClass(){}; public static SingleClass getClass() { return s; }}
原因是:饿汉式的单例 是在程序一运行时就创建的。不需要修改就可以保证现场安全问题。而懒汉式不修改的话,有可能造成创建了多个实例的错误。
懒汉式(又称延迟加载)
//懒汉式代码class SingleClass{ private static SingleClass s = null; private SingleClass(){}; public static getClass() { if(s == null) s = new SingleClass(); else return s; }}
上面代码:若有两个线程T1 和 T2. 若T1运行到 if(s == NULL)后,但还没创建对象的时候就被切换了,即CPU转去执行T2了,那么这是的 s 还是等于 NULL,所以T2会创建 s 对象,此时,T1醒了,又继续执行,T1又执行创建对象的代码,就会创建多个对象了。
6. DCL单例模式(双端检测锁单例模式)
6.1 双锁(两个 if == null 判断)
当然,上面的问题可以通过加synchronized来解决,如下:
//改进的懒汉式代码class SingleClass{ private static SingleClass s = null; private SingleClass(){}; public static getClass() { if(s == null) { synchronized(SingleClass.class) { if(s ==null) s = new SingleClass(); } } return s; }}
1.上述的代码首先说同步的问题,加了同步代码块(synchronized())就不会出现两个线程T1,T2同时创建单例对象的问题。
2.为什么要用双重判断(s == NULL)?这是为了解决效率问题,因为如果不加第一层判断,有多个线程的话,就会反复判断同步代码块中的锁是否可用,判断锁是否可用会消耗一定例的资源。给个流程:如在一开始的时候,有两个线程T1,T2, T1进入同步代码块后被切换了,停在了 if(s == NULL) 和 s = new SingleClass()之间,然后T2还是可以通过第一层的 s == NULL 判断,会判断一次同步代码块的锁可不可用,然后会停在同步代码块外(因为T1进去了),当T1继续执行,创建完单例对象后,运行结束。此时T2进去同步代码块,判断第二重s==NULL,发现 s 不等于 NULL了,也结束了。然后其他线程进来,就永远通不过第一重s == NULL判断了,因为s已经不为NULL。所以就不会存在反复判断同步代码块中的锁是否可用的问题。
6.2 volatile的使用
上面的代码看似已经完美了,但实际上是这样吗?并非如此
首先我们要明白,对象的创建过程其实是分3步的:
- 给对象开辟一段内存空间
- 初始化对象
- 设置对象的引用指向开辟的内存空间
由于第二步和第三步之间并无 数据依赖关系,因此有可能会出现指令重排,指令重排的本质是为了提高代码的执行效率,指令重排在单线程下是可以保证重排前和重排后指令的执行结果是一样的,但是在多线程状态下,就不行了。例如上面的3个过程,会被重排为:
- 给对象开辟一段内存空间
- 设置对象的引用指向开辟的内存空间
- 初始化对象
假设 线程A创建对象,运行到 重拍后的第二步时,这时候引用指向的是一个半初始状态的对象,自然这时候 SingleClass s 就不是 null 了。这时候如果线程B进来,拿到了半初始化的s对象,那么就会出现不可预估的情况,因此为了防止指令重排序,要给 SingleClass s 加上一个 volatile关键字。
完整代码:
class SingleClass{ private volatile static SingleClass s = null; private SingleClass(){} public static SingleClass getInstance(){ if(s==null){ synchronized (SingleClass.class){ if(s==null){ s = new SingleClass(); } } } return s; }}public class Test5 { public static void main(String[] args) { new Thread(()->{ SingleClass s = SingleClass.getInstance(); System.out.println(s); }).start(); new Thread(()->{ SingleClass s = SingleClass.getInstance(); System.out.println(s); }).start(); }}
运行结果:
发表评论
最新留言
关于作者
