设计模式之单例模式

设计模式之单例模式

单例模式是设计模式中,关于生产对象类型的设计模式的一种!

单例模式的具体表现是不可以获取同一个类的多个对象,反复获取也只会得到同一个对象!

总的来说,饿汉式的特点是:刚刚加载时就创建了对象,而懒汉式的特点是:当第1次尝试获取对象时才创建对象!

单件模式用途:
单件模式属于工厂模式的特例,只是它不需要输入参数并且始终返回同一对象的引用。
单件模式能够保证某一类型对象在系统中的唯一性,即某类在系统中只有一个实例。它的用途十分广泛,打个比方,我们开发了一个简单的留言板,用户的每一次留言都要将留言信息写入到数据库中,最直观的方法是每次写入都建立一个数据库的链接。这是个简单的方法,在不考虑并发的时候这也是个不错的选择。但实际上,一个网站是并发的,并且有可能是存在大量并发操作的。如果我们对每次写入都创建一个数据库连接,那么很容易的系统会出现瓶颈,系统的精力将会很多的放在维护链接上而非直接查询操作上。这显然是不可取的。
如果我们能够保证系统中自始至终只有唯一一个数据库连接对象,显然我们会节省很多内存开销和 cpu 利用率。这就是单件模式的用途。

下面对单例模式饿汉式懒汉式进行简单介绍:
1、饿汉式:在程序启动或单件模式类被加载的时候,单件模式实例就已经被创建。
2、懒汉式:当程序第一次访问单件模式实例时才进行创建。

如何选择:
如果单件模式实例在系统中经常会被用到,饿汉式是一个不错的选择。
反之如果单件模式在系统中会很少用到或者几乎不会用到,那么懒汉式是一个不错的选择。

懒汉式在多线程中是不支持的,所以相对来说,更多的是用饿汉式
比如,现在有 A 线程和 B 线程,A 线程刚好在这个 getInstance()方法中,刚刚判断完非空(此时为 null),即需要创建实例,然而,就是这么巧,B 线程抢到了 CPU 的执行权,A 线程 sleep 了,这时,B 线程也进行了这个判断,和 A 一样,都需要创建实例,而这时,A 也抢到了 CPU,这时,B 就 sleep 了,然后 A 执行了实例化后,B 又抢到 CPU 执行权,然后 B 也实例化,这时,出现问题了,A 和 B 都实例化了一个对象,这就是两个对象,没有单例也不唯一。

1.饿汉式单例模式

假设需要将King类设计为单例的,首先,普通的类的代码例如:

public class King {
}

这样的类是可以随意创建对象的,例如:

King k1 = new King();
King k2 = new King();
King k3 = new King();

显然,随意创建对象是违背了单例模式的设计思想的,所以,为了避免随意对象,可以:

public class King {
  private King() {
    // 将构造方法私有,使之不可以在类的外部来创建对象
  }
}

当然,私有化构造方法的目的并不是“不允许创建对象”,而是“不允许随意创建对象”,为了保证在类的外部依然可以获取当前类的对象,还可以在类中添加获取对象的方法:

public class King {
  private King() {
    // 将构造方法私有,使之不可以在类的外部来创建对象
  }
  
  public King getInstance() {
    return new King();
  }
}

则外部通过调用getInstance()方法仍可以获取对象,但是,每调用一次,都会创建一个新的对象,所以,还需要进一步调整:

public class King {
  private King king = new King();
  
  private King() {
    // 将构造方法私有,使之不可以在类的外部来创建对象
  }
  
  public King getInstance() {
    return king; // 直接返回私有的全局属性
  }
}

由于在初始化时,private King king = new King();只会执行1次,反复调用getInstance()方法,获取到的都是同一个对象!这就符合了单例模式的设计思想!

但是,在以上代码中,如果没有King的对象,是无法调用getInstance()方法,而获取对象的唯一途径就是要调用这个方法!所以,为了解决这个矛盾,需要使用static修饰这个方法:(用类名.就可以调用方法了)

public class King {
  private King king = new King();
  
