单例模式与工厂模式

本文详细介绍了Java中的单例模式,包括饿汉式、懒汉式、双检锁/双重校验锁(DCL)、登记式/静态内部类和枚举的实现方式,总结了各种方式的优缺点。同时讲解了工厂模式的概念,如何创建ShapeFactory工厂类,通过创建Shape接口和实体类,实现对象的创建和管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、单例模式

1.1 什么是单例模式

单例模式是大多数开发人员在实际开发中使用频率最多的一种设计模式,目的就是使类的一个对象成为系统中的唯一实例。即一个类只能有一个对象实例。单例模式的好处有:节约系统内存空间,控制资源的使用等。

1.2 饿汉式

描述:就是按照一定的开发步骤,按照一定的模板进行开发,达到程序中只会有一个实例在干活的目的
优点:在类加载的时候就完成了实例化,避免了线程同步问题。
缺点:因为再类加载的时候就完成实例化,没有达到lazy-loading的效果,如果从始至终没有使用过这个实例,就会造成内存的浪费。

1.2.1 饿汉式(静态常量)

public class Singleton {
	//1.在类的内部,创建静态的全局唯一的对象
	//static:静态只能调静态,要想被getInstance()调用,就必须是静态资源
	private static Singleton singleton=new Singleton();
	//2.私有构造方法,不让外部来调用
	private Singleton() {}
	//3.通过自定义的静态方法获取实例
	//提供公共的访问方法,把singleton返回给外界调用
	public static Singleton getInstance() {
		return singleton;
	}
}

1.2.1 饿汉式(静态代码块)

public class Singleton {
	private static Singleton singleton;
	static {
		singleton=new Singleton();
	}
	private Singleton() {}
	public static Singleton getInstance() {
		return singleton;
	}
}

1.3 懒汉式

描述:延迟访问(没有第一时间创建对象,而是什么时候调用get()什么时候创建),线程不安全(有多条语句对共享资源进行操作)
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁synchronize才能保证单例,但加锁会影响效率。

1.3.1 懒汉式(线程不安全)

描述:这种写法起到了lazy loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了 if(singleton == null)判断语句时,还未来得及执行下一句,另一个线程也通过了这个判断语句,就会产生多个实例。所以在多线程环境下不能使用这种方式。

public class Singleton {
	//在类的内部,提供创建好的对象
	private static Singleton singleton;
	//私有化构造方法,目的是不让外界随便new
	private Singleton() {}
	//提供公共的访问方式,把singleton返回给外界调用位置
	public static Singleton getInstance() {
		if(singleton==null)
			singleton = new Singleton();
		return singleton;
	}
}

1.3.2 懒汉式(线程安全,方法同步)

描述:这种写法会让每一个线程在想获得类的实例时候,执行getInstance()方法都要进行同步,而实际上这个方法只要执行一次实例化代码就可以了,后面想获取该类实例,直接return就可以了。

public class Singleton {
	private static Singleton singleton;
	private Singleton() {}
	public static synchronized Singleton getInstance() {//把方法同步
		if(singleton==null) {
			singleton=new Singleton();
		}
		return singleton;
	}
}

1.4 双检锁/双重校验锁(DCL)

描述:采用双检锁机制,安全且在多线程情况下保持高性能。进行两次 if(singleton == null) 检查,这样可以保证线程安全。实例化代码只需要执行一次,后面再次访问时,判断 if(singleton == null),直接return实例化对象就可以了。
优点:线程安全,延迟加载,效率较高

public class Singleton {
	private static volatile Singleton singleton;
	private Singleton() {}
	//getInstance()的性能对应用程序很关键
	public static Singleton getInstance() {	
		if(singleton == null) {
			//锁的共享资源是静态,此时,锁对象必须是Singleton的字节码对象
			synchronized (Singleton.class) {
				if(singleton == null) {
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}
}

1.4.1 volatile的作用

一)保证线程修改的可见性
在Java的编写中,有时候为了提高运行效率,编译器会自动对其优化,把经常访问的变量缓存起来,而程序在读取这个变量的时候有可能读取的是缓存中的值,而不是内存中的。但是,在多线程编程时,变量的值可能因为别的线程而改变,而缓存中的值是不会跟着改变的,所以会引起读取的值与实际变量的值不一致的问题。
volatile是用来修饰不同线程访问和修改的变量。被volatile类型定义的变量,系统每次用它的时候都是直接从对应的内存中取值,而不会利用缓存。这样所有线程在任何时候拿到的变量的值都是相同的
二)禁止指令重排序
在JVM中,处理器的指令顺序在不影响结果的情况下是可以无序的,比如在执行 singleton = new Singleton(); 这条语句的时候,JVM并不是一次性执行完毕的,而是按照如下三步完成的:

