引言
我用这个标题告诉那些没有接触单例模式的同学,单例有8中写法只多,不要小看单例模式。同时告诉那些了解单例模式的人,我们不能学孔乙己,咬文嚼字敲砖弄沙认为懂了8中模式就是懂了单例。
单例模式是什么
单例模式对于一个类只能创建一个实例,即在JVM中只有一块内存代表该对象的实例。
- 确保任何时候使用该实例的时候所有的资源都是相同的。
- 单例的类有义务提供全局访问的入口即getInstance。
为什么要有单例模式
- 资源节约,避免重复的对象占用宝贵的内存资源
- 全局共享,如数据库连接,线程池等。就像你最好对外提供一个证件,太多的切换不是容易控制的。
单例的8中写法
01 :基本功
/**
* 唯一缺点:类加载的时候就会创建实例
*
* 注:工作中常用的反而是这种略带缺陷的方法,简单实用
*/
public class Mgr01 {
//JVM保证每个CLASS只会LOAD到内存一次
private static final Mgr01 instance = new Mgr01();
//私有构造器
private Mgr01() {
}
public static Mgr01 getInstance() {
return instance;
}
}
02 同上
/**
* 同Mgr01本质一样
*/
public class Mgr02 {
//JVM保证每个CLASS只会LOAD到内存一次
private static final Mgr02 INSTANCE;
static {
INSTANCE = new Mgr02();
}
//私有构造器
private Mgr02() {
}
public static Mgr02 getInstance() {
return INSTANCE;
}
}
03 饿汉式
饿汉仅仅是个花名,如果直接创建对象,而该对象特别消耗资源,可以考虑下面模式。
注:实际上如果你可以接收01的写法,通常就很完美
package com.kouryoushine.deginmodel.singleton.msb;
/**
* @ClassName Mgr01
* @Description TODO
* @Author kouryoushine
* @Date 2021/3/15 22:36
* @Version 1.0
*/
/**
* 缺点:多线程访问的时候有影响,不安全
*/
public class Mgr03 {
//JVM保证每个CLASS只会LOAD到内存一次
private static Mgr03 INSTANCE;
//私有构造器
private Mgr03() {
}
/**
* 懒汉式,获取实例的时候在初始化实例
* @return
*/
public static Mgr03 getInstance() {
if (INSTANCE == null) {
//模拟线程停顿,增加线程切换,暴露多线程问题
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr03();
}
return INSTANCE;
}
/**
* 如果获取到hashcode一致说明对象是一个对象
* 这里验证:多线程环境可能创建两个不同实例
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i <100 ; i++) {
new Thread(()->{
Mgr03 instance = Mgr03.getInstance();
System.out.println(instance.hashCode());
}).start();
}
}
}
04 加锁保平安
你说多线程不安全,那好我加锁确保安全。
对一个方法加锁会降低该方法100倍的性能,因此加锁的方法不能再高并发的复杂场景中使用。
package com.kouryoushine.deginmodel.singleton.msb;
/**
* @ClassName Mgr01
* @Description TODO
* @Author kouryoushine
* @Date 2021/3/15 22:36
* @Version 1.0
*/
/**
* 缺点:加锁可能影响效率
*/
public class Mgr04 {
//JVM保证每个CLASS只会LOAD到内存一次
private static Mgr04 INSTANCE;
//私有构造器
private Mgr04() {
}
/**
* 懒汉式,获取实例的时候在初始化实例
* @return
*/
public static synchronized Mgr04 getInstance() {
if (INSTANCE == null) {
//模拟线程停顿,增加线程切换,暴露多线程问题
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
return INSTANCE;
}
/**
* 如果获取到hashcode一致说明对象是一个对象
* 这里验证:多线程环境可能创建两个不同实例
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i <100 ; i++) {
new Thread(()->{
Mgr04 instance = Mgr04.getInstance();
System.out.println(instance.hashCode());
}).start();
}
}
}
05 简单的妄想
下面方法是错误的,等同于没加锁。这种方法很容易想到,深入考虑就发现在一步的时候线程已经不安全了
package com.kouryoushine.deginmodel.singleton.msb;
/**
* @ClassName Mgr01
* @Description TODO
* @Author kouryoushine
* @Date 2021/3/15 22:36
* @Version 1.0
*/
/**
* 缺点:加锁可能影响效率,这里缩小锁的范围优化性能,实际上没有效果,getInstace的第一句就无法保证线程安全
* 这种方式看似明显的错误,实际上却是直观的解决线程不安全的方法
*/
public class Mgr05 {
//JVM保证每个CLASS只会LOAD到内存一次
private static Mgr05 INSTANCE;
//私有构造器
private Mgr05() {
}
/**
* 懒汉式,获取实例的时候在初始化实例
* @return
*/
public static Mgr05 getInstance() {
if (INSTANCE == null) {
//模拟线程停顿,增加线程切换,暴露多线程问题
synchronized (Mgr05.class){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr05();
}
}
return INSTANCE;
}
/**
* 如果获取到hashcode一致说明对象是一个对象
* 这里验证:多线程环境可能创建两个不同实例
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i <100 ; i++) {
new Thread(()->{
Mgr05 instance = Mgr05.getInstance();
System.out.println(instance.hashCode());
}).start();
}
}
}
06 双重检查机制
注意双重检查的巧妙之处,在确定之后多一步判断,确保只生产一个实例
package com.kouryoushine.deginmodel.singleton.msb;
/**
* @ClassName Mgr01
* @Description TODO
* @Author kouryoushine
* @Date 2021/3/15 22:36
* @Version 1.0
*/
/**
* 缺点:针对加锁的优化,双重检查机制
*/
public class Mgr06 {
//JVM保证每个CLASS只会LOAD到内存一次
// volatile 防止指令重排
private static volatile Mgr06 INSTANCE;
//私有构造器
private Mgr06() {
}
/**
* 懒汉式,获取实例的时候在初始化实例
* @return
*/
public static Mgr06 getInstance() {
if (INSTANCE == null) {
//模拟线程停顿,增加线程切换,暴露多线程问题
synchronized (Mgr06.class){
//双重检查:获取到锁后再次检查是否已经存在实例
//锁定过程中instance值不会被改变,所以此时判断是准确的
if(INSTANCE== null){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
/**
* 如果获取到hashcode一致说明对象是一个对象
* 这里验证:多线程环境可能创建两个不同实例
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i <100 ; i++) {
new Thread(()->{
Mgr06 instance = Mgr06.getInstance();
System.out.println(instance.hashCode());
}).start();
}
}
}
07 看起来很美
推荐内部类的写法,简单就是美。
package com.kouryoushine.deginmodel.singleton.msb;
/**
* @ClassName Mgr01
* @Description 完美的单例写法之一
* @Author kouryoushine
* @Date 2021/3/15 22:36
* @Version 1.0
*/
/**
* 静态内部类的方式实现
* 1. JVM保证每个CLASS只会LOAD到内存一次
* 2. 加载外部类时不会加载内部类,这样可以保证懒加载
*/
public class Mgr07 {
private static class Mgr07Holder{
//JVM保证每个CLASS只会LOAD到内存一次
private final static Mgr07 INSTANCE = new Mgr07();
}
//私有构造器
private Mgr07() {
}
/**
* 懒汉式,获取实例的时候在初始化实例
* @return
*/
public static Mgr07 getInstance() {
return Mgr07Holder.INSTANCE;
}
/**
* 如果获取到hashcode一致说明对象是一个对象
* 这里验证:多线程环境可能创建两个不同实例
* @param args
*/
public static void main(String[] args) {
for (int i = 0; i <100 ; i++) {
new Thread(()->{
Mgr07 instance = Mgr07.getInstance();
System.out.println(instance.hashCode());
}).start();
}
}
}
一行代码实现单例-枚举类
Effective JAVA作者推荐写法,个人并不喜欢。我要的类,给我的枚举。
public enum Mgr08 {
INSTANCE;
public static void main(String[] args) {
for (int i = 0; i <100 ; i++) {
new Thread(()->{
Mgr08 instance = Mgr08.INSTANCE;
System.out.println(instance.hashCode());
}).start();
}
}
}
后记
单例模式使用的场景其实不多,为了面试上面8中写法足够了。如果从技术追求的角度,单例模式从字面上理解即可,掌握第一种写法即可。工作中发现必须用单例的时候用一次就彻底理解了。另外,如果你的代码中大量使用单例模式,就要思考一下你真的懂单例模式吗?