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

: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

您在阅读:女朋友:你能给我讲讲单例模式吗?

Copyright©2013-2024 JSedu114 All Rights Reserved. 江苏教育信息综合发布查询平台保留所有权利

苏公网安备32010402000125 苏ICP备14051488号-3南京思必达教育科技有限公司版权所有

南京思必达教育科技有限公司版权所有   百度统计

最热文章
最新文章
  • 卡尔蔡司镜片优惠店,镜片价格低
  • 苹果原装手机壳