文章目录
前言
设计模式是开发中的一柄利剑,有助于提高效率,减少冗余代码,使代码更适应将来业务的变化等.
但在实际项目中,我们要尽量做到"心中有剑,手中无剑",不要被这些条条框框所限制,使用设计模式的目的是为了帮助我们开发,不要让它成为我们的限制/禁锢.
单例模式
下面说的"完美"不一定适用于所有场景,
其他不那么"完美"的方式也未必就一点都不能用,还是要具体问题具体分析.
"完美"的两种方式(都算是懒汉)
懒汉:这个类很懒,只有被用到时才去加载资源,否则不加载资源
饿汉:这个类很饿,程序运行-类被加载时,就加载资源.
使用枚举enum
这是effectice JAVA中提出的,可以说是单例模式最完美的一种实现了,不仅可以从正常使用上保证单例,还能防止反序列化,因为枚举类没有构造方法.
但是我从个人使用习惯上,感觉用枚举类实现单例,怪怪的,不习惯.
我还是喜欢饿汉模式,或者使用内部类方式实现的懒汉.
public enum SingletonTest001 {
INSTANCE("大黄");
private String name;
SingletonTest001(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void say() {
System.out.println("hello~");
}
public static void main(String[] args) {
// 通过输出hashcode,可以判断其是否是多线程安全的单例
for (int i = 0; i < 100; i++) {
new Thread(() -> System.out.println(SingletonTest001.INSTANCE.hashCode())).start();
}
}
}
使用内部类
这个也是很完美的一种方式,有JVM来帮我保证单例.
众所周知,当一个类被加载的时候,它的内部类是不会被加载的;只有当第一次使用它的内部类时,才会加载内部类.
public class SingletonTest002 {
private String name = "大黄";
// 私有化构造方法
private SingletonTest002() {
}
// 内部类,作为一个持有SingletonTest002的容器
private static class Holder {
private static final SingletonTest002 INSTANCE = new SingletonTest002();
}
public static SingletonTest002 getInstance() {
return Holder.INSTANCE;
}
public String getName() {
return name;
}
public void say() {
System.out.println("hello~");
}
public static void main(String[] args) {
// 通过输出hashcode,可以判断其是否是多线程安全的单例
for (int i = 0; i < 100; i++) {
new Thread(() -> System.out.println(SingletonTest002.getInstance().hashCode()))
.start();
}
}
}
其他实现方式
饿汉方式
饿汉很简单,直接,干脆,在一些我们知道它一定会被用到的类,我觉得直接用饿汉方式更好.
JVM只会加载一次类,在类加载的时候初始化静态变量.
public class SingletonTest003 {
public static final SingletonTest003 INSTANCE = new SingletonTest003();
private String name = "大黄";
// 私有化构造方法
private SingletonTest003() {
}
public String getName() {
return name;
}
public void say() {
System.out.println("hello~");
}
public static void main(String[] args) {
// 通过输出hashcode,可以判断其是否是多线程安全的单例
for (int i = 0; i < 100; i++) {
new Thread(() -> System.out.println(SingletonTest003.INSTANCE.hashCode()))
.start();
}
}
}
一个懒汉的错误示范(无锁,线程不安全)
上面那个懒汉方式,有人觉得不优雅,有些类根本用不到还加载资源,浪费内存,然后就想了个办法,getInstance时先判断有没有初始化,没有初始化过就去初始化,然后返回实例.
下面这样写在单线程环境下没问题,如果是多线程并发访问的话,就可能不会是真的单例了;
加入线程t1和t2同时调用getInstance()方法,他俩都判断INSTANCE == null为true,就各自都加载资源去了,造成资源浪费或者严重的不易察觉的错误.
public class SingletonTest004 {
private static SingletonTest004 INSTANCE;
public static SingletonTest004 getInstance() {
if (INSTANCE == null) {
// 模拟加载资源的过程,初始化比较慢
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new SingletonTest004();
}
return INSTANCE;
}
private String name = "大黄";
// 私有化构造方法
private SingletonTest004() {
}
public String getName() {
return name;
}
public void say() {
System.out.println("hello~");
}
public static void main(String[] args) {
// 通过输出hashcode,可以判断其是否是多线程安全的单例
for (int i = 0; i < 100; i++) {
new Thread(() -> System.out.println(SingletonTest004.getInstance().hashCode()))
.start();
}
}
}
方法上加锁的懒汉
上面那个线程不安全的问题,有人很快想到答案了,加锁就是了嘛!
但是加锁是需要额外消耗资源的,这里如果多线程下getInstance调用比较频繁,还是很消耗资源的.
public class SingletonTest004 {
private static SingletonTest004 INSTANCE;
public static synchronized SingletonTest004 getInstance() {
if (INSTANCE == null) {
// 模拟加载资源的过程,初始化比较慢
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new SingletonTest004();
}
return INSTANCE;
}
private String name = "大黄";
// 私有化构造方法
private SingletonTest004() {
}
public String getName() {
return name;
}
public void say() {
System.out.println("hello~");
}
public static void main(String[] args) {
// 通过输出hashcode,可以判断其是否是多线程安全的单例
for (int i = 0; i < 100; i++) {
new Thread(() -> System.out.println(SingletonTest004.getInstance().hashCode()))
.start();
}
}
}
一个懒汉的错误示范(锁代码块,单次判断,线程不安全)
既然锁加在方法上,会消耗很多资源,那就想着:先判断一次,只有加载资源时候加锁,以后判断就不加锁了.
下面这个例子有很大问题,假设还未实例化,很多个线程同时访问getInstance,大家都判断INSTANCE == null为true了,然后排着队一个个的去加载资源,实例化,没有解决问题.
public class SingletonTest006 {
private static SingletonTest006 INSTANCE;
public static SingletonTest006 getInstance() {
if (INSTANCE == null) {
// 模拟加载资源的过程,初始化比较慢
synchronized (SingletonTest006.class) {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new SingletonTest006();
}
}
return INSTANCE;
}
private String name = "大黄";
// 私有化构造方法
private SingletonTest006() {
}
public String getName() {
return name;
}
public void say() {
System.out.println("hello~");
}
public static void main(String[] args) {
// 通过输出hashcode,可以判断其是否是多线程安全的单例
for (int i = 0; i < 100; i++) {
new Thread(() -> System.out.println(SingletonTest006.getInstance().hashCode()))
.start();
}
}
}
双重检验锁Double Check Lock
这里要注意,一定要加volatile关键字,防止指令重排序.
这个算是没啥问题的懒汉了,但是感觉略复杂,不那么优雅…
public class SingletonTest007 {
private static volatile SingletonTest007 INSTANCE;
public static SingletonTest007 getInstance() {
if (INSTANCE == null) {
// 模拟加载资源的过程,初始化比较慢
synchronized (SingletonTest007.class) {
if (INSTANCE == null) {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new SingletonTest007();
}
}
}
return INSTANCE;
}
private String name = "大黄";
// 私有化构造方法
private SingletonTest007() {
}
public String getName() {
return name;
}
public void say() {
System.out.println("hello~");
}
public static void main(String[] args) {
// 通过输出hashcode,可以判断其是否是多线程安全的单例
for (int i = 0; i < 100; i++) {
new Thread(() -> System.out.println(SingletonTest007.getInstance().hashCode()))
.start();
}
}
}