vlambda博客
学习文章列表

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

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

作者 | 静幽水
责编 | Elle

女朋友:你能给我讲讲单例模式吗?
问题背景

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

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


女朋友:你能给我讲讲单例模式吗?
解决方法


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


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


上面这种解决方案就是单例模式(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和双重检查加锁也能保证懒汉式的线程安全。


女朋友:你能给我讲讲单例模式吗?
新的问题


由于小强工作很卖命,公司业绩发展的不错,老板决定再招一名程序员,应聘者小华也是一个单身汉,老板也承诺会给他分配女朋友,单是问题来了,之前的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<String,GirlFriend> girlFriendMap = new HashMap<String, GirlFriend>();


//私有化构造方法,防止外部调用
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的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。
类级内部类:
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资源空闲的情况下该对象会被清理掉,下次再调用时就需要重新产生一个对象。
声明:本文为作者投稿,版权归作者个人所有。
【End】

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

热 文 推 荐 

点击阅读原文,即刻参加!

你点的每个“在看”,我都认真当成了喜欢