女朋友:你能给我讲讲单例模式吗?

问题背景:

某公司老板在招程序员时承诺帮助解决单身问题,给程序员分配一个女朋友,于是单身的小强毫不犹豫就去应聘了,并被顺利录用。那么我们怎么用代码来模拟一下呢?首先定义一个女朋友的类,拥有两个属性,姓名和年龄:

public class GirlFriend {
    private String name;
    private Integer age;

    public GirlFriend(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "GirlFriend{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

接着程序员小强就可以new出来一个女朋友的实例了,只需要传进去姓名和年龄就可以了,如下:

public class Programmer {
    public static void main(String[] args){
        GirlFriend girlFriend = new GirlFriend("小美",20);
        System.out.println(girlFriend.toString());
    }
}

打印出的结果是GirlFriend{name='小美', age=20}

有何问题:

突然有一天,程序员小强已经不满足只有一个女朋友了,于是他私自new出了多个女朋友对象出来,如下:

public class Programmer {
    public static void main(String[] args){

        GirlFriend girlFriend = new GirlFriend("小美",20);
        GirlFriend girlFriend2 = new GirlFriend("小红",18);
        GirlFriend girlFriend3 = new GirlFriend("小丽",19);
        System.out.println(girlFriend.toString());
        System.out.println(girlFriend2.toString());
        System.out.println(girlFriend3.toString());
    }
}

打印结果如下:

GirlFriend{name='小美', age=20}
GirlFriend{name='小红', age=18}
GirlFriend{name='小丽', age=19}

但是不久就被老板发现了,因为内存中存在多个女朋友实例对象,严重浪费了公司的资源,老板决定只能给小强分配一个女朋友,老板绞尽脑汁,终于想出了应对方法。
更多内容请关注我的weixin公号【程序员修炼】

解决方法:

老板发现,问题的根源就是不能把创造女朋友的权限交给小强,应该给他一个创造好的对象,并且姓名和年龄也不能由小强来决定,不然他肯定只要18岁的。而是要把创建实例的权限收回,让类自身负责自己类实例的创建。

来看看代码如何实现:

public class GirlFriend {
    private String name;
    private Integer age;
    //定义一个变量来存储创建好的类实例
    private static GirlFriend girlFriend = null;

    //私有化构造方法,防止外部调用
    private GirlFriend(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
    //定义一个方法为程序员类提供女朋友实例
    public static GirlFriend getGirlFriend(){
        //判断存储实例是否为空
      if(girlFriend==null){
          //如果没值,就new出一个实例,并赋值给存储实例的变量
          girlFriend = new GirlFriend("小美",28);
      }
      return girlFriend;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "GirlFriend{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

}

主要的核心思想有三点:

  • 定义一个变量来存储创建好的类实例
  • 私有化构造函数,防止外部new该对象
  • 对外提供一个能获取到该对象的方法

程序员小强该如何获取呢:

public class Programmer {
    public static void main(String[] args){
       GirlFriend girlFriend = GirlFriend.getGirlFriend();
        System.out.println(girlFriend.toString());
    }
}

直接使用GirlFriend类调用获取对象的getGirlFriend方法获取到实例对象,打印结果如下:

GirlFriend{name='小美', age=28}

模式讲解:

上面这种解决方案就是单例模式(Singleton),单例模式定义:

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

通用类图如下:

image

Singleton:负责创建单例类自己的唯一实例,并提供一个getInstance的方法让外部来访问这个类的唯一实例。

单例模式功能:保证类在运行期间只允许被创建一个实例。有懒汉式和饿汉式两种实现方式。

懒汉式:上面的代码就是懒汉式的实现方式,顾名思义,懒汉式指只有当该实例被使用到的时候才会创建,通过三个步骤就可以实现懒汉式:

  • 私有化构造方法:防止外部使用。
  • 提供获取实例的方法:全局唯一的类实例访问点。
  • 把获取实例的方法改为静态:因为只有静态的方法才能直接通过类名来调用,否则就要通过实例调用,这就陷入了死循环。

完整代码:

public class Singleton{
    //定义变量存放创建好的实例,因为要在静态方法中使用,所以变量也必须是静态的
    private static Singleton uniqueInstance = null;
    //私有化构造方法,可以在内部控制创建实例的数目
    private Singleton(){
        
    }
    //定义一个方法为客户端提供类实例,synchronized同步保证线程安全
    public static synchronized Singleton getInstance(){
        //判断是否已经有实例
        if(uniqueInstance == null){
            uniqueInstance = new Singleton();
        }
        //有就直接用
        return uniqueInstance;
    }
}

这里使用到了synchronized用来保证线程安全,如果不加会带来什么问题呢?比如两个线程A和B,就有可能导致并发的问题,如图所示:

image

这种情况就会创建出两个实例出来,单例模式也就失效了。加上synchronized虽然能保证线程安全,但是却降低了访问速度,影响了性能,可以考虑使用双重检查加锁来解决这个问题,双重检查加锁意思是并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法之后先检查实例是否存在,如果不存在才进入同步块。这是第一重检查。进入同步块之后再检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。

代码实现:

public class Singleton{
    //对保存实例的变量添加volatile修饰
    private volatile static Singleton instance = null;
    private Singleton(){
        
    }
    public static Singleton getInstance(){
        //第一次检查
        if(instance == null){
            //同步块,线程安全的创建实例
            synchronized (Singleton.class){
                //第二次检查
                if(instance==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这种方式即可以安全的创建线程,又不会对性能造成太大的影响。

饿汉式:所谓饿汉式也就是在类加载的时候直接new出一个对象来,不管以后用不用得到,是一种以空间换取时间的策略。代码也非常简单:

public class Singleton{
    //定义一个变量来存储创建好的实例,直接在这里创建实例,只能创建一次
    //static变量在类加载时进行初始化,并且只被初始化一次。
    private static Singleton uniqueInstance = new Singleton();
       //私有化构造方法,可以在内部控制创建实例的数目,防止在外部创建
    private Siingleton(){
        
    }
    //定义一个方法为客户端提供类实例,方法上加static将该方法变为静态
    //目的是不需要对象实例就可以在外部直接通过类来调用
    public static Singleton getInstance(){
        //直接使用已经创建好的实例
        return uniqueInstance;
    }
}

单例模式作用范围:目前Java里面实现的单例是一个虚拟机的范围,虚拟机在通过自己的ClassLoader装载饿汉式实现的单例类时就会创建一个类实例。如果一个虚拟机中有多个类加载器或者一个机器中有多个虚拟机,那么单例就不再起作用了。

单例模式优缺点:

1.节约内存资源

2.时间和空间:懒汉式是以时间换空间,饿汉式是以空间换时间。

3.线程安全:不加同步synchronized的懒汉式是线程不安全的,而饿汉式是线程安全的,因为虚拟机只会装载一次,并且在装载的时候是不会发生并发的。加上synchronized和双重检查加锁也能保证懒汉式的线程安全。

新的问题:

由于小强工作很卖命,公司业绩发展的不错,老板决定再招一名程序员,应聘者小华也是一个单身汉,老板也承诺会给他分配女朋友,单是问题来了,之前的GirlFriend只能new出一个对象,总不能让小强和小华共用一个对象吧。于是要想办法实现一个可以提供两个实例的GirlFriend类。

更多内容请关注我的weixin公号【程序员修炼】

其实思路很简单,只需要通过Map来缓存实例即可,代码如下:

import java.util.HashMap;
import java.util.Map;

public class GirlFriend {
    private String name;
    private Integer age;
    private static int maxNumsOfGirlFriends = 2;//最大数量
    private static int number = 1;//当前编号
    //定义一个变量来存储创建好的类实例
    private static Map girlFriendMap = new HashMap();

    //私有化构造方法,防止外部调用
    private GirlFriend(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
    //定义一个方法为程序员类提供女朋友实例
    public static GirlFriend getGirlFriend(){

        GirlFriend girlFriend = girlFriendMap.get(number+"");
        if(girlFriend==null){
            //new一个新实例,并放到map中,用number当做key,实例是value
          girlFriend = new GirlFriend("小美",28);
          girlFriendMap.put(number+"",girlFriend);
        }
        number++;
        if(number>maxNumsOfGirlFriends){
            number = 1;
        }
        return girlFriend;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

程序员类进行获取女朋友实例,如下:

public class Programmer {
    public static void main(String[] args){
       GirlFriend girlFriend1 = GirlFriend.getGirlFriend();
        System.out.println(girlFriend1);
        GirlFriend girlFriend2 = GirlFriend.getGirlFriend();
        System.out.println(girlFriend2);
        GirlFriend girlFriend3 = GirlFriend.getGirlFriend();
        System.out.println(girlFriend3);
        GirlFriend girlFriend4 = GirlFriend.getGirlFriend();
        System.out.println(girlFriend4);
    }
}

上面代码获取了四次,看看打印的结果如何:

GirlFriend@6e0be858
GirlFriend@61bbe9ba
GirlFriend@6e0be858
GirlFriend@61bbe9ba

可以看错,第一次和第三次是一样的,第二次和第四次是一样的,一共就只有两个对象,解决了这个问题。但是如何判断哪个女朋友实例是小强的哪个是小华的呢?一种简单的方法是通过给获取实例的函数getGirlFriend传参,比如小强获取的时候传如number = 1,小华的number = 2。

相关扩展:

在Java中还用一种更好的单例实现方式,既能够实现延迟加载,又能够实现线程安全。这种解决方案被称为Lazy initialization holder class模式,这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。

类级内部类:

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

缺省同步锁:在某些情况下,JVM已经隐含地执行了同步,不需要自己进行同步控制了,这些情况包括:

  • 由静态初始化器初始化数据时。
  • 访问final字段时
  • 在创建线程之前创建对象时
  • 线程可以看见它将要处理的对象时。

思路:使用静态初始化器的方式,由jvm保证线程安全。但是这样就像饿汉式的实现方式了,浪费一定的空间。采用类级内部类,在这个类级内部类里面创建对象实例,只要不使用这个类级内部类,就不会创建实例对象。

代码:

public class Singleton{
    //类级内部类,该内部类的实例与外部类的实例没有绑定关系,
    // 而且只有被调用到时才会装载,从而实现延迟加载
    private static class SingletonHolder{
        //静态初始化器,由jvm保证线程安全
        private static Singleton instance = new Singleton();
    }
    private Singleton(){
        
    }
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

getInstance方法第一次被调用的时候,它第一次读取 SingletonHolder.instance导致SingletonHolder类得到初始化,从而创建Singleton实例。

枚举实现单例:

  • Java的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法。
  • Java枚举类型的基本思想是通过共有的静态fianl域为每个枚举常量导出实例的类。
  • 从某种角度将,枚举是单例的泛型化,本质上是单元素的枚举

代码:

public enum Singleton{
    uniqueInstance;
    //单例自己的操作函数
    public void singletonOperation(){
        //功能处理
    }
}

使用枚举来实现单例控制更加简洁,而且无偿地提供了序列化的机制,并由JVM从根本上提供保障,绝对防止多次实例化。

在Spring中,每个Bean默认就是单例的,这样的优点是Spring容器可以管理这些Bean的生命周期,决定什么时候创建出来,什么时候销毁,销毁的时候如何处理等等。

使用单例模式需要注意的就是JVM的垃圾回收机制,如果我们的一个单例对象在内存中长久不使用,JVM就认为这个对象是一个垃圾,在CPU资源空闲的情况下该对象会被清理掉,下次再调用时就需要重新产生一个对象。

你可能感兴趣的:(女朋友:你能给我讲讲单例模式吗?)