单例模式,顾名思义,就是被单例的对象只能由一个实例存在。单例模式的实现方式是,一个类能返回一个对象的一个引用(永远是同一个对象)和一个获得该唯一实例的方法(必须是静态方法)。通过单例模式,我们可以保证系统中只有一个实例,从而在某些特定的场合下达到节约或者控制系统资源的目的。
这个看起来是最为简单的设计模式,其实它有许多坑~~~
单例模式
1、饿汉模式
最常见、最简单的单例模式写法之一。顾名思义,“饿汉模式” 就是很 “饥渴”,所以一上来就需要给它新建一个实例。但这种方法有一个明显的缺点,那就是不管有没有调用过获得实例的方法(本例中为 getInstance()
),每次都会新建一个实例。
package cn.liangyy;
/**
* 饿汉式
* 类加载到内存后,就实例一个单例,JVM保证线程安全
* 优点:简单实用,推荐使用
* 缺点:不管用到与否,类装载时就完成实例化
*/
public class Mgr01 {
//一开始就新建一个实例
private static final Mgr01 INSTSNCE = new Mgr01();
//默认构造方法
private Mgr01() {};
//获得实例的方法
public static Mgr01 getInstance(){
return INSTSNCE;
}
//模拟其他业务方法
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);
System.out.println(m1);
System.out.println(m2);
for (int i = 0; i < 100; i++) {
System.out.println(Mgr01.getInstance().hashCode());
}
}
}
复制代码
2、懒汉模式
最常见、最简单的单例模式写法之二,跟 “饿汉模式” 是 “好基友”。再次顾名思义,“懒汉模式” 就是它很懒,一开始不新建实例,只有当它需要使用的时候,会先判断实例是否为空,如果为空才会新建一个实例来使用。
package cn.liangyy;
/**
* lazy loading
* 懒汉式
* 虽然达到了按需初始化的目的,但却带来了线程不安全的问题
*/
public class Mgr02 {
private static Mgr02 INSRANCE;
private Mgr02(){}
public static Mgr02 getInstance(){
if (INSRANCE == null){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSRANCE = new Mgr02();
}
return INSRANCE;
}
public void m(){
System.out.println("m");
}
public static void main(String args[]){
//多线程测试
for (int i = 0;i < 100;i++){
new Thread(() -> {
System.out.println(Mgr02.getInstance());
}).start();
}
}
}
复制代码
现在,问题来了,当我们多线程访问的时候,我们获得的实例对象还是同一个对象吗?答案很肯定不是的,我们一起来分析一下。
执行过程:
多线程的时候,执行到 if 判断,第一个线程 A 在判断中,另一个线程 B 也进来了,B 比 A 要快,它也进行 if 判断,因为还没有 new Mgr02,所以 INSTANCE = null,这时它继续往下执行,执行到 INSRANCE = new Mgr02();
它就 new 了一个 Mgr02 对象,这时线程 A 执行完了,它也 new 了一个 Mgr02,这时就有两个 INSTANCE 对象,所以带来了更大的问题。这样我们用多线程测试并打印对象的 HashCode,便会发现结果中的对象并非一个,而是多个。
3、 线程安全的懒汉模式
这种方式用来解决懒汉模式的线程安全的
package cn.liangyy;
/**
* 加锁解决Mgr02中出现的线程不安全问题
*/
public class Mgr03 {
private static Mgr03 INSTANCE;
private Mgr03(){}
public static synchronized Mgr03 getInstance(){
if (INSTANCE == null){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr03();
}
return INSTANCE;
}
public void m(){
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Mgr03.getInstance().hashCode());
}).start();
}
}
}
复制代码
4、 双重检验锁(double check)
线程安全的懒汉模式解决了多线程的问题,看起来完美了。但是它的效率却下降了,每次调用获得实例的方法getInstance()
时都要进行同步,但是多数情况下并不需要同步操作(例如我的 INSTANCE 实例并不为空可以直接使用的时候,就不需要给getInstance()
加同步方法,直接返回 INSTANCE 实例就可以了)。所以只需要在第一次新建实例对象的时候,使用同步方法。于是在前面的基础上,又有了“双重检验锁”的方法。
package cn.liangyy;
/**
* 懒汉式
* 使用双重检查保证线程安全,但也带来了效率的下降
* 这是目前好的解决方法之一
*/
public class Mgr06 {
/**
* volatile:保证线程可见性,不具有原子性
* 当将volatile加到变量上,该变量的初始化过程将不被指令重排序
*/
private static volatile Mgr06 INSTANCE;
private Mgr06(){}
public static Mgr06 getInstance(){
if (INSTANCE == null){
//双重检查
synchronized (Mgr06.class){
if (INSTANCE == null){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Mgr06.getInstance().hashCode());
}).start();
}
}
}
复制代码
5、 静态内部类
上面的方法,修修补补,实在是太复杂了,还涉及到 JVM 的一些知识,而且volatile
关键字对 JDK 的版本也有要求。所以我们得换一种方法,即“静态内部类”。这种方式利用 JVM 自身的机制来保证线程安全,因为 Mgr07Holder 是私有的,除了getInstance()
之外没有其他的方式可以访问实例对象了,而且只有在调用getInstance()
时才会去真正的创建实例对象。(这里有点类似“懒汉模式”)
package cn.liangyy;
/**
* 静态内部类方式(JVM帮我们保证线程安全)
* JVM保证单例
* 加载外部类时不会加载内部类,这样可以实现懒加载
*/
public class Mgr07 {
private Mgr07(){}
private static class Mgr07Holder{
private final static Mgr07 INSTANCE = new Mgr07();
}
public static Mgr07 getInstance(){
return Mgr07Holder.INSTANCE;
}
public void m(){
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Mgr07.getInstance().hashCode());
}).start();
}
}
}
复制代码
6、 枚举单例
当然,除了“静态内部类”的方式,还有简单的不能再简单的“枚举单例”。我们可以通过Mgr08.INSTANCE
来访问实例对象,这比getInstance()
要简单的多,而且创建枚举默认就是线程安全的,还可以防止反序列化带来的问题。这种方式虽然不常用,但是最为推荐。悄悄告诉你们这么优(niu)雅(bi)的方法,来自于新版《Effective Java》这本书。
package cn.liangyy;
/**
* 枚举单例(完美的单例模式)
* 不用担心线程安全的问题
* 不仅可以解决线程同步,还可以防止反序列化
*/
public enum Mgr08 {
//唯一的取值,下面的调用只能调用这一个,没有争议
INSTANCE;
//剩下的都是业务方法
public void m(){
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Mgr08.INSTANCE.hashCode());
}).start();
}
}
}
复制代码
评论