rooobot / architecture-training

Architecture training camp homework
0 stars 2 forks source link

架构师训练营-第三周总结 #8

Open rooobot opened 4 years ago

rooobot commented 4 years ago

面向对象的设计目标是:高内聚,低耦合。

有句话叫:字数越少,信息量越大。此话不假,上面的设计目标只有六个字,却是浓缩之精华。

什么是内聚?如何实现高内聚?什么是耦合?如何实现低耦合?

这四个问题就能引申出面向对象设计的五大原则:

这五大原则中的每一个原则都是围绕着面向对象设计的六个字来展开的,上一堂课已经讲过,这里不再重述。

那又如何去保证我们在软件设计时,让我们的设计尽可能的满足这五大设计原则呢?

大师们在积累了无数的经验之后,总结出了面象对象设计的23种设计模式。

那什么是面向对象编程的设计模式?

总的来说:设计模式是一种可重复使用的解决方案。

每一种设计模式都描述了一种问题的通用解决方案,且这种问题是反复地在我们的工作场景中出现的。

一个设计模式分为四个部分:

这些设计模式按不同的分类方式可以分为不同的类型:

按功能来分可分为三类:

按方式来分可分为两类:

具体的23种设计模式就不一一在这里列举了,老师布置的第一道作业题要求手写一个单例模式,我想在这里谈谈我对单例模式的理解,因为想在面试的时候真正写出一个没有问题的单例不是那么简单。

一般,我们写单例,最容易写的是饿汉模式:

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
   } 
}

这种方式是没问题,但是挑剔的人会说,这里的INSTANCE在初始化的时候初始化了,如果这样的类非常多,但是,只有少数几个才使用,那这里实例化的对象就白白的占用了很多的内存。

这个时候,一般会想到Lazy了,也就是懒汉模式:

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
   } 
}

这份代码看上去是懒汉模式,但是其实是有问题的,在多线程环境下,如果两个线程同时进入到if (instance == null)这里之后,instance会被初始化两次,两个线程拿到的不是同一个对象,这就不是单例了。

于是乎,首先想到的就是给getInstance()方法加上synchronized关键字:

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
   } 
}

这份代码虽然是解决了重复实例化的问题,但是同步的粒度比较大,并发线程比较多的时候,每个线程调用都要去做同步的操作,而单例模式的实例对象,一旦被实例化之后就不会再改变(除非重启应用),所以这个同步的粒度是可以优化的。

于是乎,就出现了双重检查的懒汉模式:

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
   } 
}

这里对instance的初始化做了两次检查,所以叫双重检查。双重检查完美的避免了上面的问题,只要instance被实例化了就不再走同步的代码块。

但是,上面的代码还是有问题的,问题点就在于instance的实例化。

Java中,实例化一个对象分为三步:

  1. 分配内存空间;
  2. 初始化对象;
  3. 将内存空间的地址赋值给对应的引用;

然而,现实是操作系统可以对指令进行重排序,所以上面的步骤可能会变成132,而不是123。所以,双重检查的懒汉模式需要给instance的定义处,加上volatile关键字,这个关键字在这里的作用是:禁止指令重排序优化。换句话说,就是volatile修饰的变量的赋值操作后面会有一个内存屏障,读操作不会被重排序到内存屏障之前。

所以,正确的代码应该是这样的:

public class Singleton {
    private volatile static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
   } 
}

双重检查(据说需要使用JDK 1.5+,在此之前JMM模型存在缺陷,我没有验证过)完成了,但是还是会有同步的操作存在,也就是说会有锁,而无锁肯定会优于无锁的方式,那有没有一种无锁的方式来实现这个单例呢?

答案是有的。

我最初看到这种方式实现的单例是在apache commons utils的一个类的源码中,第一次看到时我是真的惊讶到了,一个被大家写滥和鄙视到不行的单例模式,居然可以写得这么优雅。

具体代码如下,也是我交的作业1的答案:

public class Singleton {

    private Singleton() {}

    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

}

这份代码没有锁,并且,LazyHolder中的INSTANCE实例在Singleton#getInstance()方法调用之前是不会被实例化的,这就不会出现饿汉模式那种过早占用内存的情况;而且,上面的代码是使用静态嵌套类的方式实现的,所以,能绝对的保证INSTANCE只会被实例化一次,因此,就不需要双重检查了。

至此,我个人觉得,最美的单例模式已经产生了~~

以上就作为第三周的总结吧。

Reference: