2.1 善用设计模式

合理的使用设计模式,不仅能使系统更容易被他人理解,同时也能使系统拥有更加合理的结构。

单例模式

在Java中,使用单例模式能带来两大好处:
1、对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。
2、由于 new 操作次数减少,对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
因此对于系统的关键组件和被频繁使用的对象,使用单例模式可以有效地改善系统的性能。

单例模式的实现方式:

单例模式有5种实现方式,下面列举了三个比较常见的实现方式。
1、懒汉式

  1. public class SingletonLazy {
  2. private static SingletonLazy instance;
  3. private SingletonLazy(){
  4. }
  5. public static synchronized SingletonLazy getInstance(){
  6. if (instance == null){
  7. instance = new SingletonLazy();
  8. }
  9. return instance;
  10. }
  11. }

使用 synchronized 关键字进行加锁,保证线程安全。
优点:在第一次调用才初始化,避免了内存浪费。
缺点:对获取实例方法加锁,大大降低了并发效率。

2、饿汉式

public class SingletonEager {
    private static final SingletonEager instance = new SingletonEager();

    private SingletonEager(){}

    public static SingletonEager getInstance(){
        return instance;
    }
}

饿汉式是利用类加载机制来避免了多线程的同步问题,所以是线程安全的。
优点:未加锁,执行效率高。
缺点:类加载时就初始化实例,造成内存浪费。

3、双重锁校验

public class SingletonDCL {
    private volatile static SingletonDCL instance;

    private SingletonDCL(){}

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

利用了volatile修饰符的线程可见性(被一个线程修改后,其他线程立即可见),即保证了懒加载,又保证了高性能。
在《Java程序性能优化》一书中,为了保证懒加载的同时提高性能,作者采用静态内部类的方法来实现单例模式。实现代码如下:

public class SingletonInnerClass {

