(设计模式学习笔记)单例模式与线程安全

单例模式的动机

对于一个软件系统的某些类而言,我们无须创建多个实例。例如windows的任务管理器,当我们多次点击的时候只能打开一个窗口。为什么要这样做呢?第一,打开进程管理器时会进行一些计算,消耗系统资源,如果每次都重新打开一个窗口,会占用较多的系统资源,毕竟浪费可耻。第二,如果弹出的多个窗口内容不一致,问题就更加严重了,这意味着在某一瞬间系统资源使用情况和进程、服务等信息存在多个状态,例如任务管理器窗口A显示“CPU使用率”为10%,窗口B显示“CPU使用率”为15%,到底哪个才是真实的呢?这纯属“调戏”用户,给用户带来误解,更不可取

单例模式的三个要点

实现单例模式主要有三点:1. 一个类只能有一个实例;2.它必须自行创建这个实例;3.它必须自行向整个系统提供这个实例。

ex:

class TaskManager  
{  
     private static TaskManager tm = null;  
     private TaskManager() {……}
     public static TaskManager getInstance()  
     {  
        if (tm == null)  
        {  
            tm = new TaskManager();  
        }  
        return tm;  
    }  
   ……  
}

饿汉单例模式与懒汉单例模式

在实际运用中,多次调用单例类时,可能会出现多个实例的情况。例如:单例模式类LoadBalancer,a、b进程同时调用LoadBalancer::getLoadBalancer实例化,a进程判断instance === null为真,然后执行instance = new LoadBalancer()操作,在实例化时可能会有很多操作需要一些时间。此时b进程也执行LoadBalancer::getLoadBalancer,由于instance此时还未创建成功,instance === null 为真,b进程执行instance = new LoadBalancer()。导致最终创建了多个instance对象,这违背了单例模式的初衷,也导致系统运行发生错误。

public class LoadBalancer {
    private static LoadBalancer instance = null;

    private LoadBalancer() {
    }

    public static LoadBalancer getLoadBalancer() {
        if (instance == null) {
            instance = new LoadBalancer();

        }
        return instance;
    }

}
public class Client {
    private static int lent = 100;
    public static void main(String args[]) {
        for (int i = 0; i < lent; i++) {
            Thread t = new Thread(() ->
            {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(LoadBalancer.getLoadBalancer().hashCode());
            });
            t.start();

        }
    }
}

执行client代码,显示结果如下:

[外链图片转存失败(img-esa2Y2A0-1568775492995)(/Users/hubobo/Desktop/image-20190918094134590.png)]

结果显示创建了多个instance实例,那么如何才能避免创建多个实例呢?

  1. 饿汉单例类

    public class LoadBalancer {
        private static final LoadBalancer instance = new LoadBalancer();
    
        private LoadBalancer() {
        }
    
        public static LoadBalancer getLoadBalancer() {
            return instance;
        }
    
    }
    

    当类被加载时,静态变量instance会被初始化,此时私有构造函数被调用,单例的唯一实例将被创建。保证了不会同时创建多个实例。实验结果如下:

    [外链图片转存失败(img-NUPZYDGW-1568775492996)(/Users/hubobo/Desktop/image-20190918095223512.png)]

    并没有出现多个实例的情况。

  2. 懒汉式单例类
    为了避免多线程同时调用getLoadBalancer,我们可以使用synchronized,代码如下:

    public class LoadBalancer {
        private static LoadBalancer instance = null;
    
        private LoadBalancer() {
        }
    
        synchronized public static LoadBalancer getLoadBalancer() {
            if (instance == null) {
                instance = new LoadBalancer();
    
            }
            return instance;
        }
    
    }
    

    对getLoadBalancer()方法加线程锁虽然保证了线程安全,但是在多线程并发环境中性能将大打折扣。那么如何才能在保证线程安全的同时又不影响性能呢?我们只需要对new LoadBalancer()加锁,改动后的代码如下:

    public class LoadBalancer {
        private static LoadBalancer instance = null;
    
        private LoadBalancer() {
        }
    
        public static LoadBalancer getLoadBalancer() {
            if (instance == null) {
                synchronized (LoadBalancer.class) {
                    instance = new LoadBalancer();
                }
            }
            return instance;
        }
    
    }
    

    实验结果如下:

    [外链图片转存失败(img-ZP4XE5Z5-1568775492997)(/Users/hubobo/Desktop/image-20190918101417413.png)]

    结果并不如预期,还是出现了多个实例的情况,是什么原因导致了这种情况呢?原因如下:

    线程a和b同时调用getLoadBalancer(),此时instance对象为null,均能通过instance == null的验证。由于实现了synchronized加锁机制,a线程执行实例创建代码,b线程进入排队等待状态。等待a执行完成,b线程并不知道实例已被创建,将继续创建实例,导致创建多个实例。违反了单例模式的设计思想,因此需要进一步改进,在synchronized中再进行一次instance==null的判断,这种判断称为双重检验锁定(double-check locking)示例如下:

    public class LoadBalancer {
        private volatile static LoadBalancer instance = null;
    
        private LoadBalancer() {
        }
    
        public static LoadBalancer getLoadBalancer() {
            if (instance == null) {
                synchronized (LoadBalancer.class) {
                    if (instance == null) {
                        instance = new LoadBalancer();
                    }
                }
            }
            return instance;
        }
    
    }
    

一种更好的单例实现–IoDH

饿汉式单例模式不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例模式线程安全控制烦琐,而且性能受影响。那么有没有一种方法能兼具二者的优点呢?下面我们来学习这种更好的技术–Initialization Demand Holder (IoDH)

public class LoadBalancer {
    private static class InstanceClass {
        private final static LoadBalancer instance = new LoadBalancer();
    }

    private LoadBalancer() {
    }

    public static LoadBalancer getLoadBalancer() {
        return InstanceClass.instance;
    }

}

由于静态单例对象并没有作为LoadBalancer的成员变量直接实例化,因此类加载时不会实例化LoadBalancer,第一次调用getLoadBalancer时将加载内部类InstanceClass,该内部类定义了一个静态变量instance,此时会首先初始化这个成员变量,由java虚拟机来保证其线程安全,确保该成员变量只能初始化一次,由于getLoadBalancer没有任何线程锁定,因此其性能不会有任何影响。

单例模式总结

单例模式的主要优点:

  1. 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
  2. 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
  3. 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。

单例模式的主要缺点如下:

  1. 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难
  2. 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起
  3. 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

你可能感兴趣的:(设计模式学习笔记)