设计模式之单例模式

设计模式:不偏代码,纯偏思想,解决一类问题最行之有效的办法。

Java中有23种设计模式。

单例模式定义

单例模式,保证系统中一个类只有一个对象实例。
要保证对象唯一,单例模式有如下特点:
1。为避免其他对象过多建立实例对象,先禁止其他程序创建该类对象;
即将构造方法私有化;
2。 为让其他程序访问该类对象,可在本类中自定义一对象;
即在类中创建一个本类对象,该对象也是私有化的;
3。为方便其他程序对自定义对象的访问,对外提供访问方式;
即提供一个方法获取该对象。

动机

为什么使用单例模式

对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。

为什么不使用全局变量

1,全局变量可以实现全局访问,但是它不能保证应用程序中只有一个实例
2,编码规范建议少用全局变量
3,全局变量不能实现继承

哪些类是单例模式的候选类?在Java中哪些类会成为单例?

(1) 系统资源,如文件路径,数据库链接,系统常量等
(2)全局状态化类,类似AutomicInteger的使用

单例模式的优点:

在内存中只有一个对象,节省内存空间。
避免频繁的创建销毁对象,可以提高性能。
避免对共享资源的多重占用。
可以全局访问。

适用场景:

由于单例模式的以上优点,所以是编程中用的比较多的一种设计模式。我总结了一下我所知道的适合使用单例模式的场景:
需要频繁实例化然后销毁的对象。
创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
有状态的工具类对象。
频繁访问数据库或文件的对象。

实现方式

第一种方式:饿汉式

所谓的饿汉式,即在类一加载的时候就初始化对象,对于单线程来说,简单安全,使用方便。

class Singleton{
    private Singleton(){}
    //final可有可无,有了更好,
    private static final Singleton s=new Singleton();
    public static Singleton getInstance(){
        return s;
    }
}
优点

1.线程安全;:类加载时已创建实例,不存在线程安全的问题。
2.在类加载的同时已经创建好一个静态对象,调用时反应速度快

缺点

资源效率不高,可能getInstance()永远不会执行到,但执行该类的其他静态方法或者加载了该类(class.forName),那么这个实例仍然初始化

第二种方式:懒汉式

优点

资源利用率高,不执行getInstance()就不会被实例,可以执行该类的其他静态方法

缺点

第一次加载时不够快,多线程使用不必要的同步开销大
懒汉式单例模式,即对象在方法被调用时才初始化,这种方式也叫做对象的延时加载。

class Singleton{
    private Singleton(){}
    private static Singleton s;
    public static Singleton getInstance(){
        if(s==null){
            s=new Singleton();
        }
        return s;
    }
}

在多线程时会出现线程安全问题。如在if s==null处,A线程进来时,判断s为空,则准备new对象,在这时B线程也进来了,A对象还未new完,因此B也准备new对象,这样就不能保证Singleton的唯一性了。

解决方法:同步锁synchronized
//同步锁解决线程安全问题
class Singleton{
    private Singleton(){}
    private static Singleton s;
    public static synchronized Singleton getInstance(){
        if(s==null){
            s=new Singleton();
        }
        return s;
    }
}

由于懒汉式同步锁,每次都得判断锁,比较低效,因此将其变为同步代码块形式,即双重判断加同步锁解决线程安全

双重判断加同步锁解决线程安全
class Singleton{
    private Singleton(){}
    private static Singleton s;
    public static  Singleton getInstance(){
        if(s==null){
            synchronized (Singleton.class) {
                if(s==null)
                    s=new Singleton();
            }
        }
        return s;
    }
}

为什么使用双重判断呢?
如果两线程AB同时到达第一层判断,由于此时判空都为true,因此都会进入同步代码块,其中一个先进入同步,new对象,另一个阻塞等待前者释放锁,当前者释放后,另一个进入,如果没有第二层判空,则将再次new一个对象,不符合单例模式的唯一性,因此在同步代码块里面也加了一层判空条件。

这种方式是绝对的线程安全的么?!

其实不是的。这就涉及到了JVM的内存模型了。我们知道,由于硬件与操作系统之间内存访问的差异性,产生了JMM。变量都是存放在JVM的主内存中的,变量的操作都是在线程的工作内存中,在工作内存中进行运算之后再写回主内存的。
程序中s=new Singleton()这句话,它不是一个原子操作,其实是分为三个JVM指令的:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:

memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory); //2:初始化对象

我们可以看到,2是建立在1上的,和1有一定的依赖关系,而3是指向1的,和2之间不存在依赖关系,这就会导致变成字节码指令时会出现指令重排,即可能是1,2,3,也可能是1,3,2。

对于1,3,2.可能在3完成之后就将其写回主内存,导致其指向的地址不为空,当前线程还未进行2,或者2的结果还未返回到主内存,这时另外一个线程就得到了主内存中的s,结果发现不为空,就直接返回s.但实际上s的地址内是空的,这就会出现错误。因此,针对于这种情况,我们需要禁止指令重排。

重点

首先,同步块加锁,可以对对象实例加锁,也可以对类对象加锁,而单例模式中不对instance加锁的原因在于加锁时,instance还未初始化,如果对其加锁,会报空指针异常。
其次,应该注意的是使用内置锁加锁的是Singleton.class,并不是instance,也就是说没有在instance实现同步,那么在这种情况下,当有两个线程同时进行到synchronized代码块时,只有一个线程可以进入,然后初始化了instance,但是这仅仅只能保证的是两个线程在访问上的独占性,也就是说两个线程在此一定是一先一后进行访问,但是不能保证的是instance的内存可见性,原因很简单,因为同步的对象并不是instance,而是Singleton.class(可以保证内存可见性)。不能保证内存可见性的后果就是当第一个线程初始化instance之后,第二个线程并不能马上看见instance被初始化,或者更准确的来说,第二个线程看到的可能只是被部分构造的instance。因此,这种造成的后果是第二个线程读取到了错误的instance的状态,有可能instance会被再次实例化。

volatile关键字修饰

在Java中,关键字volatile不仅可以保持可见性,只要一被改动就立即写回内存,让其他线程看到改变,还可以禁止指令重排,因此我们可以将实例定义为volatile类型的,如下所示:

class Singleton{
    private Singleton(){}
    private static volatile Singleton s;
    public static  Singleton getInstance(){
        if(s==null){
            synchronized (Singleton.class) {
                if(s==null)
                    s=new Singleton();
            }
        }
        return s;
    }
}

Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。

有一种方式,既能实现饿汉式的特性,又能实现懒汉式的特性么? 有!内部类实现单例模式

内部类实现单例模式
-Lazy initialization holder class模式

这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。

相应的基础知识
什么是类级内部类?

  简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。
  类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。
  类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。
  类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。

多线程缺省同步锁的知识

  大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:
  1.由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
  2.访问final字段时
  3.在创建线程之前创建对象时
  4.线程可以看见它将要处理的对象时

  要想很简单地实现线程安全,可以采用静态初始化器的方式,它可以由JVM来保证线程的安全性。比如前面的饿汉式实现方式。但是这样一来,不是会浪费一定的空间吗?因为这种实现方式,会在类装载的时候就初始化对象,不管你需不需要。

  如果现在有一种方法能够让类装载的时候不去初始化对象,那不就解决问题了?一种可行的方式就是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,那就不会创建对象实例,从而同时实现延迟加载和线程安全。

class Singleton{
    private Singleton(){}
    //类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载。
    private static class SingletonHolder{
        // 静态初始化器,由JVM来保证线程安全 
        private static Singleton s=new Singleton();
    }
    public static Singleton getInstance(){
        return SingletonHolder.s;
    }
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值