创建者模式的主要关注点是”怎样创建对象”,主要特点是”将对象的创建与使用分离”
这样降低了系统耦合度,使用者不需要关注对象的创建细节。
创建者模式分为:
单例模式
工厂方法模式
抽象工程模式
原型模式
建造者模式
今天先学习一下单例模式
什么是单例设计模式(Singleton Pattern) 单例设计模式是Java中最简单的设计模式之一。属于创建型模式,提供了创建对象的最佳方式。
该模式涉及到一个单一的类,该类负责创建自己的对象,且确保只有一个实例被创建。这个类同时也提供了一种访问其唯一实例的方式。
因此单例模式主要有以下角色:
单例模式的实现 饿汉式
类加载就会导致该单实例对象被创建
静态变量方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Singleton { private Singleton () { } private static Singleton instance = new Singleton (); public static Singleton getInstance () { return instance; } }
静态代码块方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class Singleton { private Singleton () { } private static Singleton instance; static { instance = new Singleton (); } public static Singleton getInstance () { return instance; } }
以上两种方式在类的加载时就会创建对象,但如果实例化的对象长时间不被使用,则都会带来一定的内存开销。因此并不推荐饿汉式的声明方式。
懒汉式
类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
方式一(线程不安全) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Singleton { private Singleton () { } private static Singleton instance; public static Singleton getInstance () { if (instance == null ) { instance = new Singleton (); } return instance; } }
方式一当有多个先线程在获取单例方法中等待CPU执行权时,导致有多个对象被实例化
方式二(线程安全) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Singleton { private Singleton () { } private static Singleton instance; public static synchronized Singleton getInstance () { if (instance == null ) { instance = new Singleton (); } return instance; } }
这种方法一改方式一,在getInstance
方法上加了同步锁,使得线程安全,但由于直接给方法上锁,使得锁粒度过高,大大降低了程序的性能
方式三(双重检查锁) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class Singleton { private Singleton () { } private static volatile Singleton instance; public static Singleton getInstance () { if (instance == null ) { synchronized (Singleton.class) { if (instance == null ) { instance = new Singleton (); } } } return instance; } }
在判断为空后,Singleton加上类锁,再进行一次判断,为空则实例化对象。此方法也是一种常见的单例模式的实现方式
方式四(静态内部类) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Singleton { private Singleton () { } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton (); } public static Singleton getInstance () { return SingletonHolder.INSTANCE; } }
在第一次加载Singleton
类时不会去初始化INSTANCE,只有第一次调用getInstance
,虚拟机加载SingletonHolder
,并初始化INSTANCE
,这样不仅能确保线程安全,也能保证Singleton
类的唯一性。静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。它利用了JVM的特性,在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和间的浪费。
枚举方式
单元素的枚举类型已经成为实现Singleton的最佳方法
这句话是Joshua Bloch 前辈在《Effective Java》一书中提到的。此方式不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
1 2 3 4 5 6 public enum Singleton { INSTANCE }
1 2 3 4 5 6 7 8 9 10 11 public class Client { public static void main (String[] args) { Singleton instance = Singleton.INSTANCE; Singleton anotherInstance = Singleton.INSTANCE; System.out.println(instance == anotherInstance); } }
枚举方式属于饿汉式,在不考虑浪费内存空间的情况下,可首选枚举法。上面提到了,枚举方式可以避免反序列化重新创建新的对象,那么看来单例是可以被破坏的,下面就来分析分析
存在的问题 上面的方式,枚举除外,都可以创建多个对象。有两种实现方式:序列化和反射
序列化 采用懒汉式静态内部类方式创建单例(要继承序列化接口)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Singleton implements Serializable { private Singleton () { } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton (); } public static Singleton getInstance () { return SingletonHolder.INSTANCE; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class Client { public static void main (String[] args) throws Exception { writeObject2File(); readObjectFromFile(); readObjectFromFile(); } public static void readObjectFromFile () throws Exception { ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("C:\\Users\\Lin\\Desktop\\demo.txt" )); Singleton instance = (Singleton) ois.readObject(); System.out.println(instance); ois.close(); } public static void writeObject2File () throws Exception { Singleton instance = Singleton.getInstance(); ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("C:\\Users\\Lin\\Desktop\\demo.txt" )); oos.writeObject(instance); oos.close(); } }
1 2 3 4 tech.maiquer.signleton.destroy.demo1.Singleton@312b1dae tech.maiquer.signleton.destroy.demo1.Singleton@7530d0a 进程已结束,退出代码0
说明:将单例对象流读取写到桌面的demo文件中,再读取文件取出对象打印出来,发现两次地址并不统一,说明序列化破坏了单例
反序列破解解决方法 只需要在Singleton
类中增加readResolve
方法
1 2 3 4 5 6 7 8 private Object readResolve () { return SingletonHolder.INSTANCE; }
测试:
1 2 3 4 tech.maiquer.signleton.destroy.demo1.Singleton@3764951d tech.maiquer.signleton.destroy.demo1.Singleton@3764951d 进程已结束,退出代码0
现在就解决了序列化破坏单例的问题了
反射 先同样使用懒汉式静态内部类的方式构造单例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Singleton { private Singleton () { } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton (); } public static Singleton getInstance () { return SingletonHolder.INSTANCE; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Client { public static void main (String[] args) throws Exception { Class clazz = Singleton.class; Constructor cons = clazz.getDeclaredConstructor(); cons.setAccessible(true ); Singleton s1 = (Singleton) cons.newInstance(); Singleton s2 = (Singleton) cons.newInstance(); System.out.println(s1 == s2); } }
通过反射,拿到对象构造器,无视private
权限
打印结果如下:
发现反射毫无感情的破坏了单例(我单例不要面子的吗…)或许这就是暴力美学吧~~~
解决反射 魔高一尺,道高一丈
既然反射可以任性的获取构造器构造实例,我们拦不住。那就加一个flag
标志,限制该类只能创建一个实例
同样以懒汉式静态内部类为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class Singleton { private static boolean flag = false ; private Singleton () { synchronized (Singleton.class) { if (flag) { throw new RuntimeException ("唯一实例已被创建,不能再创建啦!!!" ); } flag = true ; } } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton (); } public static Singleton getInstance () { return SingletonHolder.INSTANCE; } }
客户端测试:
1 2 3 4 5 6 7 8 9 10 11 Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at tech.maiquer.signleton.destroy.demo2.Client.main(Client.java:17) Caused by: java.lang.RuntimeException: 唯一实例已被创建,不能再创建啦!!! at tech.maiquer.signleton.destroy.demo2.Singleton.<init>(Singleton.java:13) ... 5 more 进程已结束,退出代码1
可见,程序出发了运行时异常,告诉我们唯一实例已存在,休想再贪
JDK源码实例 JDK中的Runtime类的构造方式就是一种单例,我们一起来看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Runtime { private static Runtime currentRuntime = new Runtime (); public static Runtime getRuntime () { return currentRuntime; } private Runtime () {} }
通过前面的铺垫,不难看出,这是一个通过饿汉式创建单例的案例
该类有很多好玩的函数,例如exec方法,它可以运行终端命令,例如我要获取我的电脑的网卡信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import java.io.IOException;import java.io.InputStream;public class RuntimeDemo { public static void main (String[] args) throws IOException { Runtime runtime = Runtime.getRuntime(); String command = "ipconfig" ; Process process = runtime.exec(command); InputStream is = process.getInputStream(); byte [] arr = new byte [1024 * 1024 * 100 ]; int len = is.read(arr); System.out.println(new String (arr, 0 , len, "GBK" )); } }
打印结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 Windows IP 配置 以太网适配器 以太网: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 本地连接* 10: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 本地连接* 11: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : 以太网适配器 SSTAP 1: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : 以太网适配器 VMware Network Adapter VMnet1: 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::70c5:6a39:2ffc:e4d1%16 IPv4 地址 . . . . . . . . . . . . : 192.168.35.1 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 以太网适配器 VMware Network Adapter VMnet8: 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::9506:5df3:8e59:c204%9 IPv4 地址 . . . . . . . . . . . . : 192.168.221.1 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 以太网适配器 蓝牙网络连接: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 WLAN: 连接特定的 DNS 后缀 . . . . . . . : IPv4 地址 . . . . . . . . . . . . : 192.168.3.119 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 192.168.3.1 进程已结束,退出代码0
结束语:
我是自己最大的敌人 — 拿破仑