多线程--单例模式

单例模式是23种设计模式中比较简单的模式之一,本博客较为详细的梳理了该设计模式,并实现该模式。

问题由来 : 我们为什么需要单例模式?

在许多时候,一个系统只需要一个全局对象,这样有利于我们协调系统的整体行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这样做可以简化对系统的管理,并且可以避免出现不一致的状况。

如何解决 : 单例模式的概念

在Java中,单例模式就是确保一个类中只有一个实例,并且它可以提供一个全局的访问点以访问该实例。它可以为整个系统提供一个全局访问点,避免了(不必要的)频繁的创建与销毁对象,可以提高系统性能,节省资源,并且便于整体把控系统。

概念模型 : 在java中建立单例模式的模型

在Java中,万物皆对象,那么,如何将单例模式的概念抽象为Java中的对象呢?

将单例模式看作一类对象,则这类对象需要有三大要素:

  • 指向自己(唯一)实例的私有静态引用 :因为只有一个唯一的实例,所以用静态引用
  • 私有的构造方法 : 避免外部创建实例对象
  • 公有的以自己(唯一)实例为返回值的静态方法:提供一个访问该实例的方法

类的实现 :

单例模式的实现有两种比较经典的实现方法

  1. 饿汉模式(立即加载模式):在加载该类的时候就实例化对象
  2. 懒汉模式(延迟加载模式):在真正使用的时候才实例化对象

单线程实现 :

饿汉模式 :

class Hunger_Thread{
    // 指向自己实例的私有静态引用,
    // 在类加载时便new了一个对象(类只会加载一次,所以只会new一个对象)
    private static Hunger_Thread hunger = new Hunger_Thread();

    // 私有构造方法
    private Hunger_Thread(){};

    // 以自己实例为返回值的静态公有方法
    public static Hunger_Thread getHunger(){
        return hunger;
    }
    
}

懒汉模式 :

class Lazy_Thread{
    // 指向自己实例的私有静态引用
    private static Lazy_Thread lazy;

    // 私有构造方法
    private Lazy_Thread(){}

    // 以自己实例为返回的静态公有方法
    public static Lazy_Thread getLazy(){
        if(lazy==null){
            lazy = new Lazy_Thread();
        }
        return lazy;
    }
}

多线程实现 :

多线程需要考虑线程安全问题 ,由于饿汉模式(不管是单线程还是多线程)只会在类加载时new对象,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例,故它是线程安全的。

但懒汉模式会出现线程不安全的例子 :(就拿单线程的懒汉代码为例)

假设有线程 t1,t2,这两个线程都调用了getLazy()方法(lazy==null,t1先执行),t1执行到 if(lazy==null){...}(刚判断完,还未创建实例)时,时间片用完了,此时该t2执行,t2完整的执行了该方法(已经创建了实例),又该t1执行了,此时它并不知道已经创建了实例,所以还会创建一个,这就与该模式的设计理念不符,也就是线程不安全。

解决方法也很简单,加锁就可以了

class Lazy_Thread{
    // 指向自己实例的私有静态引用
    private static Lazy_Thread lazy;

    // 私有构造方法
    private Lazy_Thread(){}

    // 以自己实例为返回的静态公有方法
    public static synchronized Lazy_Thread getLazy(){
        if(lazy==null){
            lazy = new Lazy_Thread();
        }
        return lazy;
    }
}

这样可以解决线程安全问题,但是效率低下,锁的粒度太大。下面的代码段与上面的相比没什么效率的提升。

class Lazy_Thread{
    // 指向自己实例的私有静态引用
    private static Lazy_Thread lazy;

    // 私有构造方法
    private Lazy_Thread(){}

    // 以自己实例为返回的静态公有方法
    public static Lazy_Thread getLazy(){
        synchronized (Lazy_Thread.class){
            if(lazy==null){
                lazy = new Lazy_Thread();
            }
        }
        return lazy;
    }
}

一般,我们会使用双重检查来实现线程安全,这是一种比较高效的做法。

class Lazy_Thread{
    // 要用 volatile 修饰,防止指令重排序
    private static volatile Lazy_Thread lazy;

    private Lazy_Thread(){}

    // 双重检查实现线程安全
    public static Lazy_Thread getLazy(){
        if(lazy==null){
            // 只有第一次创建实例时才加锁
            synchronized (Lazy_Thread.class){
                if(lazy==null){
                    lazy = new Lazy_Thread();
                }
            }
        }
        return lazy;
    }
}

为什么要用 volatile 修饰 lazy ?

 new AnyClass();是一个非原子操作 ,大致会分为这三步

  1. 分配对象的内存空间
  2. 初始化对象
  3. 将该对象的引用指向(第一步)分配好的内存空间

如果发生了指令重排序,则有可能把步骤3放在步骤2之前执行(lazy不为null,但是它没有初始化,它就是一个残缺的对象)。

假设线程t1在执行new的时候发生了指令重排序,new的步骤变成了1,3,2,而t1在执行完3后时间片刚好用完(此时 lazy 就是一个残缺的对象)这时,t2进入线程,判断 lazy != null 之间返回的残缺的对象,这也是线程不安全的!

你可能感兴趣的:(多线程,单例模式)