单例模式专集细节讲述
本篇博客对单例模式的饿汉式、懒汉式应用在多线程下是否存在安全隐患及其解决方法进行细节讲述。
单例模式
定义:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
类型: 创建类模式
单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访
问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
要点:主要有三个,一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
注意:如何保证对象的唯一性?
- 不允许其他程序用new创建该类对象。
- 在该类中创建一个本类实例。
- 对外提供一个方法让其它程序可以获得该类对象。
单例的步骤:
- 私有化该类构造函数
- 通过new在本类中创建一个本类对象
- 定义一个公有方法
单例模式中的饿汉式(又叫单例设计模式)、懒汉式(又叫延迟加载设计模式)。
1 //饿汉式(开发时常用) 2 class Single{//类一加载,对象就已经存在了 3 private static final Single S= new Single(); 4 private Single();//私有化该类的构造函数,这样其它类就不能够再用new方式创建对象了。 5 public static Single getInstance(){ 6 return S;//返回一个本类对象,因为该方法是静态的,静态的方法访问的只能是静态的S,所以Single S = new Single()必须是静态的。 7 } 8 } 9 10 class SingleDemo{ 11 public static void main(String[] args){ 12 Single ss = Single.getInstance();//因为Single类的构造函数已经private(私有化了),所以此处只能用 类名.函数名 调用getInstance方法。 13 //但是类名调用有前提:必须是静态的方法.所以getInstance是静态方法,要有static修饰, 14 //而该方法中返回一个已经创建好的对象ss,保证只有一个对象. 15 } 16 }
1 //懒汉式(延迟加载形式)(面试常被问到) 2 class Single{//类一加载,只是先创建引用,只有调用了getInstance方法,才会创建对象 3 private static Single S = null; 4 private Single(); 5 public static Single getInstance(){ 6 if(S==null){ 7 S = new Single(); 8 return S; 9 } 10 } 11 } 12 //如果后期被多线程并发访问时,保证不了对象的唯一性,存在安全隐患,如果改后,效率降低 13 class SingleDemo{ 14 public static void main(String[] args){ 15 Single ss = Single.getInstance(); 16 } 17 }
问题1. 分析两者在多线程并发访问情况下存在的安全隐患?
说到这里讨论多线程并发访问下存在的安全隐患,就必须要了解一下,线程安全问题产生的原因是什么?
- 多个线程在操作共享的数据。
- 操作共享数据的线程代码有多行
当一个线程在执行操作共享数据的多行代码过程中,其它线程参与了运算就会导致线程安全问题的产生。
解决思路:就是将多条操作共享数据的代码封装起来,当有线程在执行这些代码的时候,其他线程不可参与运算。必须要当前线程把这些代码都执行完毕后,其他线程不可参与运算。
(1)在Java中,用同步代码块就可以解决这个问题
同步代码块格式: synchronized(对象){
需要被同步的代码块
}
同步的前提:同步中必须有多个线程并使用同一把锁
同步的好处:解决了线程的安全问题
同步的弊端:会相应降低效率,因为线程外的其它线程都会判断同步锁。
(2)同步函数也可以解决问题:将synchronized放到方法中修饰
(3)同步函数与同步代码块的区别?
同步函数使用的锁是固定的this。同步代码块使用的锁是任意的对象(建议使用)
(4)静态的同步函数使用的琐是:该函数所属字节码文件对象,可用当前 类名.class 表示。
对于安全隐患分析:
在饿汉式中(不存在安全隐患),当遇到多线程并发访问时,getInstance加载到线程的run()方法中,返回的对象S是共享数据,因为操作共享数据的代码只有一句,所以不会涉及到线程安全问题。
在懒汉式中(存在安全隐患),共享数据是S,且操作共享数据的代码有多条,当其中一个线程A在执行期间,执行到S=new Single();前(即执行完if(S==null),但还没有执行S=new Single()时 ),突然被切换执行权,导致下一条线程B开始执行,一直到线程B创建完对象S后,线程A重新获得执行权,此时线程A又会再次执行S=new Single();再次创建对象,这样线程A、线程B各自创建了一个对象,导致对象不唯一,也就不能是单例了!
2. 存在的安全隐患如何解决?
(1)首先解决懒汉式中多线程并发访问时存在的对象不唯一的安全隐患!(即在getInstance()方法上用synchronized修饰)
1 //懒汉式(延迟加载形式)(面试常被问到) 2 class Single{//类一加载,只是先创建引用,只有调用了getInstance方法,才会创建对象 3 private static Single S = null; 4 private Single(); 5 public static synchronized Single getInstance(){ 6 if(S==null){ 7 S = new Single(); 8 return S; 9 } 10 } 11 } 12 13 class SingleDemo{ 14 public static void main(String[] args){ 15 Single ss = Single.getInstance(); 16 } 17 }
(2)问题2. 问题又来了,因为使用同步函数,虽然解决了安全隐患,但是同样的降低了效率。那么是什么原因导致多线程并发访问的效率降低了呢?
分析:因为第一次创建完对象后,之后的线程每一次获取对象都要先判断同步锁,因此导致效率降低。
问题3. 懒汉式在线程安全前提下如何提高效率?
解决思路:不用同步函数中的同步,改用同步代码块中的同步,上代码
1 //懒汉式(延迟加载形式)(面试常被问到) 2 class Single{//类一加载,只是先创建引用,只有调用了getInstance方法,才会创建对象 3 private static Single S = null; 4 private Single(); 5 public static Single getInstance(){ 6 if(S==null){//此处多加了一次判断 7 synchronized(Single.class){//使用同步代码块,此处注意使用Single.class锁,因为getInstance()是静态方法,所以只能用Single.class .而this.getClass()锁是用于非静态的方法。 8 if(S==null){ 9 S = new Single(); 10 } 11 } 12 return S; 13 } 14 } 15 } 16 17 class SingleDemo{ 18 public static void main(String[] args){ 19 Single ss = Single.getInstance(); 20 } 21 }
上述代码中,多加了一次判断是为了解决效率问题,这样当线程A创建完对象后,线程B在运行时首先判断S==null,此时因为线程A已经创建完对象,所以线程B就不在执行创建对象操作。使用同步锁是为了解决安全问题。(以上都是以程序中只有线程A、线程B来解释的,再多线程原理也是一样的)
结论:所以说真正程序开发时,多数用的是饿汉式;懒汉式当多线程并发访问时,保证不了对象的唯一性(多线程切换执行权执行程序,导致对象不唯一),虽经过修改保证安全,但是其效率相应降低。