单例模式和工厂模式都是编程语言的设计模式,都是为了解决语言的设计缺陷。
一、单例模式
在某些情况下,我们希望某个类只能创建一个实例对象,实现这种类的代码模式,我们称为单例模式,单例模式分为两种实现方式:懒汉模式,饿汉模式;下面先介绍相对较简单的饿汉模式
(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的线程池