写在前面
- 记录学习设计模式的笔记
- 提高对设计模式的灵活运用
学习地址
https://www.bilibili.com/video/BV1G4411c7N4
https://www.bilibili.com/video/BV1Np4y1z7BU
参考文章
http://c.biancheng.net/view/1317.html
项目源码
https://gitee.com/zhuang-kang/DesignPattern
4,创建者模式
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。
这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。
创建型模式分为:
- 单例模式
- 工厂方法模式
- 抽象工程模式
- 原型模式
- 建造者模式
5,单例模式
5.1 单例模式的定义和特点
单例(Singleton)模式的定义: 指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。
单例模式在现实生活中的应用也非常广泛,例如公司 CEO、部门经理等都属于单例模型。J2EE 标准中的 ServletgContext 和 ServletContextConfig、Spring 框架应用中的 ApplicationContext、数据库中的连接池等也都是单例模式。
单例模式有 3 个特点:
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点。
单例模式的优点:
- 单例模式可以保证内存里只有一个实例,减少了内存的开销。
- 可以避免对资源的多重占用。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
单例模式的缺点:
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
5.2 单例模式的结构与实现
5.2.1 单例模式的结构
- 单例类:包含一个实例且能自行创建这个实例的类。
- 访问类:使用单例的类。
5.2 代码实现
单例设计模式分类两种:
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
饿汉式(静态变量)
package com.zhuang.singleton.type1;
/**
* @Classname SingletonTest01
* @Description 饿汉式(静态变量)
* @Date 2021/3/17 9:26
* @Created by dell
*/
public class SingletonTest01 {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton {
//1.构造器私有化,外部能new、
private Singleton() {
}
//本类内部创建对象实例
private final static Singleton instance = new Singleton();
//对外部提供一个公有的静态方法
public static Singleton getInstance() {
return instance;
}
}
说明:
该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
静态代码块
package com.zhuang.singleton.type2;
/**
* @Classname SingletonTest02
* @Description 静态代码块
* @Date 2021/3/17 9:35
* @Created by dell
*/
public class SingletonTest02 {
public static void main(String[] args) {
Singleton2 instance = Singleton2.getInstance();
Singleton2 instance2 = Singleton2.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton2 {
//1.构造器私有化,外部能new、
private Singleton2() {
}
//本类内部创建对象实例
private static Singleton2 instance;
/*
在静态代码块中创建对象
*/
static {
instance = new Singleton2();
}
//对外部提供一个公有的静态方法
public static Singleton2 getInstance() {
return instance;
}
}
说明:
该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1基本上一样,**当然该方式也存在内存浪费问题。**
懒汉式 线程不安全
package com.zhuang.singleton.type3;
/**
* @Classname SingletonTest03
* @Description 懒汉式 线程不安全
* @Date 2021/3/17 9:39
* @Created by dell
*/
public class SingletonTest03 {
public static void main(String[] args) {
System.out.println("懒汉式,线程不安全!!!");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton {
private static Singleton instance;
private Singleton() {
}
//提供一个静态的公有方法 当使用到该方法时,才去创建instance
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
说明:
从上面代码我们可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。
懒汉式(线程安全 , 同步方法)
package com.zhuang.singleton.type4;
/**
* @Classname SingletonTest04
* @Description 懒汉式(线程安全 , 同步方法)
* @Date 2021/3/17 9:46
* @Created by dell
*/
public class SingletonTest04 {
public static void main(String[] args) {
System.out.println("懒汉式,线程安全!!!");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton {
private static Singleton instance;
private Singleton() {
}
//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
说明:
该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。
懒汉式(线程安全 , 同步代码块)
package com.zhuang.singleton.type5;
/**
* @Classname SingletonTest05
* @Description 懒汉式(线程安全 , 同步代码块)
* @Date 2021/3/17 9:50
* @Created by dell
*/
public class SingletonTest05 {
public static void main(String[] args) {
System.out.println("懒汉式,线程安全!,同步代码块");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton {
private static Singleton instance;
private Singleton() {
}
//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile
关键字, volatile
关键字可以保证可见性和有序性。
package com.zhuang.singleton.type6;
/**
* @Classname SingletonTest06
* @Description 双重检查,推荐使用
* @Date 2021/3/17 9:54
* @Created by dell
*/
public class SingletonTest06 {
public static void main(String[] args) {
System.out.println("懒汉式,双重检查,推荐使用");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
//提供一个静态的公有方法,加入双重检查代码,加入同步处理的代码,解决懒加载的问题
//保证效率。推荐使用
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
小结:
添加 volatile
关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。
静态内部类实现单例模式!
package com.zhuang.singleton.type7;
/**
* @Classname SingletonTest07
* @Description 静态内部类实现单例模式!
* @Date 2021/3/17 9:59
* @Created by dell
*/
public class SingletonTest07 {
public static void main(String[] args) {
System.out.println("静态内部类实现单例模式");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
class Singleton {
private static Singleton instance;
private Singleton() {
}
//写一个静态内部类,该类中有个静态属性,Singleton
public static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE;
public static synchronized Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
说明:
第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder
并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
小结:
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
枚举的方式实现单例模式
package com.zhuang.singleton.type8;
/**
* @Classname SingletonTest08
* @Description 枚举的方式实现单例模式
* @Date 2021/3/17 10:06
* @Created by dell
*/
public class SingletonTest08 {
public static void main(String[] args) {
System.out.println("枚举的方式实现单例模式,推荐使用");
Singleton instance = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println(instance == instance2);
//判断是否为单例
System.out.println(instance == instance2);
System.out.println("intstance的哈希值" + instance.hashCode());
System.out.println("intstance2的哈希值" + instance2.hashCode());
}
}
/*
枚举
*/
enum Singleton {
INSTANCE;//属性
public void method() {
System.out.println("method()方法被调用...");
}
}
说明:
枚举方式属于恶汉式方式。
5.3 单例模式的应用场景
- 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
- 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
- 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
- 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
- 频繁访问数据库或文件的对象。
- 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
- 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
5.4 存在的问题
破坏单例模式:
使上面定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。
-
序列化反序列化
Singleton类:
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; } }
Test类:
public class Test { public static void main(String[] args) throws Exception { //往文件中写对象 //writeObject2File(); //从文件中读取对象 Singleton s1 = readObjectFromFile(); Singleton s2 = readObjectFromFile(); //判断两个反序列化后的对象是否是同一个对象 System.out.println(s1 == s2); } private static Singleton readObjectFromFile() throws Exception { //创建对象输入流对象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\dell\\Desktop\\a.txt")); //第一个读取Singleton对象 Singleton instance = (Singleton) ois.readObject(); return instance; } public static void writeObject2File() throws Exception { //获取Singleton类的对象 Singleton instance = Singleton.getInstance(); //创建对象输出流 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\dell\\Desktop\\a.txt")); //将instance对象写出到文件中 oos.writeObject(instance); } }
上面代码运行结果是
false
,表明序列化和反序列化已经破坏了单例设计模式。 -
反射
Singleton类:
public class Singleton { //私有构造方法 private Singleton() {} private static volatile Singleton instance; //对外提供静态方法获取该对象 public static Singleton getInstance() { if(instance != null) { return instance; } synchronized (Singleton.class) { if(instance != null) { return instance; } instance = new Singleton(); return instance; } } }
Test类:
public class Test { public static void main(String[] args) throws Exception { //获取Singleton类的字节码对象 Class clazz = Singleton.class; //获取Singleton类的私有无参构造方法对象 Constructor constructor = clazz.getDeclaredConstructor(); //取消访问检查 constructor.setAccessible(true); //创建Singleton类的对象s1 Singleton s1 = (Singleton) constructor.newInstance(); //创建Singleton类的对象s2 Singleton s2 = (Singleton) constructor.newInstance(); //判断通过反射创建的两个Singleton对象是否是同一个对象 System.out.println(s1 == s2); } }
上面代码运行结果是
false
,表明序列化和反序列化已经破坏了单例设计模式
注意:枚举方式不会出现这两个问题。
问题的解决
-
序列化、反序列方式破坏单例模式的解决方法
在Singleton类中添加
readResolve()
方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。Singleton类:
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; } /** * 下面是为了解决序列化反序列化破解单例模式 */ private Object readResolve() { return SingletonHolder.INSTANCE; } }
源码解析:
ObjectInputStream类
public final Object readObject() throws IOException, ClassNotFoundException{ ... // if nested read, passHandle contains handle of enclosing object int outerHandle = passHandle; try { Object obj = readObject0(false);//重点查看readObject0方法 ..... } private Object readObject0(boolean unshared) throws IOException { ... try { switch (tc) { ... case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法 ... } } finally { depth--; bin.setBlockDataMode(oldMode); } } private Object readOrdinaryObject(boolean unshared) throws IOException { ... //isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类, obj = desc.isInstantiable() ? desc.newInstance() : null; ... // 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { // 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量 // 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。 Object rep = desc.invokeReadResolve(obj); ... } return obj; }
-
反射方式破解单例的解决方法
public class Singleton { //私有构造方法 private Singleton() { /* 反射破解单例模式需要添加的代码 */ if(instance != null) { throw new RuntimeException(); } } private static volatile Singleton instance; //对外提供静态方法获取该对象 public static Singleton getInstance() { if(instance != null) { return instance; } synchronized (Singleton.class) { if(instance != null) { return instance; } instance = new Singleton(); return instance; } } }
说明:
这种方式比较好理解。当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作。
5.5 JDK源码解析-Runtime类
Runtime类就是使用的单例设计模式。
-
通过源代码查看使用的是哪儿种单例模式
public class Runtime { private static Runtime currentRuntime = new Runtime(); /** * Returns the runtime object associated with the current Java application. * Most of the methods of class
Runtime
are instance * methods and must be invoked with respect to the current runtime object. * * @return theRuntime
object associated with the current * Java application. */ public static Runtime getRuntime() { return currentRuntime; } /** Don't let anyone else instantiate this class */ private Runtime() {} ... }从上面源代码中可以看出Runtime类使用的是恶汉式(静态属性)方式来实现单例模式的。
-
使用Runtime类中的方法
public class RuntimeDemo { public static void main(String[] args) throws IOException { //获取Runtime类对象 Runtime runtime = Runtime.getRuntime(); //返回 Java 虚拟机中的内存总量。 System.out.println(runtime.totalMemory()); //返回 Java 虚拟机试图使用的最大内存量。 System.out.println(runtime.maxMemory()); //创建一个新的进程执行指定的字符串命令,返回进程对象 Process process = runtime.exec("ipconfig"); //获取命令执行后的结果,通过输入流获取 InputStream inputStream = process.getInputStream(); byte[] arr = new byte[1024 * 1024* 100]; int b = inputStream.read(arr); System.out.println(new String(arr,0,b,"gbk")); } }
写在最后
- 如果我的文章对你有用,请给我点个,感谢你!
- 有问题,欢迎在评论区指出!