单例设计模式
1.1 单例设计模式概述
1.1.1 Gof设计模式简介
- Gof:1995 《设计模式》 Gang of Fore
- 23种基于面向对象的设计模式
1.1.2 为什么需要单例模式
实际项目开发中,经常遇到一些对象,这样的对象在全局中仅存在有一个就可以了,如果对象出现多个的话,就会造成程序整体执行流程失败,或者内存管理上的问题。
1.1.3 单例模式概述
单例模式是确保某一个类只有一个实例,而且这个实例一般都是自行实例化,通过一个全局访问点,向整个系统提供这个实例
1.2 单例模式创建
1.2.1 懒汉式创建
1.2.1.1 创建方法
对象在成员变量上,只声明,不创建。
当我们在使用到该变量的时候,才会创建出来该对象
1.2.1.2 案例代码:
1.创建项目结构
2. 代码
-
2.1 最开始的代码
FootBall.java类:
package com.renbiao.pojo; public class FootBall { }
Test.java测试类
package com.renbiao.test; import com.renbiao.pojo.FootBall; public class Test { public static void main(String[] args) { FootBall fb1 = new FootBall(); FootBall fb2 = new FootBall(); FootBall fb3 = new FootBall(); System.out.println(fb1); System.out.println(fb2); System.out.println(fb3); } }
运行结果:
com.renbiao.pojo.FootBall@71dac704
com.renbiao.pojo.FootBall@123772c4
com.renbiao.pojo.FootBall@2d363fb3分析:
可以看出输出的三个地址都不相同,即是三个对象。
三个对象分别来自三块堆,一块堆代表一个对象。 -
2.2 第一次优化代码
不能让程序随便的去new对象,需要将构造方法私有化。
这样其他程序就不能随便new对象了,想要获取该对象,就要由该类提供方法FootBall.java类:
package com.renbiao.pojo; public class FootBall { // 构造方法私有化 private FootBall(){ } // 想要得到该对象,必须由该类提供方法 // 但是该方法不是一个static静态方法,外部暂时不能调用 public FootBall getFootBall(){ FootBall fb = new FootBall(); return fb; } }
Test.java测试类
可以看到下面:FootBall中没有提供可利用的方法
-
2.3 静态方法创建和调用
getFootBall()方法必须是一个static静态方法。只有静态方法,才能够使用类去调用
FootBall.java类:
package com.renbiao.pojo; public class FootBall { // 构造方法私有化 private FootBall(){ } // 想要得到该对象,必须由该类提供方法 // 该方法必须是一个static静态方法。只有静态方法,才能够使用类去调用 public static FootBall getFootBall(){ FootBall fb = new FootBall(); return fb; } }
Test.java测试类
package com.renbiao.test; import com.renbiao.pojo.FootBall; public class Test { public static void main(String[] args) { FootBall fb1 = FootBall.getFootBall(); FootBall fb2 = FootBall.getFootBall(); FootBall fb3 = FootBall.getFootBall(); System.out.println(fb1); System.out.println(fb2); System.out.println(fb3); } }
运行结果:
com.renbiao.pojo.FootBall@71dac704
com.renbiao.pojo.FootBall@123772c4
com.renbiao.pojo.FootBall@2d363fb3分析:
可以看出输出的三个地址都不相同,即是三个对象。
虽然我们在测试时,没有重新创建对象,
但是在调用的方法中,每次调用都会new一个对象,
本质上原理没变,只是和原来new对象的位置发生了改变,结果没变。 -
2.4 改变为单例模式
声明一个成员变量,但是不new,用到的时候判断是否要再创建。
FootBall.java类:
package com.renbiao.pojo; public class FootBall { // 将该类的对象,声明成为成员变量 // 下方的getFootBall()方法要使用到该成员变量, // 如果一个静态方法要直接的使用成员变量,那么成员变量也必须是静态的 private static FootBall fb; // 构造方法私有化 private FootBall(){ } // 想要得到该对象,必须由该类提供方法 // 该方法必须是一个static静态方法。只有静态方法,才能够使用类去调用 public static FootBall getFootBall(){ //如果成员变量是null,说明成员变量还没有被实例化过,需要创建给成员变量实例 //该实例作为单例对象,为全局使用 //第一次访问时,创建对象,以后再访问时,不再创建,取得的是第一次创建的对象 if(fb == null){ fb = new FootBall(); } return fb; } }
Test.java测试类
package com.renbiao.test; import com.renbiao.pojo.FootBall; public class Test { public static void main(String[] args) { FootBall fb1 = FootBall.getFootBall(); FootBall fb2 = FootBall.getFootBall(); FootBall fb3 = FootBall.getFootBall(); System.out.println(fb1); System.out.println(fb2); System.out.println(fb3); } }
运行结果:
com.renbiao.pojo.FootBall@71dac704
com.renbiao.pojo.FootBall@71dac704
com.renbiao.pojo.FootBall@71dac704分析:
对象在成员变量上,只声明,不创建。
当我们在使用到该变量的时候,才会创建出来该对象。
第一次访问创建出来单例对象,
当第二次到第N次访问时,就不创建对象了,而是取得第一次访问时,创建的对象
1.2.2 饿汉式创建
1.2.2.1 创建方法
当该类加载完毕后,单例对象就已经创建出来了,不管你第几次访问
1.2.2.2 案例代码
1.创建项目结构
2. 代码
-
2.1 饿汉式创建
BasketBall.java
package com.renbiao.pojo; public class BasketBall { //声明单例对象 private static BasketBall bb = new BasketBall(); // 构造方法私有化 private BasketBall() { } // 通过方法唯一的入口点,来获取单例对象 public static BasketBall getBasketBall() { return bb; } }
Test.java
package com.renbiao.test; import com.renbiao.pojo.BasketBall; public class Test { public static void main(String[] args) { BasketBall bb1 = BasketBall.getBasketBall(); BasketBall bb2 = BasketBall.getBasketBall(); BasketBall bb3 = BasketBall.getBasketBall(); System.out.println(bb1); System.out.println(bb2); System.out.println(bb3); } }
运行结果:
com.renbiao.pojo.BasketBall@71dac704
com.renbiao.pojo.BasketBall@71dac704
com.renbiao.pojo.BasketBall@71dac704分析:
不论是第一次访问,还是第几次访问,执行方法是一模一样的,
该入口已经帮我们准备好一个单例对象,我们通过该入口直接取得对象就可以了。
1.2.3 双重检查式创建
1.2.3.1 创建方法
懒汉式创建的基础上,使用synchronized同步块,并进行两次判断
1.2.3.2 案例代码
1.创建项目结构
2. 代码
-
2.1 创建懒汉式单例模式
VolleyBall.java
package com.renbiao.pojo; public class VolleyBall { // 创建单例对象 private static VolleyBall vb; // 创建无参构造方法 public VolleyBall(){} // 提供方法来获取单例对象 public static VolleyBall getVolleyBall(){ if(vb == null){ vb = new VolleyBall(); } return vb; } }
Test.java
package com.renbiao.test; import com.renbiao.pojo.VolleyBall; public class Test { public static void main(String[] args) { VolleyBall vb1 = VolleyBall.getVolleyBall(); VolleyBall vb2 = VolleyBall.getVolleyBall(); VolleyBall vb3 = VolleyBall.getVolleyBall(); System.out.println(vb1); System.out.println(vb2); System.out.println(vb3); } }
运行结果:
com.renbiao.pojo.VolleyBall@71dac704
com.renbiao.pojo.VolleyBall@71dac704
com.renbiao.pojo.VolleyBall@71dac704分析:
这是一个最初的懒汉式单例模式的创建
-
2.2 双重检查式创建
因为两根线程同时访问方法时,同时判断是否为空,同时进入if语句块,这样就会new出两个对象,如下图:
基于上面发生的问题,有以下解决方案
-
2.2.1 解决方法1:
方案:使用同步方法
方法:我们可以将取得的单例对象的方法进行同步的操作,将该方法改造为同步方法。
问题:访问该方法,可以让两个线程一起访问,如果将该方法改变成为同步方法,在取得该单例对象的时候,就只能一个线程一个线程的排队取得单例对象,会大大降低效率代码:
VolleyBall.java
package com.renbiao.pojo; public class VolleyBall { // 创建单例对象 private static VolleyBall vb; // 创建无参构造方法 public VolleyBall(){} // 提供方法来获取单例对象 // 方法1:同步方法 public synchronized static VolleyBall getVolleyBall(){ if(vb == null){ vb = new VolleyBall(); } return vb; } }
-
2.2.2 解决方法2:
方案:使用synchronized同步块的方法
方法:将方法从同步方法改为非同步方法,使用synchronized同步块的形式来操作if语句。
问题:虽然多个线程同时访问到该方法,但是仍然是在同步代码块的代码上进行排队等候,实质上与同步方法没有区别,降低程序的访问效率代码:
VolleyBall.java
package com.renbiao.pojo; public class VolleyBall { // 创建单例对象 private static VolleyBall vb; // 创建无参构造方法 public VolleyBall(){} // 提供方法来获取单例对象 public synchronized static VolleyBall getVolleyBall(){ // 方法2:synchronized同步块 synchronized (VolleyBall.class){ if (vb == null){ vb = new VolleyBall(); } } return vb; } }
-
2.2.3 解决方法3:
方案:使用双重检查方法
进行两次if判断
第一次的判断没有涉及到同步代码块, 不会造成线程排队来取对象的问题。
第二次的判断是为了让并发的线程没有机会创建第二个对象的可能。代码:
VolleyBall.java
package com.renbiao.pojo; public class VolleyBall { // 创建单例对象 private static VolleyBall vb; // 创建无参构造方法 public VolleyBall(){} // 提供方法来获取单例对象 public synchronized static VolleyBall getVolleyBall(){ //方法3:双重检查 if (vb == null) { synchronized (VolleyBall.class) { if (vb == null) { vb = new VolleyBall(); } } } return vb; } }
Test.java
package com.renbiao.test; import com.renbiao.pojo.VolleyBall; public class Test { public static void main(String[] args) { VolleyBall vb1 = VolleyBall.getVolleyBall(); VolleyBall vb2 = VolleyBall.getVolleyBall(); VolleyBall vb3 = VolleyBall.getVolleyBall(); System.out.println(vb1); System.out.println(vb2); System.out.println(vb3); } }
运行结果:
com.renbiao.pojo.VolleyBall@71dac704
com.renbiao.pojo.VolleyBall@71dac704
com.renbiao.pojo.VolleyBall@71dac704分析:
双重检查的单例模式的创建,可以实现懒汉式模式创建,也可以避免多线程的影响。
1.3 单例模式创建方式选择
-
懒汉式:
创建时机:不用时不创建,用到时才创建。
优点:节约内存空间
缺点:第一次访问时创建单例对象,第二次到第N次就不用创建新对象了,而是使用第一次创建出来的单例对象。第一次用到(创建单例)对象的执行过程,要比第二次到第N次的执行效率要低,访问效率要慢,因为有一个创建对象的过程。
-
饿汉式:
创建时机:不论暂时用不用,都先将对象创建出来
优点:不论用不用,都有这个单例对象。不论是第一次访问还是第二次第N次访问,对于这个单例对象的取得,都一样,就是将已经创建好的单例对象取出来,直接用就可以了。
缺点:如果这个单例对象暂时没有用处,就比较占用内存。
-
双重检查式:
懒汉式的多线程并发问题的处理,既然是懒汉式的多线程并发问题的扩充,如果我们要使用懒汉式,一般我们都要使用这种扩充后的双重检查形式。
== 在实际项目中,以上懒汉式和饿汉式都会用到。==
1.4 单例模式的优缺点
1.4.1 优点
- 由于单例模式在内存中有且只有一个实例,减少了内存的开支。
- 由于单例模式只生成一个实例,所以减少了系统性能的开销。
- 单例模式是在系统中,有且只有一个访问点,方便管理。
1.4.2 缺点
- 单例模式一般情况下都是没有接口的,如果没有接口,对于程序的扩展就会变得困难,如果要扩展,除了修改单例所在类的源代码,没有其他办法。
- 单例模式对测试是不方便的。在并行环境中,单例模式没有完成,就不能进行有效的测试。
1.5 附录
转载自蛙课网
B站地址:单例设计模式
Gitee代码:
gitee地址:https://gitee.com/gitofrr/wkcto-singleton.git