一、模式解析:
单例模式是最简单和最常用的设计模式,面试的时候,不管新毕业的学生还是已经工作多年的筒子,对单例模式基本都能聊上两句。单例模式主要体现在如下方面:
1、类的构造函数私有化,保证外部不能直接使用构造函数创建类的实例
2、提供获取实例的方法,外部可以通过此方法获取已经创建好的实例对象,
3、获取实例的方法必须保证实例唯一性。
4、根据获取实例方法保证唯一性的方式,单例模式又分为以下几种:
二、模式代码
1、懒汉模式
/** * 单例模式-懒汉模式 * 懒汉模式意味着此模式比较懒惰,直到系统发起调用时候才会创建此对象的实例。 * 需要注意的是要保证线程的同步,防止出现多个实例创建的情况 * @author zjl * */public class Singleton1 { //创建元素实例 private static Singleton1 singleton; //构造对象置为私有变量,不能从外部进行创建 private Singleton1(){ } /** * public方法,在外部调用时候,获取此对象的实例 * 注意需要使用同步方法,防止多线程时候产生多实例 * @return 单例对象实例 */ public static synchronized Singleton1 getInstance(){ if(singleton==null){ singleton=new Singleton1(); } return singleton; }}
懒汉模式实现了对象的懒加载,不过缺点是由于getInstance上加了同步关键字,导致此方法只能有一个线程访问,效率会比较低
2、为了解决懒汉模式的速度问题,引入检测,也就是加锁之前先做一次判定,并将实例声明为volatile
public class Singleton { private volatile static Singleton instance; //声明成 volatile private Singleton (){} public static Singleton getSingleton() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
3、痴汉模式
/** * 单例模式的痴汉模式 * 痴汉模式表示此模式很急,一旦对象初始化,立马创建一个实例,以后获得实例的都采用此实例 * @author zjl * */public class Singleton2 { /** * 私有构造函数,保证不能从外部创建实例 */ private Singleton2(){} //痴汉模式重点,对象初始化直接创建实例 private static Singleton2 singleton=new Singleton2(); /** * 直接返回之前创建的实例 * @return 对象实例 */ public static Singleton2 getInstance() { return singleton; }}
痴汉模式不会存在线程同步问题,但是缺点是不是懒加载,对象创建后立马创建实例。
4、静态内部类
/** * 使用静态内部类来创建单例 * @author zjl * */public class Singleton3 { //私有化构造函数,使他不能在外部被创建 private Singleton3(){}; //创建静态内部类,初始化成员变量 private static class SingletonHodler{ private static final Singleton3 INSTANCE=new Singleton3(); } /** * 获取实例方法,执行时候才去创建实例 * @return */ public static final Singleton3 getInstance(){ return SingletonHodler.INSTANCE; }}
静态内部类创建的方法使用不多,也是在面试中很少了解到的,但却是最为推荐的方法,因为他实现了懒加载,线程安全且不需要依赖jdk版本。
三、应用场景
单例模式在框架中应用较多,比如spring的bean管理可以设置是否为单例模式,数据库对象实例化
四、场景代码
由于比较简单,略过。
五、疑问解决
为什么我们在做懒汉模式的双重检测的时候,需要将对象改变为使用volatile进行修饰,此处与java的内存模式有关,并涉及到synchronized和volatile的内存操作
1、java的内存模型主要分为主内存和工作内存,对应计算机的结构可以认为是计算机内存和cpu的高速缓存,对于任何主内存的数据进行操作时候,必须先将数据读取到工作内存形成一个备份,当对工作内存的数据操作完成后,将工作内存数据重新同步到主内存。
对于工作内存和主内存的操作主要为:读取过程-read,load,use,写入过程 assign、store、write,这六个操作均具有原子性,但整个流程不是原子性。因此为了保证可见性和原子性,增加了lock和unlock操作,主要针对主内存区数据的锁定,一旦主内存区数据被lock,表示此线程独占了变量,不可被其他线程更改
2、synchronized的作用主要有两点:程序临界区和内存同步。
程序临界区:是一段仅允许一个线程进行访问的程序,一旦一个线程进入临界区,另外线程进入临界区只能在临界区外进行等待,等临界区线程执行完毕后,其他线程开始争夺资源,胜利者进入临界区,其他线程继续等待。
内存同步:主要是主内存和工作内存的同步。进入临界区时,如果有工作内存的数据未被同步的主内存,则先进行同步,成为其他线程可见状态。工作内存的数据会被丢失,如果要使用,需要重新进行read和load操作。退出临界区时,将工作内存数据写入主内存,保证修改可见。
3、volatile的作用是保证数据可见性,对于任何工作内存的assign操作会立刻store和write到主内存中,同时使其他工作内存放弃原有持有数据。但是volatile不保证操作原子性。
4、具体分析可能出现的问题:
public class Singleton { private static Singleton instance; //声明成 volatile private Date date=new Date; private Singleton (){} public static Singleton getSingleton() { //1 if (instance == null) { //2 synchronized (Singleton.class) { //3 if (instance == null) { //4 instance = new Singleton(); //5 } } //6 } return instance; //7 } public Date getDate(){ return date; } }
如上边的代码,我们将原有懒汉模式稍作修改,增加了date字段,模拟双线程A与B的创建过程
由于new Singleton()不是一个原子操作,程序在进入和出临界区时候,均会同步主内存的内容,除此之外B线程如果在3和6之间调用,就会发生A线程的工作线程内的内容不一定全部写入了主内存,假设此阶段instance写入了主内存,但是date没有写入,B线程将对data内容不可见,因此getDate将返回null。
如果对instance添加了volatile,那么针对instance的修改,随时b线程都是可见的。