【设计模式】学习单例模式,五种方式实现单例模式


前言

设计模式一直是我的弱项,最近也在花时间学习,趁热打铁,做一下单例模式的总结!
(文中参考的是尚学堂Java视频)


正文

单例单例,字面意思很容易理解,就是单个实例,单例模式的核心作用就是:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点(公共方法)。

单例模式的常见应用场景:

  • Windows的任务管理器
  • Windows的回收站
  • 网站的计数器
  • 应用程序的日志应用
  • 数据库连接池的设计
  • Application也是单例应用(Servlet会涉及到)
  • 在Spring中,每个Bean默认是单例
  • 在Servlet编程中,每个Servlet也是单例
  • 在Spring MVC框架/struts1框架中,控制器对象也是单例

优点:

  • 由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
  • 单例模式可以在系统设置全局的访问点、优化环共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理

单例模式的常见应用场景:

主要:

饿汉式(线程安全、调用效率高。但是不能延时加载)
懒汉式(线程安全,调用效率不高。但是可以延时加载)

其他:

双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题,不建议使用)
静态内部类式(线程安全,调用效率高。可以延时加载)
枚举单例(线程安全、调用效率高,不能延时加载)

饿汉式实现(单例对象立即加载)

要点

饿汉式单例模式代码中,static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,可以省略synchronized关键字

存在的问题

如果该类一直没有被调用,但是又自动初始化了,就会造成资源浪费

代码实现

package designpatterns.singleton;

/**
 * 饿汉式单例模式
 * @author Hertter
 * @since 2019年8月3日下午12:17:23
 */
public class HungryTypeTest {
	public static void main(String[] args) {
		// 测试单例模式
		HungryType hungryType1 = HungryType.getInstance();
		HungryType hungryType2 = HungryType.getInstance();
		// 两者的地址相等,对外存在一个对象
		System.out.println(hungryType1 == hungryType2);
	}
}

class HungryType {
	/*
	 * 在类加载器加载的时候,instance会自动被实例化
	 */
	private static HungryType instance = new HungryType();
	
	/*
	 * 私有化构造器,避免外部调用
	 */
	private HungryType() {}
	
	/*
	 * 提供对外的获取实例的方法
	 * 这里不需要加synchronized,因为创建对象时,此类是在类加载器中加载的,具备天然的线程安全
	 * 而且不加synchronized可以提高效率
	 */
	public static /*synchronized*/ HungryType getInstance() {
		return instance;
	}
}

懒汉式实现(单例对象延迟加载)

要点:

lazy load!延迟加载,懒加载!真正用的时候才加载!

存在的问题

资源利用率高了。但是每次调用getInstance()方法都要同步,并发效率较低

代码实现

package designpatterns.singleton;

/**
 * 懒汉式单例模式
 * @author Hertter
 * @since 2019年8月3日下午4:35:29
 */
public class LazyTypeTest {
	public static void main(String[] args) {
		// 测试单例模式
		LazyType lazyType1 = LazyType.getInstance();
		LazyType lazyType2 = LazyType.getInstance();
		// 两者的地址相等,对外存在一个对象
		System.out.println(lazyType1 == lazyType2);
	}
}

class LazyType {
	/*
	 * 在类加载器加载的时候,instance暂时先不初始化
	 */
	private static LazyType instance;

	/*
	 * 私有化构造器,避免外部调用
	 */
	private LazyType() {}
	
	/*
	 * 需要用到实例的时候,再进行初始化(实现了延迟加载)
	 * 考虑到线程安全的问题,需要加上synchronized,但会导致效率低
	 */
	public static synchronized LazyType getInstance() {
		if (instance == null) 
			instance = new LazyType();
		return instance;
	}
}

双重检测锁实现

要点:

这个模式将同步内容下放到if内部,提高了执行的效率,不必每次获取对象时都进行同步,只有第一次才同步,创建了以后就没必要了

存在的问题

由于编译器优化原因和JVM底层内部模型原因,偶尔会出问题(指令重排),不建议使用。

代码实现