  1. 为对象分配内存
  2. 执行构造方法语句,初始化实例对象
  3. 把singleton的引用指向分配的内存空间

在JVM这三步中的2和3不一定是顺序执行的,如果线程A执行顺序是132,在第2步执行完的时候,正好线程B执行完第一次 if(singleton == null) 判断,则会直接返回singleton,那么此时得到的singleton仅仅只是不为null,实际上并没有初始化,这样的对象是有问题的。
volatile在这个时候就体现它的意义所在,那就是保证执行命令不会被重排序,避免上述异常情况的发生,保证了线程的安全。

1.5 登记式/静态内部类

描述:对静态域使用延迟初始化,应该使用这种方法而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
它与饿汉式一样利用classloader机制保证初始化singeton时只有一个线程,但是不同的是:饿汉式是Singleton类被加载时就会实例化,没有lazy-loading的作用,而静态内部类方式在Singleton类被加载时不会实例化,而是在需要实例化时,调用getInstance(),才会加载SingletonInstance类,从而完成singleton实例化。
优点:避免线程不安全,延迟加载,效率高

public class Singleton {
	private Singleton() {}
	private static class SingletonInstance{
		private static final Singleton singleton = new Singleton();
	}
	public static Singleton getInstance() {
		return SingletonInstance.singleton;
	}
}

1.6 枚举

描述:这是实现单例模式的最佳方法,由于是从JDK1.5才添加,所以在实际开发项目中,很少有人用。它自动支持序列化机制,绝对防止多次实例化。

public enum Singleton {
	INSTANCE;
	public void whateverMethod() {	
	}
}

1.7 总结

  1. 一般情况,不推荐使用懒汉式,使用饿汉式
  2. 如果明确要实现lazy-loading效果时,可以使用静态内部类方式
  3. 如果涉及到反序列化创建对象时,可以使用枚举方式
  4. 如果有其他特殊需求,则可以考虑双检锁方式

2.工厂模式

2.1 什么是工厂模式

工厂模式是Java中最常见的设计模式之一。是一种创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
优点:

  1. 一个调用者想要创建一个对象,只要知道其名称就可以了
  2. 扩展性高,如果想要增加一个产品,只要扩展一个工厂类就行
  3. 屏蔽产品的具体实现,调用者只要关心产品的接口即可

缺点:
每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖

2.2 创建一个ShapeFactory工厂类

在这里插入图片描述

2.2.1 创建一个Shape接口

public interface Shape {
	void draw();
}

2.2.2 创建实现接口的实体类(Triangle、Square、Circle)

Triangle.java

public class Triangle implements Shape {
	@Override
	public void draw() {
		System.out.println("这是一个三角形");
	}
}

Square.java

public class Square implements Shape {
	@Override
	public void draw() {
		System.out.println("这是一个正方形");
	}
}

Circle.java

public class Cricle implements Shape {
	@Override
	public void draw() {
		System.out.println("这是一个三角形");
	}
}

2.2.3 创建ShapeFactory工厂类,生成基于给定信息的实体类的对象

public class ShapeFactory {
	//使用getShape方法获取形状类型的对象
	public Shape getShape(String shapeType) {
		if(shapeType == null) {
			return null;
		}
		if(shapeType.equalsIgnoreCase("Triangle")) {
			return new Triangle();
		}else if(shapeType.equalsIgnoreCase("Square")) {
			return new Square();
		}else if(shapeType.equalsIgnoreCase("Cricle")) {
			return new Cricle();
		}
		return null;
	}
}

2.2.4 使用该工厂,通过传递类型信息来获取实体类的对象

public class FactoryPatternDemo {
	public static void main(String[] args) {
		ShapeFactory shapeFactory=new ShapeFactory();
		//获取Triangle的对象,并调用它的draw方法
		Shape shape1=shapeFactory.getShape("Triangle");
		//调用Triangle的draw方法
		shape1.draw();
		Shape shape2=shapeFactory.getShape("Square");
		shape2.draw();
		Shape shape3=shapeFactory.getShape("Cricle");
		shape3.draw();
	}
}

2.2.5 运行结果

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值