单例:保证在内存当中只有一个实例存在
在代码实现级别保证只能有一个实例存在
有8种写法,这里融合了一下,将基础的和最推荐的写了出来。其中饿汉式中包含了两种:静态变量和静态代码块
饿汉式,如其名。在类加载的时候就创建了单例,并不等到第一次调用的时候,就仿佛饿汉一样迫不及待地想赶紧先得到一个单例。
/**
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全
* 简单实用,推荐使用!
* 唯一缺点:不管用到与否,类装载时就完成实例化,会拖慢程序启动速度
*/
public class Mgr01 {
// 自己创建一个实例留给其他类使用
// 方式一:直接创建并初始化对象(静态常量)
private static final Mgr01 INSTANCE = new Mgr01();
/*
// 方式二 创建变量,将初始化过程放在status代码块中(静态代码块)
private static final Mgr01 INSTANCE;
status {
INSTANCE = new Mgr01();
}
*/
// 保证别人创建不了这个对象的实例
private Mgr01() {}
// 其他类获取实例的唯一方法
public static Mgr01 getInstance() {
return INSTANCE;
}
// 单例中的行为即方法
public void m(){
System.out.println("m");
}
}
// 测试
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
}
// 结果
> true
// 通过判断两次获取的对象实例的地址都是同一个,说明两次获取的均为同一个实例,即为单例
如果要保证Mgr01
这个对象在内存中只能有一个实例,那么构造方法就肯定不能被其他类调用。所以这里构造方法设置成private
私有的
那么禁止别人创建实例,自己就要先创建一个实例,留着给别人用,那么就出现了创建status
、final
修饰的Mgr01
的实例
那么就还需要一个外界能访问并得到实例的方法,于是便有了getInstance()
方法。
由于在类加载阶段就已经创建了单例,那么在运行时JVM就能保证线程安全。
不管用到与否,类装载时就完成实例化,会拖慢程序启动速度
懒汉式,如其名。在类加载时并不获取单例,而是在第一次使用时,才初始化并获取这个单例。仿佛是懒的动,到使用前才去拿一样。
/**
* 懒汉式 延迟加载(lazy loading)
* 虽然达到了按需初始化的目的,但却带来了多线程访问不安全的情况
*/
public class Mgr03 {
// 自己创建一个变量,但是没有初始化
private static Mgr03 INSTANCE;
// 保证别人创建不了这个对象的实例
private Mgr03 (){}
// 其他类获取实例的唯一方法
public static Mgr03 getInstance() {
// 在其他类每次获取时都会去判断,这个变量是否为null,如果是null的话,创建一个实例并返回,否则直接返回
if (INSTANCE == null) {
// 此处的try-catch包裹的线程睡眠,是为了测试当多线程同时获取时的问题而写的,否则可不写try-catch里的内容
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr03();
}
return INSTANCE;
}
}
// 测试
// 多线程测试,测试10个线程去获取这个单例的情况。
// 通过输出hash值来判断是否时同一个实例。同一个类不同的对象的hash值是不同的
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
// 使用lambda表达式创建线程
new Thread(() ->
System.out.println(Mgr03.getInstance().hashCode())
).start();
}
}
// 结果
1754195085
135417039
478974226
218859975
546405844
224049094
224049094
1037172763
79372097
858497792
// 通过结果可以看出,基本每一个对象的hash值都说是不同的,也就是说,每一个实例都是新的实例,这就造成了获取单例失败的问题
前面还是将构造方法设置为私有的,并创建一个静态的变量INSTANCE
用于存放实例。
但是此时这个变量并没有被初始化,所以不能用final
修饰。
那么初始化的过程留在了获取实例的方法中。每一个调用者访问getInstance()
方法时,都要先判断INSTANCE
变量是不是被初始化过,如果为null
,那么就初始化一次;如果不为null
,那么直接返回。
但是当多线程同时访问getInstance()
方法时就会发生意外。比如线程A
优先访问了getInstance()
方法,当它还在方法体内判断为null
,要去但还没来得及去调用构造方法初始化对象时*(代码中的线程睡眠就是为了延长这里的执行时间)*,线程B
进入了getInstance()
方法,此时它判断也是null
,那么它也去调用构造方法初始化对象去了。此时有可能不止只有A
和B
,可能还会有很多线程都进去了。那么每一个线程所获得的实例都不是同一个实例,而是自己线程所创建的实例。INSTANCE
最后的值将由最后一个判断为``null`的线程所创建的实例决定。
不在类加载时初始化获取单例,而是在初次使用时初始化,加快了程序启动的速度
在未初始化时,当多线程同时获取实例时,有几率返回多个不同的实例出来,就不再是单例了,虽然后续的实例是同一个。
主要采用:同步方法
懒汉式在未初始化时多线程同时访问是会有线程安全问题的。
/**
* 加锁的懒汉式
* 在getInstance()方法前加关键字:synchronized
*/
public class Mgr04 {
private static Mgr04 INSTANCE;
private Mgr04 (){}
public static synchronized Mgr04 getInstance() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
return INSTANCE;
}
}
// 测试
// 多线程测试,测试100个线程去获取这个单例的情况。
// 通过输出hash值来判断是否时同一个实例。同一个类不同的对象的hash值是不同的
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
// 使用lambda表达式创建线程
new Thread(() ->
System.out.println(Mgr04.getInstance().hashCode())
).start();
}
}
// 多次实验结果
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
// 通过结果可以看出,加锁的懒汉式返回的就是同一个对象了
synchronized
关键字在这里锁定的是Mgr04.class
。
那么加把锁让多线程在同时获取实例时一个一个进,前一个出来了后一个才能进.
使得懒汉式变得线程安全
由于在获取单例的方法上加了锁,所以所有的线程到这里都变成了串行,严重影响效率
号称:完美的写法之一
主要采用:同步代码块 + 双重检查
像是方式三那样给懒汉式加锁,会导致性能变低。那么有效的缩小加锁范围也能够提高性能
/**
* 双重检查+加锁的懒汉式
* 在getInstance()方法前加关键字:synchronized
*/
public class Mgr05 {
private static Mgr05 INSTANCE;
private Mgr05 (){}
public static Mgr05 getInstance() {
// 第一重检查
if (INSTANCE == null) {
// 加锁
synchronized (Mgr05.class) {
// 第二重检查
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr05();
}
}
}
return INSTANCE;
}
}
// 测试
// 多线程测试,测试20个线程去获取这个单例的情况。
// 通过输出hash值来判断是否时同一个实例。同一个类不同的对象的hash值是不同的
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
// 使用lambda表达式创建线程
new Thread(() ->
System.out.println(Mgr05.getInstance().hashCode())
).start();
}
}
// 多次执行结果
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
// 通过结果可以看出,返回的就是同一个对象
synchronized
关键字在这里锁的是Mgr05.class
。
有效的缩小加锁范围也能够提高性能。
但是如果只有第一重检查和加锁,没有第二重检查。那么可能存在线程A
在判断为null
后,被线程B
继续执行了,然后线程B
创建了实例,释放锁。此时线程A
拿到了锁,因为在第一重检查已经判断为null
,所以直接拿锁执行创建实例的操作。又造成了线程不安全的情况。
所以加了第二道检查,如果线程A
拿着锁进入时,再判断发现变量INSTANCE
不为null
了,说明已经被其他线程抢先初始化了,那么线程A
便放弃创建直接返回值就行。
使得懒汉式变得线程安全的同时减少了被锁包裹的代码范围
缩小了锁的范围,即实现了减小锁带来的低效率,又实现了懒加载,降低系统启动的时间
号称:完美的写法之一
通过内部类调用私有构造的方式返回单例,由 JVM 保证线程安全
public class Mgr06 {
// 构造方法依旧是私有的
private Mgr06(){}
// 创建内部类:Mgr06Holder(意为:Mgr06的持有者)
private static class Mgr06Holder {
// 内部类调用并创建外部类的私有构造
private static final Mgr06 INSTANCE = new Mgr06();
}
// 为调用者返回一个单例
public static Mgr06 getInstance() {
// 调用内部类的静态私有成员变量
return Mgr06Holder.INSTANCE;
}
}
// 测试 还是使用多线程测试
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
// 使用lambda表达式创建线程
new Thread(() ->
System.out.println(Mgr05.getInstance().hashCode())
).start();
}
}
// 多次执行结果
218859975
218859975
218859975
218859975
218859975
218859975
218859975
218859975
218859975
// 通过结果可以看出,返回的就是同一个对象
内部类和它的外部类性质:
JVM规则:
同一个类class
只能加载一次,在加载class
时就会把status
修饰的变量或者代码块或者方法执行了。
类加载时不会加载其内部类
首先,构造方法依旧是私有的,防止被外部其他类创建对象;
然后,创建一个静态内部类,在内部类里调用其外部类Mgr06
的私有构造创建实例并赋值给内部类的private
、status
、final
的变量INSTANCE
;
再然后,创建一个getInstance()
方法,提供外部调用,返回的是内部类的私有变量,也就是该类的单例。
执行过程:
程序加载时,加载了Mgr06
类,但是其内部类并没有被加载。
第一次访问getInstance()
方法时,JVM首次加载了Mgr06Holder
类,此时同时也执行了创建Mgr06
的实例并赋值给变量INSTANCE
的操作。
此时Mgr06Holder
类已经被JVM加载过一次了,根据JVM的规定哪怕是多线程此时也无法再一次的加载Mgr06Holder
类,且变量INSTANCE
是用final
修饰的,也就是说变量INSTANCE
将永远不会被再次初始化或赋值。而此时该变量里装着的就是Mgr06
类的一个实例。
那么,每一次返回Mgr06Holder
类的变量INSTANCE
都是将同一个Mgr06
类的一个实例返回,从而达到返回单例的效果。
解决了线程安全问题
实现了懒加载
解决了使用锁带来的效率低下的问题
不能防止反序列化,反序列化仍然会创建一个新的实例。
可以通过额外的工作(Serializable、transient、readResolve())来实现序列化,防止这个问题。
号称:完美的写法之一
在《Effective Java》中讲到的
不仅可以解决线程同步,还可以解决反序列化
public enum Mgr07 {
// 单例
INSTANCE;
// 该类的行为方法
public void whateverMethod() {
// ...
}
}
// 测试
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
// 使用lambda表达式创建线程
new Thread(() ->
System.out.println(Mgr07.INSTANCE.hashCode())
).start();
}
Mgr07.INSTANCE.m();
}
// 结果
2132637868
2132637868
2132637868
2132637868
2132637868
2132637868
2132637868
2132637868
2132637868
2132637868
// 通过结果可以看出,返回的就是同一个对象
借助JDK1.5中添加的枚举来实现单例模式。
不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
最优雅的实现方式. 系统内存 中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。
写法区别,比如:
Mgr01.getInstance().m();
Mgr07.INSTANCE.m();
本模式完。
回到设计模式系列
祝君有所收获,完结撒花!
如果您想关注更多,可以点击以下链接:
回到学习路线