《Java程序性能优化实战》读书笔记(二)

175 阅读9分钟

第2章 设计优化

1 善用设计模式

设计模式部分结合本书和《设计模式就该这样学》的内容。

单例模式

  • 定义:保证一个类仅有一个实例,并提供一个全局访问点
  • 适用场景:确保任何情况下都绝对只有一个实例
  • 优点:在内存里只有一个实例,减少了内存开销

特征:

单例类必须要有一个private访问级别的构造函数,其次instance成员变量和getInstance()方法必须是static的。

重点:

  • 私有构造器
  • 线程安全
  • 延迟加载
  • 序列化和反序列化安全
  • 反射

单例模式共分为两大类:

懒汉模式:实例在第一次使用时创建

饿汉模式:实例在类装载时创建

懒汉模式(线程不安全)

private static LazySingleton lazySingleton = null;
 
private LazySingleton() {
}
 
public static LazySingleton getInstance(){
    if (lazySingleton == null){
        lazySingleton = new LazySingleton();
    }
    return lazySingleton;
}

懒汉模式(加同步锁)

private static LazySingleton lazySingleton = null;
 
private LazySingleton() {
}
 
public synchronized static LazySingleton getInstance(){
    if (lazySingleton == null){
        lazySingleton = new LazySingleton();
    }
    return lazySingleton;
}

懒汉模式(双重检查)

private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
 
private LazyDoubleCheckSingleton() {
}
 
public static LazyDoubleCheckSingleton getInstance(){
    if (lazyDoubleCheckSingleton == null){
        synchronized (LazyDoubleCheckSingleton.class) {
            if (lazyDoubleCheckSingleton == null) {
                //1、分配内存给这个对象
                //2、初始化内存空间
                //3、设置lazylazyDoubleCheckSingleton变量指向刚分配的内存空间
                lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
            }
        }
    }
    return lazyDoubleCheckSingleton;
}

懒汉模式(静态内部类)

类的静态变量被初次访问会触发Java虚拟机对该类进行初始化,静态方法getInstance()被调用的时候Java虚拟机会初始化这个方法所访问的静态内部类InnerClass,使得InnerClass的静态变量staticInnerClassSingleton被初始化。

private StaticInnerClassSingleton() {
}
 
private static class InnerClass{
    private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
 
public static StaticInnerClassSingleton getInstance(){
    return InnerClass.staticInnerClassSingleton;
}

饿汉模式

public HungrySingleton() {
}
 
private final static HungrySingleton hungrySingleton;
 
static {
    hungrySingleton = new HungrySingleton();
}
 
public static HungrySingleton getInstance(){
    return hungrySingleton
}

序列化和反序列化会破坏单例模式

序列化后的对象会新建一个对象

解决方法:通过debug发现,当单例类中存在readResolve方法时,会通过反射调用,将返回的Object对象代替反序列化新建的对象。

private Object readResolve(){
    return hungrySingleton;
}

枚举单例(推荐)

示例:

public enum EnumInstance {
    INSTANCE;
    private Object data;
 
    public Object getData() {
        return data;
    }
 
    public void setData(Object data) {
        this.data = data;
    }
 
    public static EnumInstance getInstance(){
        return INSTANCE;
    }
}

jad反编译:

public final class EnumInstance extends Enum
{
 
    public static EnumInstance[] values()
    {
        return (EnumInstance[])$VALUES.clone();
    }
 
    public static EnumInstance valueOf(String name)
    {
        return (EnumInstance)Enum.valueOf(com/imooc/create/singleton/EnumInstance, name);
    }
 
    private EnumInstance(String s, int i)
    {
        super(s, i);
    }
 
    public Object getData()
    {
        return data;
    }
 
    public void setData(Object data)
    {
        this.data = data;
    }
 
    public static EnumInstance getInstance()
    {
        return INSTANCE;
    }
 
    public static final EnumInstance INSTANCE;
    private Object data;
    private static final EnumInstance $VALUES[];
 
