设计模式-单例模式及案例

单例模式

  • 单例模式:用来创建独一无二的,只能有一个实例对象,确保一个类只有一个实例,并提供一个全局访问点。

通俗说单例模式只将对象 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 相关的记录,会附链接方式

    原文作者:设计模式
    原文地址: https://segmentfault.com/a/1190000017767031
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