无论是哪种设计模式,自己啃哪本书哪个博客去学,都会考虑最起码的两个问题:
- 我到底该咋用这种设计模式呀,直接把书上的、百度上的、博客上…的程序们抄过来?
- 那我该咋用呢?就算把人家程序抄过来,抄过来放在哪里呀?
- 学了设计模式尤其是单例模式之后,看到XXX类的一个实例XXX,要能反映出来,哦,单例模式可以实现这个需求呀。
- 另外,
单例模式有三个天生的敌人
,也叫天敌,分别是:- 反射,破坏单例
- reflection(Singleton1.class)
- 预防第一个天敌,反射 ,要这样
- 预防第一个天敌,反射 ,要这样
- reflection(Singleton1.class)
- 反序列化破坏单例,因为咱们平台一个对象要在网络中传输或者存储到磁盘中时,就得实现序列化接口,从而让自己能够被序列化
- serializable(Singleton.getInstance());最后一步的readObject()出来的对象已经是一个新对象了,已经不满足单例了【
在反序列化时,如果jdk发现咱们已经重写了这个readResolve()方法时,就会用这个readResolve()方法的返回值去返回这个单例INSTANCE,而不用反序列化那个字节数组的readObject方法中的实例
】
- serializable(Singleton.getInstance());最后一步的readObject()出来的对象已经是一个新对象了,已经不满足单例了【
- Unsafe破坏单例,JDK内置的一个类,这个咱们还没办法预防
- unsafe(Singleton.class);
- 反射,破坏单例
开唠~
咱得先知道官方定义的单例模式(Ensure a class has only one instance, and provide a global point of access to it.)都有啥形式。
咱现在假设哈,咱们一个类中只有一个构造器(构造方法,方法可以重载,咱们假设一种只是为了好说一点而已)。
- 你去,把你的类的构造方法(构造器)的权限修饰符改成private
- 再来个getInstance(…)方法,像下面写法一这样写,好,你的类已经算是用上了单例模式了。
/**写法一:饿汉式单例【饿汉式是相对于懒汉式来说的,懒汉式是第一次调用getInstance这个创建实例的方法时才会去创建,人们口中常说的那个单例,也就是单个实例。饿汉嘛,反过来说就是我不管你调不调用我都提前创建好这个单例,通过加载链接初始化三个操作,只要类一经初始化,这个实例就会被提前创建】
* 这个是强烈推荐使用的解法一(其他解法都有些复杂,加了锁,you know,就会耗时间并且效率就会下降)
*/
pubic class SingletonDemo{
//将构造器设置为private,是咱们单例模式的谈资呀,相当于一切的出发点呀。
private SingletonDemo(){
}
//创建私有静态实例,意味着这个类第一次使用的时候就会进行创建
private static SingletonDemo singletonDemo = new SingletonDemo();
public static SingletonDemo getInstance(){
return singletonDemo;
}
}
- 写法一有几个注意点:
写法一属于饿汉式单例,因为是类加载的时候就创建出实例而不是第一次调getInstance()方法时才会创建实例所以叫饿汉式
【饿汉式是相对于懒汉式来说的,懒汉式是第一次调用getInstance这个创建实例的方法时才会去创建,人们口中常说的那个单例,也就是单个实例。饿汉嘛,反过来说就是我不管你调不调用我都提前创建好这个单例
,通过加载链接初始化三个操作,只要类一经初始化,这个实例就会被提前创建】写法一plus
,枚举饿汉式单例
//枚举方式是一种天然的单例实现,在项目开发中枚举方式是非常推荐使用的。它能够保证序列化和反序列化过程中实例的唯一性,而且不用担心线程安全问题 public enum SingletonTest { SERVICE_A { @Override protected void hello() { System.out.println("hello, service A"); } }, SERVICE_B { @Override protected void hello() { System.out.println("hello, service B"); } }; protected abstract void hello(); }
枚举在类加载的时候会初始化里面的所有的实例,而且 JVM 保证了它们不会再被实例化,所以它天生就是单例的
。- 反序列化没有破坏枚举类单例,序列化中的ObjectInputStream会帮我们返回枚举类实例,而不会把字节数组中的反序列化对象返回去
- 反射也没有破坏枚举类实例
- Unsafe可以破坏,他谁都挡不住
- private static SingletonDemo singletonDemo = new SingletonDemo();加了这个static相当于我们使得
SingletonDemo类的构造器变为静态构造器
了。(这样一来,java在调用静态构造函数(或者说类加载的时候)就会初始化静态变量singletonDemo )- 也就是说singletonDemo实例在此种写法下不是在getInstance()方法被第一次调用时创建的,
而是在SingletonDemo这个类第一次被别人用到时singletonDemo就会创建
。 所以也叫饿汉式
- 也就是说singletonDemo实例在此种写法下不是在getInstance()方法被第一次调用时创建的,
- java能够确保
静态
构造函数只会被调用一次(这个静态构造函数啥时候会被调用,咱们程序员是决定不了的,而是当JVM运行起来时发现第一次使用某个类(型)时自动调用该类(型)的静态构造函数)。 就可以保证singletonDemo 这个静态变量只会被初始化一次(就确保只有一个实例喽)可以说,放到JVM的静态代码块中的东西,JVM会帮咱们自动保证他的单一性,不用咱们操心
。
- Notes:强烈推荐的写法一也是有缺陷的,就是当咱们SingletonDemo类中有一个静态方法时(众所周知咱们调用此静态方法是不需要singletonDemo这个实例的(或者叫对象引用)),但是如果按照咱们这个推荐的解法一来写的话,凡是static的东东都会被一块打包运行(当那个static方法被调用到时这个静态构造器也就会被一块打包执行了,所以singletonDemo也会被无辜的创建出来?,但是我
此时用不到singletonDemo,你把他创建出来不无辜?不浪费内存?
)
所以,亚古兽进化—强烈推荐使用的写法二:按需创建实例【静态内部类方式,属于懒汉式单例】
(其他解法都有些复杂,加了锁,you know,就会耗时间并且效率就会下降)
//写法二
public class SingletonDemoToNeed{
private SingletonDemoToNeed(){
}
public static SingletonDemoToNeed getInstance(){
return Nested.singletonDemoToNeed;//当咱们第一次用到这个嵌套内部类时,会调用这个内部类的静态构造函数创建SingletonDemoToNeed的实例singletonDemoToNeed。
}
//内部定义一个私有内部类Nested,其他人无法使用到这个内部类Nested
class Nested{
static Nested(){
private static SingletonDemoToNeed singletonDemoToNeed = new SingletonDemoToNeed();
}
}
/*
* 内部类也可以这样写
*
* private static class Nested{
* static SingletonDemoToNeed singletonDemoToNeed = new SingletonDemoToNeed();
* }
*/
}
- Notes:此时只有
当咱们第一次尝试通过SingletonDemoToNeed.getInstance()得到SingletonDemoToNeed的实例singletonDemoToNeed时
,会自动调用Nested这个内部类的静态构造函数创建singletonDemoToNeed实例。如果咱们不调用SingletonDemoToNeed.getInstance()时那么也不会调用到内部类从而也就不会创建singletonDemoToNeed实例,所以也叫懒汉式单例- 静态内部类方式实现单例巧妙地利用了 Java 类加载机制,保证其在多线程环境下的线程安全性。
当一个类被加载时,其静态内部类是不会被同时加载的,只有第一次被调用时才会初始化,而且我们不能通过反射的方式获取内部的属性。由此可见,静态内部类方式实现单例更加安全,可以防止被反射入侵
。
- 静态内部类方式实现单例巧妙地利用了 Java 类加载机制,保证其在多线程环境下的线程安全性。
===============================分界线
前面两种比较实用,但是后面几种不太实用,但是算作值得一学的解法,外行看热闹内行看门道嘛。
- 饿汉式:
类加载就会导致该单实例对象被创建
- 懒汉式:类加载不会导致该单实例对象被创建,而是
首次使用该对象【或者说首次调用创建实例的方法】时才会创建
不好的写法三:只在单线程中适用,亚古兽在面对一个打手(熟悉的打手:单线程情况下)时适用,但是多线程条件下就会出现错误(没有用锁)
- 为什么说这是只在单线程中适用的写法,
在多线程中就不安全呢
。这是因为呀,如果有两个线程同时运行到判断singlrton是否为null的if判断语句中,此时也确实这两个线程谁也没有创建SingletonDemo类的实例时,那么两个线程此时都会创建一个实例,那不就打脸了嘛。(一个线程A执行 到new SingletonDemoOnlyUseSingleThread(),但还没有获得对象(对象初始化是需要时间的),第二个线程 B也在执行,执行到(singletonDemoOnlyUseSingleThread == null)判断,那么线程B获得判断条件也是为真,于是继续 运行下去,线程A获得了一个对象,线程B也获得了一个对象,)这就是说单例模式结果你创建了俩。 - 属于懒汉式,因为使用或者调用前没有创建单例,在调用创建单例的方法时才会new创建
//不好的写法三:
pubic class SingletonDemoOnlyUseSingleThread{
//将构造器设置为private,是咱们单例模式的谈资呀,相当于一切的出发点呀。
private SingletonDemoOnlyUseSingleThread(){
}
private static SingletonDemoOnlyUseSingleThread singletonDemoOnlyUseSingleThread = null;
public static SingletonDemoOnlyUseSingleThread getInstance(){
if(singletonDemoOnlyUseSingleThread == null){
return new SingletonDemoOnlyUseSingleThread();
}
}
}
不好的写法四【写法三,多线程中不安全】,拆了东墙补西墙(加锁影响效率哦):
public class SingletonDemoToMultiThread{
private SingletonDemoToMultiThread(){
}
//实现一个可重入锁
private final ReentrantLock lock = new ReentrantLock();
private static SingletonDemoToMultiThread singletonDemoToMultiThread = null;
public static SingletonDemoToMultiThread getInstance(){
lock.lock();//加锁
try{
if(singletonDemoToMultiThread == null){
return new SingletonDemoToMultiThread();
}
} catch(Exception e) {
e.printStack();
} finally {
//解锁
lock.unlock();
}
}
}
//当然啦,加锁除了可以使用Lock模板圈,synchronized也可以用啦
/**
* 构造单例模式,启动服务器端口监听
*
* @throws InterruptedException
* @param void
* @return void
*
* 下面的getInstance()也可以这样写:
* public static synchronized CommunicationLysimeter getInstance(){//这种是用了单个synchronized这个同步方法,也可解决多线程的问题
* if (instance == null) {
* instance = new CommunicationLysimeter();
* }
* return instance;// 返回实例对象
* }
* 以上这种是:通过增加synchronized关键字到getInstance()方法中,使得每个线程在进入这个方法之前必须要等到别的线程先离开该方法,保证了不会有两个线程可以同时进入此方法
*/
- 这样一来为什么在多线程条件下就行了呢。此时是这样的,假设有两个线程同时想进入if判断中创建一个实例,但是由于咱们
加了锁
,就确保了一个时刻只有一个线程能得到同步锁
并执行实例化代码创建出实例
,当第一个线程得到锁给自己加上锁之后并去运行if判断体里面的实例化代码,第二个线程只能等待。当第一个线程进入了if判断体后发现实例还没有创建时他就会自己创建一个实例出来,然后代码执行完第一个线程就会释放同步锁。接着第二个线程获得锁给自己加上同步锁并运行相应的代码,但是这个时候由于第一个线程已经把实例创建出来了所以第二个线程就不会再重复创建线程了。这样就保证了我们在多线程环境下也确保能得到一个实例。- 可以像上面那样用Lock对应的锁
- 也可以用Synchronized
- 可以在getXxx()方法前加
- 可以在getXxx()方法内加
Notes:虽然上面加了锁,但是还能再优化一下。我们只是在实例还没创建之前需要加锁操作,但是如果这个实例在其他地方已经被其他人创建出来了,那咱们肯定就不用再加这个锁了呀(加锁很耗时的哟)。那咱们怎么知道当咱们需要用单例模式去实例化出一个对象之前有没有别人在其他地方已经创建出这个类的对象了呢?
所以,代码优化一下,如下,判断之前再判断一次,如果这个类已经有实例了那咱们就不用无脑加个锁白白耗时了(咱们写的SingletonDoubleJudgeDemo只有在第一次的时候singletonDoubleJudgeDemo为空时,此时才会尝试实例化SingletonDoubleJudgeDemo类,这是第一次之前没有,所以肯定要加锁喽)
不好的写法五~DCL【Double-checked Lock:双重检验锁方式实现单例模式】懒汉式:(不好的写法四加了锁,但是还能再优化一下。我们只是在实例还没创建之前需要加锁操作)。但是如果这个实例在其他地方已经被其他人创建出来了,那咱们肯定就不用再加这个锁了呀(加锁很耗时的哟)。那咱们怎么知道当咱们需要用单例模式去实例化出一个对象之前有没有别人在其他地方已经创建出这个类的对象了呢?
- 所以,代码优化一下,如下,
判断之前再判断一次
,如果这个类已经有实例了那咱们就不用无脑加个锁白白耗时了(咱们写的SingletonDoubleJudgeDemo只有在第一次的时候singletonDoubleJudgeDemo为空时,此时才会尝试实例化SingletonDoubleJudgeDemo类,这是第一次之前没有,所以肯定要加锁喽)【在多线程环境下,为了提高实例初始化的性能,不是每次获取实例时在方法上加锁,而是当实例未创建时才会加锁。属于懒汉式单例
】- 单例模式懒汉式:
- 第一层if(singleton == null) 是为了防止有多个线程同时创建,
double-check(双层if判断)之后,下次线程过来发现第一个if中的实例已经不为空了,不满足,那么就不会进入这两个if中,直接return实例
。 - synchronized 是加锁
防止多个线程同时进入该方法创建对象
- 第二层if(singleton == null) 是防止有多个线程同时等待锁,一个执行完了后面一个又继续执行的情况。
public class SingletonDoubleJudgeDemo{ private SingletonDoubleJudgeDemo(){ } private static SingletonDoubleJudgeDemo singletonDoubleJudgeDemo = new SingletonDoubleJudgeDemo(); private static Lock lock = new ReentrantLock(); public SingletonDoubleJudgeDemo getInstance(){ if(singletonDoubleJudgeDemo == null){ lock.lock(); try{ if(singletonDoubleJudgeDemo == null){ return singletonDoubleJudgeDemo; } } catch(Exception e){ e.printStack(); }finally{ lock.unlock(); } } } } //懒汉式 //当然啦,加锁除了可以使用Lock模板圈,synchronized也可以用啦 public class CommunicationLysimeter { //延迟加载保证多线程安全,加入volatile关键字的写屏障,防止指令重排序,更加完全 private volatile static CommunicationLysimeter instance; private CommunicationLysimeter() { } /** * 双重检查加锁,相对于上面单个同步方法而言,会大大减少getInstance()的时间耗费 * @return * @author HuHongBo */ public static CommunicationLysimeter getInstance() { if (instance == null) {//检查实例,如果CommunicationLysimeter这个实例不存在,就进入同步区块 synchronized (CommunicationLysimeter.class) {//只有第一次才彻底执行这里同步区块的代码。synchronized (this) 也行 if (instance == null) {//进入区块后,在检查一次,如果CommunicationLysimeter实例仍为空,才创建CommunicationLysimeter实例 instance = new CommunicationLysimeter(); } } } return instance;// 返回实例对象 } }
- 使用 volatile 是防止指令重排序,保证对象可见,
防止读到半初始化状态的对象
【加入volatile关键字的写屏障,防止指令重排序【指令之间没有因果关系还好,如果有,顺序被打乱了谁能受得了,在多线程下会出错,单线程下还行
】,更加完全】- 第二层if(singleton == null)中的 instance = new CommunicationLysimeter()其实是分为三步执行:【
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。
指令重排在单线程环境下不会出现问题,但是在多线程环境下指令重排会导致一个线程获得还没有初始化的实例
。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getInstance() 后发现 instance 不为空,因此返回 instance,但此时 instance还未被初始化】- 1.为 instance 分配内存空间
- 2.初始化 instance
- 3.将 instance 指向分配的内存地址
- 第二层if(singleton == null)中的 instance = new CommunicationLysimeter()其实是分为三步执行:【
- 第一层if(singleton == null) 是为了防止有多个线程同时创建,
- 单例模式懒汉式:
接下来就是咋用,在哪用?
//用好的写法一和写法二,就是
......
Xxx xxx = Xxx.getInstance();
xxx就代表咱们由单例模式中得到的实例,现在就可以大张旗鼓的用xxx去调用Xxx类中的方法们咯。
从另一个从面上来讲,这些个设计模式咱们不管是实际写代码还是面试等,不仅会用到具体代码,包括纸面上的优缺点等一些特点咱们也是会用到的,so,捞唠呗。
单例模式的优点们:
- 一个对象需要频繁地 创建、销毁时,而且创建或销毁过程中的性能又无法优化,单例模式的优势就可以用一用了,快去改权限修饰符吧
- 单例模式只生成一个实例就相当于减少了系统的性能开销
- 当一个对象的产生需要 比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一 个单例对象,然后用永久驻留内存的方式,以后一直用这一个就行。
- 在Spring中,每个Bean默 认就是单例的,这样做的优点是Spring容器可以管理这些Bean的生命期,决定什么时候创建 出来,什么时候销毁,销毁的时候要如何处理,等等。如果采用非单例模式(Prototype类 型),则Bean初始化后的管理交由J2EE容器,Spring容器不再跟踪管理Bean的生命周期,就是说你管不了呀
- 单例模式只生成一个实例就相当于减少了系统的性能开销
- 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在 内存中,避免对同一个资源文件的同时写操作。
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单 例类,负责所有数据表的映射处理
结合优点就可以大概知道咱们啥时候用、怎么用、用在哪里了:
- 在一个系统中如果出现多个对象就会出现不良反 应或者说面试人家要求设计这样一个类,要求一个类有且仅有一个对象,就可以上单例模式了
- 要求生成唯一序列号的环境就可以上单例模式了
- 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以 不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的,就可以上单例模式了
- 创建一个对象需要消耗的资源过多时,如要访问IO和数据库等资源,就可以上单例模式了节省一下资源
- 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当 然可以直接声明为static的方式)
单例模式的缺点们:
- 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途 径可以实现
另外呢,
- 如果要求一个类只能(一定数量的对象)比如产生两三个对象呢----
有上限的多例模式
public class SingletonDemoMultiObj{
//定义最多能产生的实例数量
private static int maxNumOfMultiObj = 2; //使用一个ArrayList来容纳,每个对象的私有属性
private static ArrayList<String> nameList=new ArrayList<String>(); //定义一个列表,容纳所有的实例
private static ArrayList<SingletonDemoMultiObj> multiObjList = new ArrayList<SingletonDemoMultiObj>();
private static int countNumOfMultiObj = 0; //产生所有的对象
static{
for(int i = 0; i < maxNumOfMultiObj; i++){
multiObjList.add(new SingletonDemoMultiObj(i+1));
}
}
private SingletonDemoMultiObj(){ }//传入皇帝名称,建立一个皇帝对象
private SingletonDemoMultiObj(String name){ nameList.add(name); }//随机获得一个皇帝对象
public static SingletonDemoMultiObj getInstance(){
Random random = new Random(); //随机拉出一个皇帝,只要是个精神领袖就成
countNumOfMultiObj = random.nextInt(maxNumOfMultiObj);
return multiObjList.get(countNumOfMultiObj);
}
}
这种情况就可以用在例如读取文件,我们可以在系统启动时完成初始化工作,在内存中启动固定数量的reader实例,然后在需要读取文件时就 可以快速响应。
- 单例模式在JDK中的应用:
- Runtime
- System
- Collections:
- Runtime
- 单例模式在Netty中的应用:
巨人的肩膀:
head first设计模式
设计模式之禅
B站各位老师