[工作中的设计模式]单例模式singleton

一、模式解析:

单例模式是最简单和最常用的设计模式,面试的时候,不管新毕业的学生还是已经工作多年的筒子,对单例模式基本都能聊上两句。单例模式主要体现在如下方面:

  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线程都是可见的。

posted @ 2016-01-22 01:29  孤子  阅读(382)  评论(0编辑  收藏  举报