    private SingletonInnerClass() {
    }

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

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

⚠️ 序列化和反序列化可能会破坏单例。一般这种情况不多见,但如果存在,需要多加注意。

代理模式

代理模式是一种比较常见的设计模式,它是在用户与被请求对象之间使用一个代理对象完成用户请求。在软件设计中,使用代理模式的意图有很多;比如因为安全原因,需要屏蔽客户端直接访问真实对象;在远程调用中,使用代理类处理远程方法调用的技术细节等。这一小节主要讨论使用代理模式实现延迟加载,从而提升系统的性能和反应速度。

代理模式的主要结构

代理模式的主要参与者有4个,如下表所示。

角色 作用
主题接口 定义代理类和真实主题的公共对外方法,也是代理类代理的真实主题的方法
真实主题 真实实现业务逻辑的类
代理类 用来代理请求和封装真实主题
Main 客户端,使用代理类和主题接口完成一些工作

延迟加载核心思想:如果当前并没有使用这个组件,则不需要真正初始化,使用一个代理对象替代它的原有位置,只有真正使用的时候才会对其进行加载。
好处:在时间轴上分散系统压力;避免初始化一些可能不会被调用的数据从而减少资源浪费。

静态代理模式

2 设计优化 - 图1代理模式具体实现例子请点击代理模式详细

动态代理

动态代理是指运行时,动态生成代理类。即代理类的字节码将在运行时生成并载入当前的CLassLoader。
好处:
提高系统可维护性:当接口有变动时,不用频繁修改真是主题和代理类。
提升系统的灵活性:使用一些动态代理的生成方法甚至可以在运行时指定代理类的执行逻辑。
生成动态代理的方法一般包括JDK自带的动态代理、CGLIB、Javassist、ASM库等。JDK自带的动态代理相对功能较弱,ASM在使用上比较繁琐,所以一般推荐使用CGLIB或者Javassist;
动态代理具体实现点击CGLIB实现动态代理

享元模式

核心思想:如果在一个系统中存在多个相同的对象,那么只需共享一份对象的拷贝,而不必为每一次使用都创建新对象。
使用享元模式对性能提升的主要帮助有亮点:
1、可以节省重复创建对象的开销;
2、由于创建对象的数量减少,所以对系统内存的需求也会减小。
享元模式主要角色由享元工厂、抽象享元、具体享元类和主要函数组成,功能描述如下表。

角色 作用
享元工厂 用以创建具体享元类,维护相同的享元对象。
抽象享元 定义需共享的对象的业务接口。
具体享元类 实现抽象享元类的接口,完成具体逻辑。
Main 使用享元模式组件,通过享元工厂取得享元对象

享元模式类图如下图所示。
image.png
享元模式是为数不多的,只为提升系统性能而生的设计模式。它的主要作用就是复用大对象(重量级对象)以节省内存空间和对象创建的时间。
场景:系统有大量相似对象;系统业务需要使用缓冲池
优点:减少对象的创建,降低系统内存
缺点:提高了系统的负责度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。

装饰者模式

装饰者模式有一个巧妙的结构,可以动态添加对象功能。在基本设计原则中,有一条重要的设计准则叫合成/聚合复用原则。根据该原则的思想,代码复用应该尽可能使用委托,而不是使用继承。使用装饰者模式可以有效分离性能组件和功能组件,从而提升模块的可维护性并增加模块的复用性。其基本结构图如下图所示。
image.png
装饰者(Decorator)和被装饰者(ConcreteComponet)拥有相同的接口。被装饰者通常是系统的核心组件,完成特定的功能目标。而装饰者可以在被装饰者的方法前后加上特定的前置或者后置处理,增强被装饰者的功能。装饰者模式的主要角色如下表所示。

角色 作用
组件接口 组件接口是装饰者和被装饰者的超类或接口。它定义了被装饰者的核心功能和装饰者需要加强的功能点
具体组件 具体组件实现了组件接口的核心方法,完成一个具体的业务逻辑。同时也是被装饰者
装饰者 实现组件接口,并持有一个具体的被装饰者对象
具体装饰者 具体实现装饰的业务逻辑,即实现了被分离的各个增强功能点。各个具体装饰者是可以相互叠加的,从而可以构成一个功能更强大的组件对象

观察者模式

观察者模式主要是定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
优点: 1、观察者和被观察者是抽象耦合的。 2、建立一套触发机制。
缺点: 1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
使用场景: 1、有多个子类共有的方法,且逻辑相同。 2、重要的、复杂的方法,可以考虑作为模板方法。
注意事项: 1、JAVA 中已经有了对观察者模式的支持类。 2、避免循环引用。 3、如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。
观察者模式的经典结构图如下图所示。(图来源:《Java程序性能优化》)
image.png

ISubject 是被观察对象,它可以增加或者删除观察者。IObserver 是观察者,他依赖于 ISubject 的状态变化。当 ISubject 状态发生改变时,会通过 inform() 方法通知观察者。

Value Object模式

所谓 Value Object 模式就是一个值对象模式,主要实现就是将一个对象的各个属性进行封装,用封装后的对象在网络中传递,这样在获取一个对象的多个属性时不需要请求多次,从而使系统拥有更好的交互模型,并减少网络通信数据,以达到提高系统性能的目的。

业务代理模式

上面的 Value Object 模式是将远程调用的传递数据封装在一个串行化的对象中进行传输,而业务代理模式则是将一组由远程方法调用构成的业务流程,封装在一个位于展示层的代理类中。

eg:

一个用户想要修改订单,需要3步。校验用户、获取订单信息、更新订单。业务流程如下图所示;
image.png
以上的结构存在两个问题:
1、展示层大量并发线程时,线程会直接进行远程调用,进而会加重网络负担。
2、缺乏对订单修改操作流程有效封装,可扩展性以及可维护性不高。
为了解决以上存在两个问题,将结构进行优化,展示层加入业务代理对象,负责与远程服务通信,完成操作。优化的业务流程结构图如下图所示。
image.png
业务代理模式将一些业务流程封装在前台系统,为系统性能优化提供了基础平台。在业务代理中,不仅可以复用业务流程,还可以视情况为展示成组件提供缓存等功能,从而减少远程方法调用次数,降低系统压力。

2.2 常用优化组件和方法

缓冲

缓冲区是一块特定的内存区域。开辟缓冲区的目的是通过缓解应用程序上下层之间的性能差异,提高系统性能。下面用一个缓冲应用实例图—漏斗说明缓冲的应用与功能。
image.png
缓冲区可以协调上层组件与下层组件的性能差。当上层组件性能优于下层组件是,可以有效减少上层组件对下层组件的等待时间。使用缓冲之后,上层应用组件不需要等待下层组件真实地接受全部数据,即可返回操作,加快了上层组件的处理速度,同时提高了系统的整体性能。缓冲的常用场景,I/O 操作。
除了性能上的优化,缓冲区还可以作为上层组件和下层组件的一种通信工具。从而将上层组件和下层组件进行解耦,优化设计结构。

缓存(Cache)

缓存是一块为了提高系统性能开辟的内存空间,其主要作用是暂存数据处理结果,并提供下次访问。比如保存一些来之不易的数据或者不会频繁改变的计算结果,但需要再次使用这些数据时,可以直接从缓存中获取,不需要花费时间重新计算或者查询。

对象复用 —— “池”

对于池的概念,最熟悉的应该就是线程池和数据库连接池。线程池中存放的是可以被重用的对象,当任务被提交到线程池时,系统不需要花费额外的资源以及时间创建新线程,直接从池中获取一个可用的线程去完成任务。任务结束后,线程会被重新释放到线程池,以便于下次使用。同理,对象复用的池化也是将被频繁请求使用的对象生成一个实例,放入对象池,需要使用的时候可以直接从池中获取。
在Java Web 项目中,使用线程池和数据库连接池可以有效的改善系统在高并发下的性能。目前应用较为广泛的数据库连接池有C3P0、Druid 。
⚠️ 只有对重量级对象使用对象池技术才能提高系统性能,对轻量级对象使用对象池技术可能会降低系统性能。

并行替代串行

在程序开发设计时,可以使用多线程技术,以并行方式替代传统的串行执行,最大可能发挥CPU的潜能,以免造成系统资源浪费。
在 Java 中,其提供了 Thread 对象和 Runnble 接口用于创建进程内的线程。其次,为了优化并行程序性能,JDK 还提供了 java.util.concurrent 并发包,内置各种多线程性能优化工具和组件。

负载均衡

在一些较为大型的系统中,如果并发数很多,单台计算机无法承受处理大量的并发请求,这个时候为了保证系统的服务质量,需要多台服务器协同工作,将系统的负载尽可能的平均分配到各个节点上,实现负载均衡。
在 web 系统中,有一个较为典型的实现便是 Tomcat 集群。配置 Tomcat 集群实现负载均衡,可以通过 APache 服务器实现。如下图所示;
2 设计优化 - 图8
在使用 Tomcat 集群时,有两种基本的 Session 共享模式。黏性 Session 模式和复制 Session模式。在黏性模式下,所有的 Session 信息被平均分配到各个 Tomcat 节点上,但其中一个节点宕机,其所维护的 Session 信息将会丢失,不具备高可用性。且同一用户只能与一台 Tomcatr 交互。而复制模式将事所有 Session 在所有 Tomcat 节点保持一致。当其中一个节点的 Session 信息被修改时,这个 Session 会被广播到其他 Tomcat 节点上,以保持 Session 同步。但这样做容易引起网络繁忙,影响系统效率。
在 Java 开源软件中,有一款跨 JVM 虚拟机,专门用于分布式缓存的框架 —— Terracotta。使用 Terracotta 也可以实现 Tomcat 的 Session 共享,同时也是一个成熟的高可用性系统解决方案。Terracotta 工作架构如下图所示。
2 设计优化 - 图9
使用 Terracotta 可以在多个 Java 应用服务器间共享缓存,Terracotta 除了与 Tomcat 集成之外,还可以与一些主流的 Java 组件集成使用,如 Jetty、Spring 和 EHCache。
总结:Terracotta 是一款企业级的、开源的、JVM 层的集群解决方案。它可以实现诸如分布式对象共享、分布式缓存、分布式 Session 等功能。可以作为负载均衡、高可用性的解决方案。

时间空间交换

时间空间交换,顾名思义,使用时间换空间或者使用空间换时间。时间换空间主要是在内存、硬盘空间不足的情况下,牺牲 CPU 的资源去完成一些列操作。而空间换时间就是通过使用更多的内存空间换取 CPU 资源或者网络资源等,通过增加系统的内存消耗来加快程序的运行速度。如缓存;