    static
    {
        INSTANCE = new EnumInstance("INSTANCE", 0);
        $VALUES = (new EnumInstance[] {
            INSTANCE
        });
    }
}

从反编译的结果来看,符合饿汉模式的特点。

预防序列化破坏:

从ObjectInputStream源码可知,枚举类反序列化获取的是唯一的枚举常量。

预防反射破坏:

从Constructor源码的newInstance(Object ... initargs)方法可知,若目标类为枚举类型,则抛出异常,因此无法进行反射攻击。

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    throw new IllegalArgumentException("Cannot reflectively create enum objects");

容器单例(线程不安全)

private ContainerSingleton(){}
private static final Map<String, Object> singletonMap = new HashMap<>();
 
public static void putInstance(String key, Object instance) {
    if (StringUtils.isNotBlank(key) && instance != null) {
        if (!singletonMap.containsKey(key)) {
            singletonMap.put(key, instance);
        }
    }
}
 
public static Object getInstance(String key){
    return singletonMap.get(key);
}

ThreadLocal线程单例(不能保证全局唯一,只能保证线程唯一)

private ThreadLocalSingleton(){}
private static ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){
    @Override
    protected ThreadLocalSingleton initialValue() {
        return new ThreadLocalSingleton();
    }
};
 
public static ThreadLocalSingleton getInstance(){
    return threadLocalInstance.get();
}

代理模式

  • 定义:为其他对象提供一种代理,以控制对这个对象的访问
  • 代理对象在客户端和目标对象之间起到中介的作用

适用场景:

  • 保护目标对象
  • 增强目标对象

角色:

(1)抽象主题角色(ISubject):抽象主题类的主要职责是声明真实主题与代理的共同接口方法,该类可以是接口,也可以是抽象类。

(2)真实主题角色(RealSubject):该类也被称为被代理类,该类定义了代理所表示的真实对象,是负责执行系统的真正的逻辑业务对象。

(3)代理主题角色(Proxy):也被称为代理类,其内部持有RealSubject的引用,因此具备完全的对RealSubject的代理权。客户端调用代理对象的方法,也调用被代理对象的方法,但是会在代理对象前后增加一些处理代码。

优点:

  • 将代理对象和真实被调用的目标对象分离

  • 降低系统耦合,提高扩展性

  • 保护目标对象

  • 增强目标对象

缺点:

  • 系统设计中类的数目增加
  • 客户端和目标对象增加一个代理对象,造成请求处理速度变慢
  • 增加系统复杂度

代理模式分类:

  • 静态代理
  • 动态代理

动态代理实现:

  • JDK动态代理
  • CGLib动态代理

相关设计模式:

  • 装饰者模式
  • 适配器模式

CGLib和JDK动态代理对比分析

(1)JDK动态代理实现了被代理对象的接口,CGLib动态代理继承了被代理对象。

(2)JDK动态代理和CGLib动态代理都在运行期生成字节码,JDK动态代理直接写Class字节码,CGLib动态代理使用ASM框架写Class字节码。CGLib动态代理实现更复杂,生成代理类比JDK动态代理效率低。

(3)JDK动态代理调用代理方法是通过反射机制调用的,CGLib动态代理是通过FastClass机制直接调用方法的,CGLib动态代理的执行效率更高。

享元模式

  • 定义:提供了减少对象数量从而改善应用所需的对象结构的方式
  • 运用共享技术有效地支持大量细粒度的对象
  • 结构型

适用场景:

  • 用于系统底层开发,解决系统的性能问题
  • 系统有大量相似对象、需要缓冲池的场景

角色:

(1)抽象享元角色(IFlyweight):享元对象抽象基类或者接口,同时定义出对象的外部状态和内部状态的接口或实现。

(2)具体享元角色(ConcreteFlyweight):实现抽象角色定义的业务。该角色的内部状态处理应该与环境无关,不会出现一个操作改变内部状态、同时修改了外部状态的情况。

(3)享元工厂(FlyweightFactory):负责管理享元对象池和创建享元对象。

优点:

(1)减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率。

(2)减少内存之外的其他资源占用。

缺点:

(1)关注内、外部状态,关注线程安全问题。

(2)使系统、程序的逻辑复杂化。

案例:

//抽象享元角色:享元对象抽象基类或者接口
public interface Employee {
    void report();
}

