单例模式-singleton
开篇点题,大家都会单例设计思维吗?如果让你来写几种单例的类,你会写几种?
1.前言
学习设计模式是每个程序员必备的内功,是提高编程能力是必不可少的一道有效途径。
2.singleton 单例模式
应用场景:只需要一个实例,比如各种的manager,各种的factory。
2.1 饿汉式:
类加载到内存后,就实例一个单例,jvm保证线程安全,简单实用,推荐使用! 唯一缺点,类装载时就实例化。
public class Mr01 {
private static final Mr01 INSTANCE = new Mr01();
// 将Mr01的空参构造方法私有化
private Mr01(){}
// 提供getInstance方法方便外部类new对象
public static Mr01 getInstance(){
return INSTANCE;
}
}
测试类:
public class Main {
public static void main(String[] args) {
// 如果外部类想调用Mr01这个类,必须使用调用getInstance方法
Mr01 mgr = Mr01.getInstance();
}
}
证明不管new多少次都是同一个实例对象:
Mr01 mgr1 = Mr01.getInstance();
Mr01 mgr2 = Mr01.getInstance();
System.out.println(mgr1==mgr2);
2.2 懒汉式
对于饿汉式的单例模型,因为一开始使用静态类,所以一旦类加载进了jvm内存,就始化了,但是懒汉式不同,它会进行判断!
缺点:懒汉式会引发线程安全。
public class Mr02 {
// 静态不初始化Mr02,注意这里不能加final。
private static Mr02 INSTANCE;
// 设置空参构造方法,必须是私有的。
private Mr02(){};
// 设置getInstance方法提供外部类new对象
public static Mr02 getInstance(){
if (INSTANCE == null){
INSTANCE = new Mr02();
}
return INSTANCE;
}
}
// 此类被实例化对象后只要是被new了一次,下次不管new多少次jvm还是使用刚开始new的对象。
测试类:
public class Main02 {
public static void main(String[] args) {
// 外部调用Mr02
Mr02 mg1 = Mr02.getInstance();
Mr02 mg2 = Mr02.getInstance();
System.out.println(mg1==mg2);
}
}
使用多线程测试懒汉式的线程安全问题:
public class Mr02 {
// 静态不初始化Mr02,注意这里不能加final
private static Mr02 INSTANCE;
// 设置空参构造方法,必须是私有的。
private Mr02(){};
// 设置getInstance方法提供外部类new对象
public static Mr02 getInstance(){
if (INSTANCE == null){
// 启用线程睡眠,如果一个线程启动起来后下一个线程在第一个线程未结束时仍然继续调用Mr02.getInstance()方法,就会引发线程安全问题。
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mr02();
}
return INSTANCE;
}
}
测试类:
public class Main02 {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println( Mr02.getInstance().hashCode());
}).start();
}
}
}
// 使用100次循环来启动线程,并且线程内调取Mr02的getInstance方法。
测试结果是100次线程启动后,几乎没有几个Mr02类对象的hashCode是相同的!所以懒汉式会引发线程安全问题!
那么怎么解决好这个问题?使用synchronized锁就行啦!
public class Mr03 {
// 静态不初始化Mr02,注意这里不能加final,加上final必须要new出对象。
private static Mr03 INSTANCE;
// 设置空参构造方法,必须是私有的。
private Mr03(){};
// 设置getInstance方法提供外部类new对象
public static synchronized Mr03 getInstance(){
if (INSTANCE == null){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mr03();
}
return INSTANCE;
}
}
测试类:
public class Main02 {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println( Mr03.getInstance().hashCode());
}).start();
}
}
}
加上锁之后,不管多少个线程抢占资源,类还是单例的!
但是后来发现这种写法是在类上面加的锁,是会引发资源空间浪费的,于是接下来又有一种写法了,叫做单例的双重判断!
public class Mr04 {
private static Mr04 INSTANCE;
// 私有化构造方法
private Mr04(){}
// 提供getInstance方法
public static Mr04 getInstance(){
if (INSTANCE == null){
synchronized (Mr04.class){
if (INSTANCE == null){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
return INSTANCE;
}
}
接下来又产生了几种很棒的写法,第一个是静态内部类的方式,jvm保证单例,加载外部类的时候不会加载内部类,这样可以实现懒汉式加载。
public class Mr05 {
// 私有化构造方法
private Mr05(){}
// 提供静态内部类
private static final class MrHolder{
// 初始化Mr05
private static final Mr05 INSTANCE = new Mr05();
}
// 提供外部类访问的getInstance方法
public static Mr05 getInstance(){
// 返回的是内部类的INSTANCE
return MrHolder.INSTANCE;
}
}
此方式保证线程安全的是jvm,jvm中只会创建Mr05的一个对象实例!
最后一种方式:最牛逼的方式,叫做枚举单例方式!
ublic enum Mr06 {
INSTANCE;
}
这种思想既可以防止线程不安全问题,还可以防止反序列化,真神了!
总结:工作中最常用的还是饿汉式!