“ 设计模式详解。”
《Design Patterns: Elements of Reusable Object-Oriented Software》(即《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为"四人组(Gang of Four)"。
设计模式不是一种规定,也不需要为了设计模式而设计,错误的应用设计模式反而会让代码变得复杂,难以维护。相反,正确的应用设计模式,可以让我们的代码更加健壮,扩展性更好,逻辑也更加清晰。
设计模式一书针对不同的应用场景,列出了各自的最佳实践。随着技术的不断发展,可能一些范例已经不再适用,但是这本书带给我们思想上的指引,意义是非常重大的。设计模式一书,根据应用场景,大致分为建造型、结构型、行为型,总共 23 种,并提出了 6 大原则。
本文开始,试着详细介绍每种设计模式,有解释不到位,理解存在偏差的地方,请提出宝贵意见。
建造型-单例模式
单例模式 Singleton,即一个实例,类本身提供获取全局唯一的实例的方式,其它人使用的时候不需要重新创建对象,只需要通过类获取实例即可。
应用场景:配置类、工厂类等
关键点:
1、将实例变成成员变量。
2、将默认构造方法改为private。
优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
2、避免对资源的多重占用(比如写文件操作)。
实现方式:分为懒汉式 Lazy ,饿汉式,下面依次介绍
01
—
饿汉式
**
**
所谓饿汉式,即程序从硬盘加载到内存中,即实现初始化,由 JVM 实现,线程安全。
**
**
方式 1
public class Singleton01 { private static final Singleton01 INSTANCE=new Singleton01(); private Singleton01(){} public void m(){ System.out.println("hello singleton1"); } public static Singleton01 getInstance(){ return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()-> System.out.println(Singleton01.INSTANCE.hashCode())).start(); } }}
输出:
1059951119
1059951119
1059951119
1059951119
1059951119
1059951119
1059951119
1059951119
1059951119
可以看到打印的是同一个对象。
方式 2 将实例过程放到静态代码块中
public class Singleton02 { private static final Singleton02 INSTANCE; static { INSTANCE = new Singleton02(); } private Singleton02() { } public void m() { System.out.println("hello singleton2"); } public static Singleton02 getInstance() { return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()-> System.out.println(Singleton02.INSTANCE.hashCode())).start(); } }}
输出:
2062686098206268609820626860982062686098206268609820626860982062686098206268609820626860982062686098
方式 3 使用枚举的方式
public enum Singleton08 { INSTANCE ; public void m(){ System.out.println("hello singleton8"); } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()-> System.out.println(Singleton08.INSTANCE.hashCode())).start(); } }}
输出:
2062686098206268609820626860982062686098206268609820626860982062686098206268609820626860982062686098
方式 1 和 2,除了创建实例的过程,其余均相同,理论上没有区别,比较简单,应用也最多。方式 3 使用枚举实现,这种方式是非常安全的,不能通过反射创建实例。整体来看,饿汉式这种方式,也存在一些问题,即我们没有使用这个对象,但是已经实例化了,能不能在我使用的时候才进行实例化呢,那就是懒汉式,即懒加载。下面演示一下懒汉式实现方法。
02
—
懒汉式
**
**
懒汉式,即在第一次调用的时候才真正的实例化,在加载的时候不进行实例化。
方式 4
public class Singleton03 { private static Singleton03 instance; private Singleton03() { } public void m() { System.out.println("hello singleton3"); } public static Singleton03 getInstance() { if (null == instance) { instance = new Singleton03(); } return instance; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()-> System.out.println(Singleton03.getInstance().hashCode())).start(); } }}
输出:
2062686098105995111910599511191059951119105995111910599511191059951119105995111910599511191059951119
可以看到,多线程的情况下,会概率出现创建多个对象的情况,那是因为,当一个线程发现当前实例为 null,需要创建一个对象,在这个过程中另一个线程也发现实例为 null,也去创建实例,导致出现多个实例,因此我们使用方式 4 来解决。
方式 5
public class Singleton04 { private static Singleton04 instance; private Singleton04() { } public void m() { System.out.println("hello singleton4"); } public static synchronized Singleton04 getInstance() { if (null == instance) { try { instance = new Singleton04(); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } return instance; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()-> System.out.println(Singleton04.getInstance().hashCode())).start(); } }}
输出:
1585461681158546168115854616811585461681158546168115854616811585461681158546168115854616811585461681
此方式在方法上面增加 synchronized 关键字,将方法变成同步方法,因为 synchronized 关键字使用的重量级锁,上下文切换影响效率,为了减少锁的范围,可能会想到下面的方式优化代码。
方式 6
public class Singleton05 { private static Singleton05 instance; private Singleton05() { } public static Singleton05 getInstance() { if (null == instance) { synchronized (Singleton05.class) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } instance = new Singleton05(); } } return instance; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(() -> System.out.println(Singleton05.getInstance().hashCode())).start(); } }}
输出:
29574062727790602098586510057349981047393000410312950991585461681206268609815881478521059951119
运行发现,这样是无法实现,因为多线程运行时,速度非常快,导致多个线程同时阻塞住,而获取到锁的时候就会去创建实例,因此会产生多个实例,因此需要使用下面的方式。
方式 7:DCL 方式(Double Check Locking)
public class Singleton06 { private static volatile Singleton06 instance; private Singleton06() { } public static Singleton06 getInstance() { if (null == instance) { synchronized (Singleton06.class) { if (null == instance) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } instance = new Singleton06(); } } } return instance; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(() -> System.out.println(Singleton06.getInstance().hashCode())).start(); } }}
输出:
2062686098206268609820626860982062686098206268609820626860982062686098206268609820626860982062686098
此种方式是比较完美的解决方式。
双重检查的意义:线程由于无法获取锁,处于阻塞状态,当线程获取锁时,第二次检查一下是否对象创建完成,如果已创建完成,直接返回此对象即可。
DCL 加 volatile 的意义:保证线程可见,防止指令重排序
方式 8:内部类的方式
public class Singleton07 { private Singleton07() { } private static class SingletonHold{ private final static Singleton07 INSTANCE=new Singleton07(); } public static Singleton07 getInstance() { return SingletonHold.INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()-> System.out.println(Singleton07.getInstance().hashCode())).start(); } }}
输出:
868202916868202916868202916868202916868202916868202916868202916868202916868202916868202916
使用内部类,可以将在类加载的时候不会实例化,但是调用的时候会进行实例化,比较好的方式,也是线程安全的。
总结:
单例模式是非常常用的设计模式,看似简单,其实这里面还是有许多值得研究学习。
设计模式不是为了生搬硬套,应该根据实际情况,灵活的运用,后面会介绍更多的设计模式和设计原则,欢迎大家一起讨论学习。
欢迎关注我们
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于