//具体享元角色:实现抽象角色定义的业务
public class Manager implements Employee{

    @Override
    public void report() {
        System.out.println(reportContent);
    }

    private String department;
    private String reportContent;

    public void setReportContent(String reportContent) {
        this.reportContent = reportContent;
    }

    public Manager(String department) {
        this.department = department;
    }
}

//享元工厂:负责管理享元对象池和创建享元对象
public class EmployeeFactory {
    private static final Map<String, Employee> EMPLOYEE_MAP = new HashMap<>();

    public static Employee getManager(String department) {
        Manager manager = (Manager) EMPLOYEE_MAP.get(department);
        if (manager == null) {
            manager = new Manager(department);
            String reportContent = department + "部门汇报:。。。";
            manager.setReportContent(reportContent);
            EMPLOYEE_MAP.put(department, manager);
            System.out.println("创建部门经理:" + department);
        }
        return manager;
    }
}

装饰者模式

  • 定义:装饰器模式也叫作包装器模式,指在不改变原有对象的基础上,动态地给一个对象添加一些额外的职责。
  • 结构型设计模式

应用场景:

(1)用于扩展一个类的功能,或者给一个类添加附加职责。

(2)动态地给一个对象添加功能,这些功能可以再动态地被撤销。

(3)需要为一批平行的兄弟类进行改装或加装功能。

角色:

(1)抽象组件(Component):可以是一个接口或者抽象类,充当被装饰类的原始对象,规定了被装饰对象的行为。

(2)具体组件(ConcreteComponent):实现/继承Component的一个具体对象,即被装饰对象。

(3)抽象装饰器(Decorator):通用的装饰ConcreteComponent的装饰器,其内部必然有一个属性指向Component;其实现一般是一个抽象类,主要为了让其子类按照其构造形式传入一个Component,这是强制的通用行为。如果系统中装饰逻辑单一,则并不需要实现许多装饰器,可以直接省略该类,而直接实现一个具体装饰器即可。

(4)具体装饰器(ConcreteDecorator):Decorator的具体实现类,理论上,每个ConcreteDecorator都扩展了Component对象的一种功能。

案例代码:

//抽象组件
public abstract class Component {
    public abstract void operation();
}
 
//具体组件
public class ConcreteComponent extends Component{
    @Override
    public void operation() {
        System.out.println("处理业务逻辑");
    }
}
 
//抽象装饰器
public abstract class Decorator extends Component{
    private Component component;
 
    public Decorator(Component component) {
        this.component = component;
    }
 
    @Override
    public void operation() {
        component.operation();
    }
}
 
//具体装饰器
public class ConcreteDecoratorA extends Decorator{
    public ConcreteDecoratorA(Component component) {
        super(component);
    }
 
    private void before(){};
 
    private void after(){};
 
    @Override
    public void operation() {
        before();
        super.operation();
        after();
    }
}

让装饰器实现与被装饰类(例如ConcreteComponent)相同的接口(例如Component),使得装饰器与被扩展类类型一致,并在构造函数中传入该接口对象,然后在实现这个接口的被包装类对象的现有功能上添加新功能。由于装饰器与被包装类属于同一类型(均为Component),且构造函数的参数为其实现接口类(Component),因此装饰器模式具备嵌套扩展功能,这样就能使用装饰器模式一层一层地对底层被包装类进行功能扩展了。

源码应用举例:

IO包装类

优点:

(1)装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态地给一个对象扩展功能,即插即用。

(2)通过使用不同装饰类及这些装饰类的排列组合,可以实现不同效果。

(3)装饰器模式完全遵守开闭原则。

缺点: (1)会出现更多的代码、更多的类,增加程序的复杂性。

(2)动态装饰在多层装饰时会更复杂。

观察者模式

  • 定义:又叫作发布-订阅模式。一种一对多的依赖关系,一个主题对象可被多个观察者对象同时监听,使得每当主题对象状态变化时,所有依赖它的对象都会得到通知并被自动更新。
  • 行为型

核心是将观察者与被观察者解耦,以类似消息/广播发送的机制联动两者,使被观察者的变动能通知到感兴趣的观察者们,从而做出相应的响应。

