单例模式
- 单例模式:用来创建独一无二的,只能有一个实例对象,确保一个类只有一个实例,并提供一个全局访问点。
通俗说单例模式只将对象 new 一次,与工厂模式区别,工厂模式像是与单例模式相同,其实差距很大,工厂模式并不是实现将对象只 new 一次,它是主要避免对象的紧耦合,实现最终的解耦,或者是说单例模式是类的设计者要考虑的问题,工厂模式是类的使用者需要考虑的问题,不要混淆两个设计模式
者
单例模式实现
public class Singleton{
// 记录唯一实例对象
private static Singleton uniqueInstance;
private Singleton(){}
// 线程非安全 返回 uniqueInstance 对象
public static Singleton getInstance(){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
编写测试类
class Test{
public static void main(String[] args){
Singleton singleton = new Singleton();
singleton.getInstance();
System.out.println(singleton.getInstance());
System.out.println(singleton.getInstance());
System.out.println(singleton.getInstance());
}
}
显示打印结果
Thread.Singleton@610455d6
Thread.Singleton@610455d6
Thread.Singleton@610455d6
可以看出只创建一次对象每次调用返回同一个对象
此方法在单线程环境下可行,没有任何问题。但是如果两个线程,访问这段代码就会出现问题,当开启线程 A 和线程 B 。线程 A 第一次执行判断 uniqueInstace 为空,进入实例化一个对象,这时候线程 B 在判断 uniqueInstance 同样为空,同样实例化一个对象,这时候就不符合单例模式的定义,会出现两个或多个实例化对象,在 Java 中解决此类问题方式是添加关键字 synchroized 加锁
多线程单例模式
我们先测试不加锁情况下会发生什么,单例模式类不改变,改动测试类的代码加入线程
class Test extends Thread{
public static void main(String[] args){
Thread thread_1 = new Test();
Thread thread_2 = new Test();
thread_1.start();
thread_2.start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "=" + Singleton.getInstance());
}
}
显示打印结果
Thread-1=Thread.Singleton@470bc213
Thread-0=Thread.Singleton@5eca2b42
明显发现,两个线程会去实例化俩个不同的对象(有时会返回相同对象,这是电脑本身计算问题,不妨多运行几次,会发现不同),如果这样的话我们的单例模式就没有存在的意义,不符合单例模式的定义:用来创建独一无二的,只能有一个实例对象,确保一个类只有一个实例,并提供一个全局访问点。需要做出更改,给 getInstance() 方法加锁看会不会有所改变,能否解决此类问题
更改单例模式类
public class Singleton {
private static Singleton uniqueInstance;
Singleton() {
}
// 加入关键字 synchronized 上锁
public synchronized static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
显示打印结果
Thread-0=Thread.Singleton@79d62897
Thread-1=Thread.Singleton@79d62897
经过多组测试,发现每次都只是返回同一个对象,感兴趣不妨可以自己测试,多添加几个线程。可以解决在多线程情况下的单例模式问题。
但是仔细想这样是会很好解决问题吗?其实并不是,学习线程我们知道,加锁代价高,如果是单纯的读操作是不需要加锁的,涉及写操作才需要加锁,使用 synchronized 需要仔细考虑,优化锁。仔细看一下代码,这段代码我们将 synchronized 关键字放在了方法上。这样的话不论哪个一个线程访问都会进行上锁解锁,其实这一步只有第一次访问时需要,其他时候访问已经有了实例化对象,就不需要再加锁,这时候的锁就造成浪费,优势变成了累赘。
更改多线程解决方式,为其添加双重锁
public class Singleton {
private volatile static Singleton uniqueInstance;
Singleton() {
}
public static Singleton getInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
分析双重锁之前,会发现增加了新的关键字 volatile ,这方面知识需要我们恶补 JVM 的知识,我有关于该关键字介绍,附链接方式
更改 getInstance() 方法,判断 uniqueInstance 是否为空,改为锁前判断一次,锁后判断一次。不要以为锁前锁后都进行判断空是多余的操作,每一步都有存在的必要性。
首先锁前检查,如果需要的对象为空,进行加锁,去实例化一个对象,相反如果不为空,检查到已经存在一个需要的对象,就不要再进行加锁了,直接返回需要的对象。省掉了代价过高的加锁操作。锁前检查为了判断读写操作。
锁后检查,很多小伙伴会这想之前已经为空了,再加锁判断为空是否多余。并不是你们想的那样,这两次的判断存在的性质不同,第一次判断读写,第二次是检查线程问题。可以编写一个测试类查看运行结果是不是自己想要的。
不使用第二个判断的运行结果
Thread-1=Thread.Singleton@352ebfc0
Thread-2=Thread.Singleton@352ebfc0
Thread-0=Thread.Singleton@3c03e5f5
Thread-3=Thread.Singleton@352ebfc0
开启四个线程,发现其中一个线程实例化了一个新对象,这时不符合单例模式的定义。当我们调整代码,加上第二次判断执行多次后,每次执行结果,不论哪个线程都是使用的同一个对象,这时才是真正的实现多线程的单例模式
总结
单例模式的实现相比之前学习的工厂模式较为简单,在学习之前我们先要对线程知识有一定了解,从而解决多线程中的单例问题。在单线程环境下我们不用考虑是否会创建多个对象,只有一个住线程,第一次执行方法为空,实例化一个对象,在整个代码运行期间,多次执行实例化方法时,第一次执行已经实例化一个对象,所以在之后调用该方法直接返回一个对象,不需要实例化一个新对象,从而实现单例模式。在多线程环境下,我们需要加锁来实现多线程环境下的单例模式,使用双重检查加锁来实现,避免直接在方法上加锁,造成资源浪费。第一次判断读写操作,第二次判断解决线程问题。
需要注意的是在 Java 语言中双重检查加锁不适用于 1.4 及更早版本,原因是关键字 volatile 发挥的作用,我有学习关键字 volatile 相关的记录,会附链接方式