设计模式之单例模式
设计模式之单例模式
应用场景
适用场景
适用场景:
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
11.需要频繁实例化然后销毁的对象。
2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
3.有状态的工具类对象。
4.频繁访问数据库或文件的对象。
以下都是单例模式的经典使用场景:
1.资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
2.控制资源的情况下,方便资源之间的互相通信。如线程池等。
实际应用
-
数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
-
多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
java之外的应用
windows的任务管理器和回收站
01 饿汉式
这也是最常用的一种方式,它是线程安全的,它依靠JVM来实现线程安全,因为JVM加载一个class,只会加载一次。
package com.cyc.design.singleton;
/**
* @author chenyunchang
* @version 1.0
* @date 2021/6/1 10:06
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全
* 简单实用,推荐使用!
* 唯一缺点:不管用到与否,类装载时就完成实例化
* Class.forName("")
* (话说你不用的,你装载它干啥)
*/
public class Singleton01 {
private static final Singleton01 INSTANCE = new Singleton01();
/**
* 构造方法为私有,只能在当前类中new,外部类无法new出来
*/
private Singleton01() {
}
public static Singleton01 getInstance() {
return INSTANCE;
}
public void c() {
System.out.println("C");
}
public static void main(String[] args) {
Singleton01 instance1 = Singleton01.getInstance();
Singleton01 instance2 = Singleton01.getInstance();
//获取到的两个示例进行比较,如果不是一个实例,则会是false,是一个实例则为true
System.out.println(instance1 == instance2);
}
}
查看结果
02 另一种饿汉式
package com.cyc.design.singleton;
/**
* @author chenyunchang
* @version 1.0
* @date 2021/6/1 10:06
* 饿汉式 第二种写法,这种是在静态代码块里进行实例化
*/
public class Singleton02 {
private static final Singleton02 INSTANCE ;
static {
INSTANCE = new Singleton02();
}
public void c() {
System.out.println("C");
}
/**
* 构造方法为私有,只能在当前类中new,外部类无法new出来
*/
private Singleton02() {
}
public static Singleton02 getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
Singleton02 instance1 = Singleton02.getInstance();
Singleton02 instance2 = Singleton02.getInstance();
//获取到的两个示例进行比较,如果不是一个实例,则会是false,是一个实例则为true
System.out.println(instance1 == instance2);
}
}
查看结果
03 懒汉式
package com.cyc.design.singleton;
/**
* @author chenyunchang
* @version 1.0
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
*/
public class Singleton03 {
private static Singleton03 INSTANCE;
public void c() {
System.out.println("C");
}
/**
* 构造方法为私有,只能在当前类中new,外部类无法new出来
*/
private Singleton03() {
}
public static Singleton03 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton03();
}
return INSTANCE;
}
public static void main(String[] args) {
Singleton03 instance1 = Singleton03.getInstance();
Singleton03 instance2 = Singleton03.getInstance();
//获取到的两个示例进行比较,如果不是一个实例,则会是false,是一个实例则为true
System.out.println(instance1 == instance2);
}
}
线程安全问题
-
懒汉式虽然达到了按需加载的目的, 但是却带来了线程安全的问题,当多个线程进入到判断
INSTANCE == null
时,可能会出现两个线程都new了一个对象,此时,便破坏了单例模式,JVM出现了两个此对象的实例,后续的业务中可能会出现由于线程不安全带来的其他问题。事例如下
package com.cyc.design.singleton; /** * @author chenyunchang * @version 1.0 * lazy loading * 也称懒汉式 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题 */ public class Singleton03 { private static Singleton03 INSTANCE; public void c() { System.out.println("C"); } /** * 构造方法为私有,只能在当前类中new,外部类无法new出来 */ private Singleton03() { } public static Singleton03 getInstance() { if (INSTANCE == null) { try { //这里让进入此代码块的线程睡一毫秒 Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Singleton03(); } return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> System.out.println(Singleton03.getInstance().hashCode()) ).start(); } } }
查看结果
可以看到,获取到对象的hashcode并不相同,所以,并不是一个对象,因此懒汉式在多线程环境下,会有线程安全问题
04 优化懒汉式 -保证线程安全
通过在获取实例的方法上加锁
package com.cyc.design.singleton;
/**
* @author chenyunchang
* @version 1.0
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class Singleton04 {
//volatile关键字禁止指令重排
private static volatile Singleton04 INSTANCE;
public void c() {
System.out.println("C");
}
/**
* 构造方法为私有,只能在当前类中new,外部类无法new出来
*/
private Singleton04() {
}
/**
* 添加synchronized锁,保证只有一个线程能进入该方法
* 每次getInstance之前,去判断此方法是否已加锁,是:则等待锁释放,否:则去尝试获取锁,并加锁。
* 由于多了获取锁,释放锁的步骤,因此降低了很多执行效率,但是却是线程安全的
* @return
*/
public static synchronized Singleton04 getInstance() {
if (INSTANCE == null) {
try {
//这里让进入此代码块的线程睡一毫秒
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton04();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(Singleton04.getInstance().hashCode())
).start();
}
}
}
查看结果
05 优化懒汉式-提升执行效率(反例)
package com.cyc.design.singleton;
/**
* @author chenyunchang
* @version 1.0
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class Singleton05 {
//volatile关键字禁止指令重排
private static volatile Singleton05 INSTANCE;
public void c() {
System.out.println("C");
}
/**
* 构造方法为私有,只能在当前类中new,外部类无法new出来
*/
private Singleton05() {
}
public static Singleton05 getInstance() {
if (INSTANCE == null) {
//妄图通过减小同步代码块的方式提高效率,然后不可行
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 < 100; i++) {
new Thread(() ->
System.out.println(Singleton05.getInstance().hashCode())
).start();
}
}
}
查看结果
06 优化懒汉式-提升执行效率
package com.cyc.design.singleton;
/**
* @author chenyunchang
* @version 1.0
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class Singleton06 {
//volatile关键字禁止指令重排
private static volatile Singleton06 INSTANCE;
public void c() {
System.out.println("C");
}
/**
* 构造方法为私有,只能在当前类中new,外部类无法new出来
*/
private Singleton06() {
}
public static Singleton06 getInstance() {
if (INSTANCE == null) {
//双重检查
synchronized (Singleton06.class) {
if (INSTANCE == null) {
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 < 100; i++) {
new Thread(() ->
System.out.println(Singleton06.getInstance().hashCode())
).start();
}
}
}
查看结果
为什么要加volatile?
为了防止指令重排。这里涉及到类的加载过程。
首先,第一个线程进来了, 加上锁之后,进入到INSTANCE = new Singleton06();代码,在初始化进行到一半的时候,也就是在preparation阶段,已经给Singleton06申请完内存,里面的成员变量已经赋过默认值,比如0,此时INSTANCE 已经指向这个分配的内存, 已经不再是null,此时另外一个线程进来了,由于此时INSTANCE 已经进行了半初始化状态,所以在if (INSTANCE == null)为false,此时另一个线程会拿到这个INSTANCE中的成员变量进行操作, 这样显然是不满足要求的。
想要解析这个问题, 需要查看其字节码文件
例如下面这个测试类T, 使用idea插件查看其字节码文件
在0 new #2 <com/cyc/jvm/c0_basic/T>之后,已经申请过内存。
4 invokespecial #3 <com/cyc/jvm/c0_basic/T.> 这个给类中的静态变量赋初始值
在调用完4之后,才会把这块内存赋值给t,但是由于指令可能会重排的原因, 如果先执行的是7 astore_1, 相当于先把这个地址扔到内存中, 然后在进行的T初始化, 这种情况下,在双重检查懒汉式单例中,就会出现有别的线程读取到半初始化的单例。
07 静态内部类方式
类被加载的时候,内部类并不会被加载。
虚拟机加载一个class的时候,只加载一次,它的内部类也只加载一次,所以从始至终就只有一个instance,所以线程安全
package com.cyc.design.singleton;
/**
* @author chenyunchang
* @version 1.0
* lazy loading
* 静态内部类方式
* JVM保证单例
* 加载外部类时不会加载内部类,这样可以实现懒加载
*/
public class Singleton07 {
private static class Singleton07Holder {
private static final Singleton07 INSTANCE = new Singleton07();
}
public void c() {
System.out.println("C");
}
/**
* 构造方法为私有,只能在当前类中new,外部类无法new出来
*/
private Singleton07() {
}
public static Singleton07 getInstance() {
return Singleton07Holder.INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(Singleton07.getInstance().hashCode())
).start();
}
}
}
查看结果
08 使用枚举类
《Effective Java》 作者给出了一种完美的单例模式,即使用枚举类,枚举类型的单例天生线程安全,
我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。
所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。