应用场景:

观察者模式可以用于事件监听、通知发布等场合,它可以确保观察者在不使用轮询监控的情况下及时收到相关消息和事件。

(1)当一个抽象模型包含两方面内容,其中一方面依赖另一方面。

(2)其他一个或多个对象的变化依赖另一个对象的变化。

(3)实现类似广播机制的功能,不需要知道具体收听者,只需分发广播,系统中感兴趣的对象会自动接收该广播。

(4)多层级嵌套使用,形成一种链式触发机制,使得事件具备跨域(跨越两种观察者类型)通知。

角色:

(1)抽象主题(ISubject):指被观察的对象(IObservable)。该角色是一个抽象类或接口,定义了增加、删除、通知观察者对象的方法。

(2)具体主题(ConcreteSubject):具体被观察者,当其内部状态变化时,会通知已注册的观察者。

(3)抽象观察者(IObserver):定义了响应通知的更新方法。

(4)具体观察者(ConcreteObserver):当得到状态更新的通知时,会自动做出响应。

代码:

//抽象主题
public interface ISubject {
    void attach(IObserver iObserver);//添加观察者

    void detach(IObserver iObserver);//删除观察者

    void inform();//通知所有观察者
}
//抽象观察者
public interface IObserver {
    void update(Event event);
}
//具体主题
public class ConcreteSubject implements ISubject{
    Vector<IObserver> observers = new Vector<IObserver>();
    @Override
    public void attach(IObserver iObserver) {
        observers.addElement(iObserver);
    }

    @Override
    public void detach(IObserver iObserver) {
        observers.removeElement(iObserver);
    }

    @Override
    public void inform() {
        Event event = new Event();
        for (IObserver observer : observers) {
            observer.update(event);
        }
    }
}
//具体观察者
public class ConcreteObserver implements IObserver{
    @Override
    public void update(Event event) {
        System.out.println("observer receives information");
    }
}

优点:

(1)观察者和被观察者是松耦合(抽象耦合)的,符合依赖倒置原则。

(2)分离了表示层(观察者)和数据逻辑层(被观察者),并且建立了一套触发机制,使得数据的变化可以响应到多个表示层上。

(3)实现了一对多的通信机制,支持事件注册机制,支持兴趣分发机制,当被观察者触发事件时,只有感兴趣的观察者可以接收到通知。

缺点:

(1)如果观察者数量过多,则事件通知会耗时较长。

(2)事件通知呈线性关系,如果其中一个观察者处理事件卡壳,则会影响后续的观察者接收该事件。

(3)如果观察者和被观察者之间存在循环依赖,则可能造成两者之间的循环调用,导致系统崩溃。

2 常用的优化组件和方法

缓冲

缓冲区是一块特定的内存区域。目的是通过协调应用程序上下层之间的性能差异,提高系统的性能。上层应用组件不需要等待下层组件真实地接收全部数据,即可返回操作。

最常用场景:提高IO的速度,如BufferedWriter、BufferedOutputStream

由于IO操作很容易导致性能瓶颈,尽可能在IO读写中加入缓冲组件,以提高系统的性能。

缓存

为提升系统性能而开辟的内存空间,其主要作用是暂存数据处理结果,并提供给下次访问使用。

常见的缓存框架:Caffeine(本地缓存)、redis(分布式缓存)

Caffeine为例,Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库,属于堆内存缓存,只适用于单点使用,不适用分布式环境。

1、导入pom依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

2、配置文件

Caffeine常用配置说明:

  • initialCapacity=[integer]: 初始的缓存空间大小
  • maximumSize=[long]: 缓存的最大条数
  • maximumWeight=[long]: 缓存的最大权重
  • expireAfterAccess=[duration]: 最后一次写入或访问后经过固定时间过期
  • expireAfterWrite=[duration]: 最后一次写入后经过固定时间过期
  • refreshAfterWrite=[duration]: 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存

注意点:

  • expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准
  • maximumSize和maximumWeight不可以同时使用

配置案例

1、properties文件配置

spring.cache.cache-names=caffeineCache
spring.cache.type=caffeine
spring.cache.caffeine.spec=maximumSize=1000,expireAfterAccess=120s

