单例模式和工厂模式

单例模式和工厂模式都是编程语言的设计模式,都是为了解决语言的设计缺陷。

一、单例模式

在某些情况下,我们希望某个类只能创建一个实例对象,实现这种类的代码模式,我们称为单例模式,单例模式分为两种实现方式:懒汉模式,饿汉模式;下面先介绍相对较简单的饿汉模式

(1)饿汉模式

先看下面使用了饿汉模式设计的类代码

class HungryMan{
    private static HungryMan hungryMan=new HungryMan();//静态的HungryMan实例
    
    private HungryMan(){}//私有权限修饰的无参构造函数
    
    public static HungryMan getHungryMan(){
        //返回唯一HungryMan实例的方法
        return hungryMan;
    }
}

上面设计的类,只可能有类对象一个实例,最关键的保证只有一个实例条件就是,这个类只有一个私有的无参构造函数,且存在一个在编译阶段就被实例化了的唯一的实例对象。

只有一个私有的无参构造函数保证了这个类对象只能来类内部创建。

静态的类对象和静态的get方法保证了只要这个类被加载,这个对象就会存在,使用类名这个类对象就能被找到;

为什么上面的模式设计叫饿汉模式,这是相对于懒汉模式代码来说的。

(2)懒汉模式

懒汉模式实现代码

class LazyMan{
    private static LazyMan lazyMan=null;//先将lazyman的初始值设置为空
    private LazyMan(){}
    
    public static LazyMan getLazyMan(){
        if(lazyMan==null){
            lazyMan=new LazyMan();
        }
        return lazyMan;
    }
}

懒汉模式和饿汉模式最大的区别,就是懒汉模式并没有一开始就实例化对象,而是在首次调用get时才创建对象,这也体现出了懒汉和饿汉的名字缘由,饿汉模式一开始就“急切”的创建了实例,懒汉模式则是先不创建,只有被叫到了才创建,非常的“懒”;

三、懒汉模式的线程安全问题

(1)避免创建多个实例

在单线程的情况下,饿汉和懒汉模式都能很好的完成只能创建一个实例的任务。

但是

在多线程的角度,饿汉模式由于是加载阶段就创建了对象,所以并不存在线程安全问题,而懒汉模式是在运行阶段创建对象,免不了出现线程安全问题,我们重新审视上面的懒汉模式代码就会发现,如果两个线程都调用了get方法,且此时lazyman还是null,一个线程运行完if判断进入if的代码块后,线程发生调度,运行了另一个线程的get,此时有与lazyman还是null,这个线程的也进入的if判断后的代码块,并且实例出了一个lazyman对象,然后返回这个对象并使用,当cpu重新开始执行第一个线程时,它已经进入了if的代码块内部,这个线程也回创建一个lazyman并使用,此时就创建了两个lazyman对象

 为了避免这种情况,我们需要对if整个代码块进行加锁,

加锁后代码:

class LazyMan{
    private static LazyMan lazyMan=null;//先将lazyman的初始值设置为空
    private LazyMan(){}
    
    public static LazyMan getLazyMan(){
        //使用synchronized对if代码块加锁
        synchronized(this){

            if(lazyMan==null){
                lazyMan=new LazyMan();
            }
        }
        return lazyMan;
    }
}

 加锁后,保证了if这段代码一次只能有一个线程运行,避免了上述问题;

(2)指令重构导致的线程安全问题

上述代码加了锁之后还有可能由于指令重构出现线程安全问题,指令重构是指上面代码在实例化一个对象的时候,编译器优化发生了指令重构

lazyMan=new LazyMan();

正常的上面new操作有三个步骤

1 申请一块内存空间

2 向内存空间赋值

3 将内存空间地址的哈希值赋值给引用

但是编译器优化以后,将123的执行顺序改成了132

此时编译器先将地址赋值给了引用,再将实例的内容放入内存空间,由于new操作不是原子的,所以完全有可能,new被编译器优化完执行顺序后,代码执行完3,就调度其他线程了,其他线程此时调用get方法,先判断lazyman是否为null,由于3步骤已经将内存地址赋值给了引用,所以get直接返回了这个引用,这个线程此时去使用这个引用时,操作的是一片完全是错误内容的空间,出现了线程安全问题

解决这个问题的方法也很简单,就是对这个引用加上volatile关键字修饰,编译器就不会对这个引用的实例化进行优化

class LazyMan{
    //将对象使用volatile修饰
    private static volatile LazyMan lazyMan=null;//先将lazyman的初始值设置为空
    private LazyMan(){}
    
    public static LazyMan getLazyMan(){
        synchronized(this){
            if(lazyMan==null){
                lazyMan=new LazyMan();
            }
        }
        return lazyMan;
    }
}

(3)减小懒汉模式的使用开销

上面的代码已经完全解决了懒汉模式的线程安全问题,我们发现,我们每次get的时候,都要先加锁,才能判断if条件,除了第一次实例化对象的时候,synchronized加锁是有意义的,后面的if条件判断的时候都不需要加锁,为了优化代码,节省cpu的开销,我们在synchronized代码块外面在套一层if条件判断

class LazyMan{
    //将对象使用volatile修饰
    private static volatile LazyMan lazyMan=null;//先将lazyman的初始值设置为空
    private LazyMan(){}
    
    public static LazyMan getLazyMan(){
        if(lazyMan==null){
            //再加一层条件判断
            synchronized(this){
                if(lazyMan==null){
                    lazyMan=new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

这样除了第一次创建对象的时候回进入加锁的代码块,对象创建好之后,每次外层条件判断都会返回false,不会再进行加锁操作

二、工厂模式

工厂模式是为了解决实际情况中采用多种方法实例对象的问题,你可能会觉得,类的构造方法不是已经踢狗了构造方法了吗,根据需要的构造参数写出构造方法不就行了吗?

请看下面例子

为了描述空间中的一个点,我们可以采用笛卡尔坐标系创建这个点,也就是x横轴和有y纵轴的方法来创建点,此时构造这个点的参数就是 double x,double y

但是如果使用极坐标系来创建这个点,极径r和极角a,此时这个点的参数就是double r,double a

在一个类中就不能同时存在这两个构造方法,因为这两个方法的参数类型是一样的,无法分辨到底是调用的哪个构造方法

使用工厂模式就能解决这个问题,为了既能以笛卡尔坐标系创建点,又能以极坐标创建点,我们先定义一个工厂类PointCreater

class PointCreater{
    //以笛卡尔坐标系创建点
    public static Point newPointByXY(double x,double y){}
    //以极坐标系创建点
    public static Point newPointByRA(double r,double a){} 
}

创建的方法都是静态方法,这样直接使用工厂类名就能调用这个方法,不用实例化一个工厂对象。

工厂模式在java库中的应用就有Executors工厂类来创建线程池

ExecutorService pool=Executors.newFixedThreadPool(10);
//创建一个线程数量为10的线程池

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值