• 当前位置: 首 页 > 教育百科 > 其他 > 正文

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

    :2020年01月01日
    脚本之家

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

    本文经授权转自公众号 程序员修炼(ID:lixing2457)

    作者:静幽水 

    如若转载请联系原公众号

    01

    问题背景

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

    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}

    02

    有何问题

    突然有一天,程序员小强已经不满足只有一个女朋友了,于是他私自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}

    但是不久就被老板发现了,因为内存中存在多个女朋友实例对象,严重浪费了公司的资源,老板决定只能给小强分配一个女朋友,老板绞尽脑汁,终于想出了应对方法。

    03

    解决方法

    老板发现,问题的根源就是不能把创造女朋友的权限交给小强,应该给他一个创造好的对象,并且姓名和年龄也不能由小强来决定,不然他肯定只要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 +

    '}';

        }

    }

    主要的核心思想有三点:

    1.定义一个变量来存储创建好的类实例;

    2.私有化构造函数,防止外部new该对象;

    3.对外提供一个能获取到该对象的方法。

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

    public class Programmer {

    public static void main(String[] args){

           GirlFriend girlFriend = GirlFriend.getGirlFriend();

            System.out.println(girlFriend.toString());

        }

    }

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

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

    04

    模式讲解

    上面这种解决方案就是单例模式(Singleton),单例模式定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    通用类图如下:

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

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

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

    1.私有化构造方法:防止外部使用。

    2.提供获取实例的方法:全局唯一的类实例访问点。

    3.把获取实例的方法改为静态:因为只有静态的方法才能直接通过类名来调用,否则就要通过实例调用,这就陷入了死循环。

    完整代码:

    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,就有可能导致并发的问题,如图所示:

    这种情况就会创建出两个实例出来,单例模式也就失效了。加上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和双重检查加锁也能保证懒汉式的线程安全。

    05

    新的问题

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

    其实思路很简单,只需要通过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。

    06

    相关扩展

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

    类级内部类:

    1.类级内部类指的是有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。

    2.类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可以直接创建。

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

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

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

    1.由静态初始化器初始化数据时。

    2.访问final字段时

    3.在创建线程之前创建对象时

    4.线程可以看见它将要处理的对象时。

    思路:使用静态初始化器的方式,由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实例。

    枚举实现单例:

    1.Java的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法。

    2.Java枚举类型的基本思想是通过共有的静态fianl域为每个枚举常量导出实例的类。

    3.从某种角度将,枚举是单例的泛型化,本质上是单元素的枚举。

    代码:

    public enum Singleton{

        uniqueInstance;

    //单例自己的操作函数

    public void singletonOperation(){

    //功能处理

        }

    }

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

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

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

    [编辑:王振袢 &发表于江苏]
    [我要纠错]

    来源:本文内容搜集或转自各大网络平台,并已注明来源、出处,如果转载侵犯您的版权或非授权发布,请联系小编,我们会及时审核处理。
    声明:江苏教育黄页对文中观点保持中立,对所包含内容的准确性、可靠性或者完整性不提供任何明示或暗示的保证,不对文章观点负责,仅作分享之用,文章版权及插图属于原作者。

    关键词: 本文 授权 转自 公众 单例模式
    有价值
    0
    无价值
    0
    猜您喜欢
    最热文章

    暂不支持手机端,请登录电脑端访问

    正在加载验证码......

    请先完成验证