单例设计模式

设计模式是一种思想,适合于任何一门面向对象的语言。共有23种设计模式。
一、【单例模式详解】
单例设计模式所解决的问题就是:保证类的对象在内存中唯一。

作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类成为单例类。


单例模式的特点:

  • 单例类只能有一个实例;
  • 单例类必须自己创建自己的唯一实例;
  • 单例类必须给所有其他对象提供这里实例。

A、B类都想要操作配置文件信息Config.java,所以在方法中都使用了Config con=new Config();但是这是两个不同的对象。对两者的操作互不影响,不符合条件。
解决思路:
1.不允许其他程序使用new创建该类对象。(别人new不可控)
2.在该类中创建一个本类实例。
3.对外提供一个方法让其他程序可以获取该对象。

解决方法:单例模式。
步骤:
1.私有化该类的构造函数
2.通过new在本类中创建一个本类对象。
3.定义一个共有的方法将创建的对象返回。

==================================================================
(1)饿汉式
雏形:

class Single 
{
    private static final Single s=new Single();
    private Single(){}
    public static Single getInstance()
    {
        return s;
    }
}

为什么方法是静态的:不能new对象却想调用类中方法,方法必然是静态的,静态方法只能调用静态成员,所以对象也是静态的。

为什么对象的访问修饰符是private,不能是public 吗?不能,如果访问修饰符是Public,则Single.s也可以得到该类对象,这样就造成了不可控。
举例:

public class Single 
{
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    private static final Single s=new Single();
    private Single(){}
    public static Single getInstance()
    {
        return s;
    }
}
public class Main {
    public static void main(String args[])
    {
        Single s=Single.getInstance();
        s.setName("张三");
        System.out.println(s.getName());
    }
}

最后结果为:张三

饿汉式其实是一种比较形象的称谓。既然饿,那么创建对象实例的时候就比较着急,饿了嘛,于是在装载类的时候就创建对象实例。

private static final Single s=new Single();

饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,不管你用不用,先先创建出来,然后每次调用的时候,就不需要在判断,节省了运行时间。

==================================================================
(2)懒汉式
前面的是饿汉式单例模式,下面开始讲解懒汉式单例模式。

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

懒汉式其实一种比较形象的称谓。既然懒,那么在创建对象实例的时候就不着急,会一直等到马上要使用对象实例的时候才会创建,懒人嘛,总是推脱不开的时候才会真正去执行工作,因此在装载对象的时候不创建对象实例

private static Single s=null;

懒汉式是典型的时间换空间,就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
由于懒汉式是线程安全的,这样会降低整个访问的速度,而且每次要判断。

我们可以看到懒汉式和饿汉式相比的区别就是懒汉式创建了延迟对象同时饿汉式的实例对象是被修饰为final类型。

懒汉式的好处是显而易见的,它尽最大可能节省了内存空间。

但是懒汉式又有着弊端,在多线程编程中,使用懒汉式可能会造成类的对象在内存中不唯一,虽然通过修改代码可以改正这些问题,但是效率却又降低了。而且如果想要使用该类对象,就必须创建对象,所以虽然貌似使用懒汉式有好处,但是在实际开发中使用的并不多。

总结:

懒汉式在面试的时候经常会被提到,因为知识点比较多,而且还可以和多线程结合起来综合考量。

饿汉式在实际开发中使用的比较多。

==================================================================
(3)懒汉式在多线程中的安全隐患以及解决方案、优化策略。
下面分析懒汉式在多线程中的应用和出现的问题以及解决方法。

懒汉式在多线程中出现的问题:

懒汉式由于多加了一次判断

if(s==null)

导致了线程安全性隐患。因为CPU很有可能在执行完if语句之后切向其它线程。解决线程安全性问题的关键就是加上同步锁。
1.使用同步函数
我们可以直接使用同步函数:

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

但是直接使用同步函数的方法效率十分低下,因为每次调用此方法都需要先判断锁。
2.使用同步代码块
我们也可以使用同步代码块:

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

但是每次调用getInstance方法仍然会判断锁,事实上没有改变效率问题。

3.最终解决方案

我们可以使用另外一种方式,达到只判断一次锁,并且实现同步的目的:

class Single
{
    private static Single s=null;
    private Single()
    { 7     }
    public static  Single getInstance()
    {
        if(s==null)//和上面的相比只是多增加了一次判断
        {
            synchronized(Single.class)
            {
                if(s==null)
                    s=new Single();
                return s;
            }
        }
        return s;
    }
}