package designpatterns.singleton;

/**
 * 双重检测锁单例模式
 * @author Hertter
 * @since 2019年8月3日下午5:00:05
 */
public class DoubleCheckTest {
	public static void main(String[] args) {
		// 多线程下测试单例模式
		new Thread(() -> {
			System.out.println(DoubleCheck.getInstance());
		}).start();
		// 打印发现两者的地址相同
		System.out.println(DoubleCheck.getInstance());
	}
}

class DoubleCheck {
	/*
	 * 在类加载器加载的时候,instance暂时先不初始化,类似懒汉式
	 */
	private static DoubleCheck instance;
	
	/*
	 * 私有化构造器,避免外部调用
	 */
	private DoubleCheck() {}
	
	/*
	 * 将同步内容下放到if内部,提高了执行效率,不必每次获取对象时都进行同步,只有第一次才需要同步
	 * 但是存在问题:由于编译器优化原因和JVM底层内部模型原因,偶尔会出现问题,不建议使用
	 */
	public static DoubleCheck getInstance() {
		if (instance == null) {
			synchronized(DoubleCheck.class) {
				if (instance == null) {
					instance = new DoubleCheck();
				}
			}
		}
		return instance;
	}
}

静态内部类实现方式(也是一种懒加载方式)

要点:

  • 外部类没有static属性,则不会像饿汉式那样立即加载对象。
  • 只有真正调用getInstance(),才会加载静态内部类。加载类时是线程安全的。
  • instance是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性。
  • 兼备了并发高效调用延迟加载的优势!

代码实现

package designpatterns.singleton;

/**
 * 静态内部类单例模式
 * @author Hertter
 * @since 2019年8月3日下午5:53:44
 */
public class StaticInnerClassTest {
	public static void main(String[] args) {
		// 测试单例模式
		StaticInnerClass staticInnerClass1 = StaticInnerClass.getInstance();
		StaticInnerClass staticInnerClass2 = StaticInnerClass.getInstance();
		// 两者的地址相等,对外存在一个对象
		System.out.println(staticInnerClass1 == staticInnerClass2);
	}
}

class StaticInnerClass {
	/*
	 * 静态内部类,在类中完成类的实例化,实现了懒加载方式
	 */
	private static class StaticInnerClassInstance {
		private static final StaticInnerClass instance = new StaticInnerClass();
	}
	
	/*
	 * 方法没有synchronized,执行效率高
	 */
	public static StaticInnerClass getInstance() {
		return StaticInnerClassInstance.instance;
	}
	
	/*
	 * 私有化构造器,避免被外部调用
	 */
	private StaticInnerClass() {}
}

枚举实现单例模式

优点:

  • 实现简单
  • 枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!

缺点:

  • 无延迟加载

代码实现

package designpatterns.singleton;

/**
 * 枚举实现单例模式
 * @author Hertter
 * @since 2019年8月4日上午12:22:04
 */
public class EnumTypeTest {
	public static void main(String[] args) {
		// 测试单例模式
		EnumType enumType1 = EnumType.INSTANCE;
		EnumType enumType2 = EnumType.INSTANCE;
		// 两者的地址相等,对外存在一个对象
		System.out.println(enumType1 == enumType2);
	}
}

enum EnumType {
	/*
	 * 定义一个枚举的元素,它代表了单例的一个实例
	 */
	INSTANCE;
	/*
	 * 单例可以有自己的操作
	 */
	public void operation() {
		// 功能操作
	}
}

如何选择合适的实现方式?

占用资源少,不需要延迟加载 ? 枚举式 好于 饿汉式
占用资源大,需要延迟加载 ? 静态内部类式 好于 懒汉式


总结

以上就是单例模式的小结,在面试的过程中,如果面试官要求手写一个单例模式的话,可以写一个饿汉一个懒汉和一个静态内部类方式,估计就没问题了,而且代码量都比较少。
强烈建议大家自己手敲一遍,加深印象!

发布了57 篇原创文章 · 获赞 282 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/weixin_41463193/article/details/98361983