2、缓存配置类

/**
* 配置缓存管理器
* @return
*/
@Bean("cacheManage")
public CacheManager cacheManage(){
    SimpleCacheManager cacheManager = new SimpleCacheManager();
    // 缓存集合
    ArrayList<CaffeineCache> caches = new ArrayList<>();
 
    caches.add(new CaffeineCache("caffeineCache", Caffeine.newBuilder()
                                 // 指定Key下的最大缓存数据量
                                 .maximumSize(1000)
                                 // 设置最后一次写入或访问后经过固定时间过期
                                 .expireAfterAccess(120, TimeUnit.SECONDS)
                                 .build()));
    cacheManager.setCaches(caches);
    return cacheManager;
}

3、注解

spring常用缓存注解:

  • @EnableCaching:启用缓存,注解在启动类或配置类上
  • @Cacheable:先从缓存中通过定义的键查询,如果可以查到数据则返回,否则执行该方法,并将返回结果保存到缓存中;
  • @CacheEvict:通过定义的键移除缓存
  • @CachePut:将方法返回结果存放到缓存中
  • @Caching:定义复杂的缓存规则

对象复用——池

核心思想:如果一个类被频繁地请求使用,那么不必每次都生成一个实例,而将这个类的一些实例保存在一个池中,待需要使用的时候直接从池中获取。

典型应用:线程池、数据库连接池

Commons Pool2开源对象池化组件使用:

  • PooledObjectFactory:用于生成连接对象的工厂接口,接口的方法都将被对象池回调,以指导对象池在对象的生命周期中如何管理这些对象。
makeObject():定义如何创建一个新的对象实例。
activateObject():在对象从对象池取出前会激活该对象。
passivateObject():在对象返回对象池时被调用。
destroyObject():在对象从对象池中被销毁时会执行这个方法。
validateObject():判断对象是否可用。
  • BasePooledObjectFactory:PooledObjectFactory接口的抽象实现类。
  • DefaultPooledObject:默认的对象包装器用于跟踪其他信息,例如状态。
  • GenericObjectPool:是一个通用的对象池,它可以设定对象池的容量,也可以设定在无可用对象的情况下对象池的表现行为(等待或者创建新的对象实例),还可以设置是否进行对象的有效性检查。
/**
 * 自定义对象工厂
 */
public class MyObjectFactory extends BasePooledObjectFactory<Connection> {
    private static AtomicInteger counter = new AtomicInteger(0);

    public Connection create() throws Exception {
        Connection conn = new Connection(counter.getAndIncrement());
        System.out.println("create object "+ conn);
        return conn;
    }

    public PooledObject<Connection> wrap(Connection conn) {
        return new DefaultPooledObject<>(conn);
    }
}

/**
 * 使用对象池
 */
public class MyObjectPool {
    static MyObjectFactory factory = new MyObjectFactory();
    static GenericObjectPool pool = new GenericObjectPool(factory);
    private static AtomicInteger endCount = new AtomicInteger(0);

    private static class PoolThread extends Thread {

        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    System.out.println("===" + i + "===");
                    Object o = pool.borrowObject();
                    System.out.println(Thread.currentThread().getName() + " get " + o);
                    pool.returnObject(o);
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                endCount.getAndIncrement();
            }
        }
    }

    public static void main(String[] args) {
        new PoolThread().start();
        new PoolThread().start();
        new PoolThread().start();
        while (true) {
            if (endCount.get() == 3) {
                pool.close();
                break;
            }
        }
    }
}

注意:只有对重量级的对象使用对象池技术才能提高系统的性能;对于轻量级的对象使用对象池,反而可能会降低系统的性能。

并行代替串行

参考java.util.concurrent并发包

负载均衡

举例:分布式session共享

时间换空间

时间换空间通常用于嵌入式设备或者内存、硬盘空间不足的情况,通过牺牲CPU性能的方式,获得原本需要更多内存或硬盘空间才能完成的工作。

空间换时间

使用更多的内存或者磁盘空间换取CPU资源或者网络资源等,通过增加系统的内存消耗,来加快程序的运行速度。