观察代码可以发现和上面的代码相比,只是增加了一次判断而已,但是,这一次判断却解决了效率问题。

我们可以分析一下这个代码:
4.最终解决方案代码分析和总结

假设我们现在并没有创建单例对象,即snull,那么我们调用getInstance方法的时候,会进入if块,然后进入同步代码块,此时,别的线程如果想要创建Single实例,就必须获取锁;等当前线程创建完实例对象,释放锁之后,假设正巧有几个线程已经进入了if块中,它们会拿到锁,进入同步代码块,但是由于进行了判空操作,所以不会创建Single实例,而是直接返回已经创建好的Single实例。如果有多个其他线程进入了if块,当它们依次进入同步代码块的时候,同理也不会创建新的Single实例。而没有进入if块的线程,判空操作之后不满足条件,进不了if块,而直接执行了下一条语句return s;其后的线程调用getInstance方法时,只会判断一次snull,不满足条件直接返回Single单例s,这样就大大提高了了执行效率。

if(s==null)
        {
            synchronized(Single.class)
            {
                if(s==null)
                    s=new Single();
                return s;
            }
        }
        return s;

总结:在代码中,第一行代码是第一次判空操作,目的是提高效率;第三行代码是同步代码块的入口,目的是保证线程安全;第五行代码进行第二次判空操作是为了保证单例对象的唯一性

5.使用“双重检查加锁”(Java1.5以上使用 最终解决方案)
使用“双重检查加锁”的方式来实现,就可以实现线程安全,又能使性能不受很大的影响。那么什么叫“双重检查加锁”机制????
所谓的“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,二十先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的的同步块,这是第一重检查,进入同步代码块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次,从而减少了多次在同步情况CIA进行判断所浪费的时间。
“双重检查加锁”机制的实现还会使用关键字volatile,他的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
注意:在Java1.4及以前的版本中,很多JVM对volatile关键字的实现是有问题的,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只能用在Java5及以上版本。

class Single
{
    private static Single s=null;
    private Single()
    { 7     }
    public volatile static  Single getInstance()
    {
    //先检查实例是否存在,如果不存在才进入下面的同步块
        if(s==null)
        {
              //同步块,线程安全的创建实例
            synchronized(Single.class)
            {
                // 再次检查实例是否存在,如果不存在才真正的创建实例
                if(s==null)
                    s=new Single();
                return s;
            }
        }
        return s;
    }

这种实现方式既可以实现线程安全的创建实例,而又不会对性能造成太大的影响。他只是第一次创建实例的时候同步,以后不需要同步了,从而加快了运行速度。

**提示:**由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不一定很高。因此一般建议,没有特别需要不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

根据上面的分析,常见的两种单例实现方式都存在小小的缺陷,那么有没有一种方案,既能实现延迟加载,又能实现线程安全吗????

=================================================================
Lazy initalization holder class 惰性初始化holder类
这个模式综合了Java的类级内部类和多线程缺省同步锁知识,很巧妙的同时实现了延迟加载和线程安全。
1.相应的基础知识
1.1 什么是类级内部类??
简单的说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。

类级内部类相当于其外部类的static成分,他的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,在绑定在外部对象实例中的。

类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。

类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被装载。

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

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

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

示例如下:

public class Singleton{
    private Singleton(){
        //类级的内部类,也就是静态的成员内部类,该内部类的实例和外部类的实例。
            private static class SingletonHoler{
                //静态初始化器,由JVM来保证线程安全
                private static Singleton instance = new Singleton();
              }
     public static Singleton getInstance(){
          return SingletonHolder.instance;
      }
 }

当getlnstance方法第一次被调用的时候,他第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化他的静态域,从而创建singelen的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证他的线程安全性。

这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

==================================================================
单例和枚举
单元数的枚举类型已经成为实现Singeton的最佳方法。用枚举来实现单例非常简单,只需要编写一个包含单一元素的枚举类型即可。

public enum Sington{
      //定义一个枚举的元素,他就代表了Singeton的一个实例
      uniqueInstance;
      //单例可以有自己的操作
      //功能处理
      }
 }
      

使用枚举来实现单实例控制会更加简洁,而且无偿的提高了序列化机制,并有JVM从根本上提供保障,绝对防止对此实例化,是更简洁,高效,安全的实现单例的方式。

  • 16
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值