  private King() {
    // 将构造方法私有,使之不可以在类的外部来创建对象
  }
  
  public static King getInstance() {
    return king; // 直接返回私有的全局属性
  }
}

一旦在getInstance()方法的声明中添加了static关键字,还需要在private King king = new King();这个属性的声明中也补充static关键字,因为“static修饰的成员不可以直接访问没被static修饰的成员”,所以,代码还需要改成:

static修饰的成员不可以直接访问没被static修饰的成员:解释:static class装载的时候,就存在了。 而类的其他部分,变量,方法,都需要在类成为实例的时候才会建立。 static的生命周期长于非static的对象,所以static的成员如果访问无static的成员时。对方可能还未产生,会发生错误。所以禁止了。也就是说:静态的方法不能直接调用非静态的方法,要有对象之后才能调用非静态的方法

-----饿汉式单例模式全代码:

//饿汉式单例模式
public class King {
    
  private static King king = new King();
  	//自己创建一个类的实例化
    
  private King() {
    // 将构造方法私有,使之不可以在类的外部来创建对象
  }
  
  public static King getInstance() {
    return king; // 直接返回私有的全局属性
  }
}

至此,单例模式就设计完成!

同时,以上单例的代码是“饿汉式单例模式”

为什么单例对作用域是有影响的?

是因为设计为单例的对象,一定是有static修饰的,而static这个关键字,带来的编程体验是不需要new对象就可以调用它的成员,比如属性、方法,但是static成员是常驻内存的,一旦加载到内存中,只有整个程序完全结束之后,这个数据才会从内存中消失。而局部变量,只要方法调用完毕,这个变量的作用域就结束了。

所以被static修饰的成员,它的作用域是非常长的,而没有被static修饰的成员就不会一直在内存中,所以是否单例就会影响它是否在内存中存在非常长的时间,所以单例就影响到作用域。

2.懒汉式单例模式

另外,还有“懒汉式单例模式”,其特征是“不到逼不得已,不创建对象”!例如:

public class King {
  private static King king;
  
  private King() {
    // 将构造方法私有,使之不可以在类的外部来创建对象
  }
  
  public static King getInstance() {
    // 判断全局的king是否被创建了对象,如果没有,则创建
    if (king == null) {
      king = new King();
    }
    return king;
  }
}

但是,以上代码是存在线程安全风险的!当存在多个“同时”运行的线程时,是可能创建出多个对象的!为了解决线程安全问题,需要:

public class King {
  private static King king;
  private static final Object lock = new Object();
  
  private King() {
    // 将构造方法私有,使之不可以在类的外部来创建对象
  }
  
  public static King getInstance() {
    // 为了解决线程安全问题,需要锁上以下代码
    synchronized(lock) {
      // 判断全局的king是否被创建了对象,如果没有,则创建
      if (king == null) {
        king = new King();
      }
    }
    return king;
  }
}

但是,一旦加了锁,效率又会变得比较低下!因为每次调用以上方法都需要锁上代码才可以继续向后执行,在多线程的应用场景中,多个“同时”运行的线程在执行这段代码是互斥的,为了解决效率偏低的问题,还会进一步改进:

-----懒汉式单例模式全代码:

//懒汉式单例模式
public class King {
    
  private static King king;
    // 自己创建一个类的实例化
    
  private static final Object lock = new Object();
  	//创建一个锁对象
    
  private King() {
    // 将构造方法私有,使之不可以在类的外部来创建对象
  }
  
  public static King getInstance() {
    // 判断是否有必要锁住代码
    if (king == null) {
      // 为了解决线程安全问题,需要锁上以下代码
      synchronized(lock) {
        // 判断全局的king是否被创建了对象,如果没有,则创建
        if (king == null) {
          king = new King();
        }
      }
    }
    return king;
  }
}

至此,懒汉式的单例模式也就完成了!

总的来说,饿汉式的特点是:刚刚加载时就创建了对象,而懒汉式的特点是:当第一次尝试获取对象时才创建对象。

posted @ 2022-02-15 14:10  Charles博客  阅读(34)  评论(0编辑  收藏  举报