【Java程序性能优化 第一版】第二章

 

                                          第2章     设计优化

    本章主要介绍与软件设计相关的性能优化和思想。软件的结构对系统整体性能有这重要的影响。优秀的设计结构可以规避很多潜在的性能问题,对系统性能的影响可能远远大于代码的优化。因此,熟悉一些常用的软件设计模式和方法,对设计高性能软件有着重要的帮助。本章着眼于设计优化,主要讲解了一些常用的与性能相关的设计模式,组件和设计方法。

   本章涉及到的主要知识点有:

    □单例模式的使用和实现;

    □代理模式的实现和深入剖析;

    □享元模式的应用;

    □装饰着模式对性能组件的封装;

    □观察者模式的使用;

    □使用ValueObject模式减少网络数据传输;

    □使用业务代理模式添加远程调用缓存;

    □缓冲和缓存的定义和使用;

    □对象池的使用场景及其基本实现;

    □构建负载均衡系统以及Terracotta框架的简单使用;

    □时间换空间和空间换时间的基本思路;

 

      2.1 善用设计模式

    设计模式是前任工作的总结和提炼,通常, 被人们广泛流传的设计模式都是对某一特定问题的成熟的解决方案。如果能合理地使用设计模式,不仅能使系统更容易被他人理解,同时也能使系统拥有更加合理的结构。本节总结归纳了一些经典的设计模式,并详细说明它们与软件性能之间的关系。

    2.1.1单例模式(Singleton Pattern)

     单例模式是设计模式中使用最为普遍的模式之一。它是一种对象创建模式,用于产生一个对象的具体事例,它可以确保系统中一个类只产生一个实例。在Java语言中,这样的行为能带来两大好处;

     (1)对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔开销。

     (2)由于new操作的次数减少,因为对系统内存的使用频率也会降低,将减轻GC压力,缩短GC停顿时间。

       接下来是一个简单的单例实现:

public class Simple {

    private Simple() {
        System.out.println("Singleton created ");
    }

    private static Simple instance = new Simple();

    public static Simple getInstance() {
        return instance;
    }

    public static void createString() {
        System.out.println("createString in Singleton");
    }

}

首先单例类必须有一个private访问级别的构造函数,只有这样,才能确保单例不会在系统中的其它代码内被实例化,这点是相当重要的。其次,instance成员变量和getInstance()方法必须是static的。这种单例实现方式非常简单,而且十分可靠。它唯一的不足仅是无法实现延迟加载。如果单例的创建过程很慢,而由于instance成员变量是static定义的,因此在JVM加载单例类时,单例对象就会被创建,如果此时,这个单例类在系统中还扮演其它角色,那么在任何使用这个单例类的地方都会初始化这个单例变量,而不管是否会被用到(在类第一次使用时加载时进行初始化)。当使用到createString()方法时, 类也是初始化了。

    可以看到, 虽然此时并没有使用单例类,但它还是被创建出来,这也许是开发人员所不愿意见到的。为了解决这个问题,并以此提高系统在相关函数调用时的反应速度,就需要引入延迟加载机制。

public class LazySingleton {

    private LazySingleton() {
        System.out.println("LazySingleton is created");
    }

    private static LazySingleton instance = null;

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

    public static void createString() {
        System.out.println("createString in Singleton");
    }

}

    首先,对于静态成员变量instance初始值赋予null,确保系统启动时没有额外的负载;其次,在getInstance()工厂方法中,判断当前单例是否存在,若存在则返回,若不存在则再建立单例。这里尤其要注意,getInstance()方法必须是同步的,否则在多线程环境下,当线程1正在新建单例时,完成赋值操作前,线程2会判断instance为null,顾线程2也将启动新建单例的程序,而导致多个实例被创建,故同步关键字是必须的。

    使用上例的单例实现,虽然实现了延迟加载的功能,但是和第一种方法相比,它引入了同步关键字,因此在多线程环境中,它的耗时远远大于第一种单例模式。

  为了使用延迟加载引入的同步关键字反而降低了系统性能,是不是有点得不偿失呢?为了解决这个问题,还需要对其进行改进。

  

public class StaticSingleton {
	private StaticSingleton() {
		System.out.println("StaticSingleton is create");
	}
	
	private static class SingletonHolder {
		private static StaticSingleton instance = new StaticSingleton();
	}
	
	public static StaticSingleton getInstance() {
		return SingletonHolder.instance;
	}

}

       在这个实现中,单例模式使用内部类来维护单例的实例,当StaticSingleton被加载时,其内部类不会被初始化,故可以确保当StaticSingleton被载入JVM时,不会初始化单例类,而当getInstance()方法被调用时,才会加载SingletonHolder,从而初始化instance。同时,由于实例的建立是在类加载时完成,故天生对多线程友好,而getInstance()方法也不需要使用同步关键字。因此,这种实现方式同时兼备以上两种实现的有点。使用内部类的方式实现单例,既可以做到延迟加载,也不必使用同步关键字,是一种比较完善的实现

       通常情况下,用以上方式实现的单例已经可以确保在系统中只存在唯一实例了。但仍然有例外情况,可能导致系统生成多个实例,比如,在代码中,通过反射机制,强行调用单例类的私有构造函数,生成多个单例。考虑到情况的特殊性,本书中不对这种极端的方式进行讨论。但仍有些合法的方法,可能导致系统出现多个单例类的实例。

 

    2.1.2代理模式(Proxy Pattern)

   代理模式特也是一种很常见的设计模式, 它使用代理对象完成用户请求,屏蔽用户对真实对象的访问。

   在现实中,使用代理模式的情况很普遍,而且原因也很多。比如,当事人因为某些隐私不方便操作,或者当事人不具备某些相关的专业技能,而需要一个执业人员来完成一些专业的操作,也可能由于当事人没有时间处理事务,而聘用代理人出面。

   在软件设计中,使用代理模式的意图也很多,比如因为安全原因,需要屏蔽客户端直接访问真实对象;或者在远程调用中,需要使用代理类处理远程方法调用的技术细节(如RMI);也可能时提升系统性能,对真实对象进行封装,从而达到延迟加载的目的

  1 代理模式的结构

   代理模式的主要参与者有4

   主题接口:定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法

   真实主题:真正实现业务逻辑的类

   代理类:用来代理和封装真实主题

   Main:客户端,使用代理类和主题接口完成一些工作 

   以一个简单的示例来阐述使用代理模式实现延迟加载的方法及其意义。假设客户端软件,有根据用户请求,去数据库查询数据的功能。在查询数据前,需要获得数据库连接,软件开启时,初始化系统的所有类,此时尝试去获得数据库连接。当系统有大量的类似操作的时候(比如XML解析),所有这些初始化操作的叠加,会使得系统的启动速度变得非常缓慢。因此,使用代理模式,使用代理类,封装对数据库查询中的初始化操作,当系统启动时,初始化这个代理类,而非正式的数据库查询类,而代理类什么都没有做,因此它的构造是相当迅速的。在系统启动时,将消耗资源最多的方法都使用代理模式分离,就可以加快系统的启动速度,减少用户的等待时间。而在用户真正做查询操作时,再由代理类,单独去加载真实的数据库查询类,完成用户请求。这个过程就是使用代理模式实现了延迟加载。(代理模式可以用于多个场合,如用于远程调用的网络代理,考虑安全按因素的安全代理,延迟加载只是其中的一个应用场景)延迟加载的核心思想是:如果当前并没有使用这个组件,则不需要真正地初始化它,使用一个代理对象替代它原有的位置,只要在真正需要的时候,才对它进行加载。首先,它可以在时间轴上分散系统压力,尤其在系统启动时,其次,对很多真实主题而言,在软件启动直至关闭的整个过程中,可能根本不会被调用,所以在一定程度上减少了资源的浪费

public interface DBQuery {
    String request();
}

 它的实现如下,它是一个重量级对象,构造会比较慢

public class DBQueryImpl implements DBQuery {

    public DBQueryImpl() {
        // 可能包含数据库连接耗时操作
    }

    @Override
    public String request() {
        return "request string";
    }
}

下面就是代理类,一个轻量级对象,创建很快, 用于替换重量级对象的位置

public class DBQueryProxy implements DBQuery {
    private DBQueryImpl real = null;

    @Override
    public String request() {
        // 在真正需要的时候,才创建真实对象, 创建过程可能很慢
        if (null == real) {
            real = new DBQueryImpl();
        }
        return real.request();
    }

    public static void main(String[] args) {
        DBQuery q = new DBQueryProxy();// 使用代理
        q.request(); // 在真正使用时才创建真实对象
    }
}

3.动态代理介绍

  动态代理是指在运行的时候,动态的生成代理类。即代理类的字节码将在运行时生成并载入当前的ClassLoader。与静态代理相比,动态类有诸多好处。它不需要为每一个真实主题单独写一个形式上完全一致的封装类,假如主题接口中的方法很多,为每一个接口写一个代理方法也是非常反锁的,并且如果接口有变动, 那么主题方法和代理方法同时都需要进行修改。其次,使用一些动态代理的生成方法甚至可以在运行时制定代理类的逻辑,从而大大提升系统的灵活性。 

   △ 注意:动态代理使用字节码动态生成加载技术,在运行时生成并加载类。

   生成动态代理类的方法很多,如,JDK自带的动态代理,CGLIB,Javassist或者ASM库。JDK的动态代理使用简单, 它内置在JDK中,因此不需要引入第三方的jar包,但相对功能较弱。CGLIB,Javassist都是高级的字节码生成库,总体性能比JDK自带的动态代理好,而且功能强大。ASM是低级的字节码生成工具,使用ASM已经几乎于在使用java bytecode编程,对开发人员要求最高,当然,也是性能最好的一种动态代理生成工具。但是它使用太过于繁琐,而且性能也没有数量级的提升,与CGLIB,Javassist等高级字节码生成工具相比,ASM程序的可维护性比较差,所以推荐CGLIB,Javassist

4.动态代理实现

    JDK的动态代理生成代理对象。JDK的动态代理需要实现一个处理方法调用的Handle,用于实现代理方法的内部逻辑。

public class JdkDbQueryHander implements InvocationHandler{
    DBQuery real = null; //主题接口
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(null == real)
            real = new DBQueryImpl();
        return real.request();
    }
}

   以上代码实现了一个Handler,可以看到,它的内部逻辑和DBQueryProxy是类似的。在调用真实主题的方法前,先尝试生成真实主题对象。接着,需要使用这个Handler生成动态代理对象。

  

public static DBQuery createJdkProxy() {
        DBQuery jdkProxy = (DBQuery) Proxy
                .newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{ DBQuery.class },
                        new JdkDbQueryHander());

        return jdkProxy;
    }

 以上代码生成了一个显示DBQuery接口的代理类,代理类的内部逻辑油JdkDbQueryHandler决定。生成代理类后, 由newProxyInstance方法返回该代理类的一个实例。至此,一个完整的JDK动态代理就完成了

  CGLIB,Javassist的动态代理的使用和JDK的动态代理非常类似。下面,尝试使用CGLIB生成动态代理。它也需要实现一个处理代理逻辑的切入类:(略过)

  2.1.3 享元模式(Flyweight Pattern)

  享元模式是设计模式中少数几个以提高系统性能为目的的设计模式之一。它的核心思想是:如果在一个系统中存在多个相同的对象,那么只需要共享一份对象的拷贝,而不必为每一次使用都创建新的对象。在享元模式中,由于需要构造和维护这些可以共享的对象,因此,常常会出现一个工厂类,用于维护和创建对象。

   享元模式对性能的提升主要帮助有两点:

   (1)可以节省重复创建对象的开销,因为被享元模式维护的相同对象只会被创建一次,当创建对象比较耗时时,便可以节省大量时间。

   (2)由于创建对象的数量减少,所以对系统的内存的需求也减少,这将使得GC的压力也相应的降低,进而使得系统拥有一个更健康的内存结构和更快的反应速度。

       享元模式的主要角色由享元工厂,抽象享元,具体享元类,和主函数几部分组成。

      享元工厂是享元模式的核心,它需要确保系统可以共享相同的对象。一般情况下,享元工厂会维护一个对象列表,当任何组件尝试获取享元类时,如果请求的享元类已经被创建,则直接返回已有的享元类;若没有则创建一个新的享元对象,并将它加入到维护队列中

      享元模式一个典型应用是在SAAS系统中(software as a service), 是目前比较流行的一种软件应用模式。假设公司甲乙丙均为SAAS系统的用户,则定义每个公司为这套系统的一个租户。每个公司(租户)又各有100员工。如果这些公司的所有员工都可以登录到这套系统查看自己的收入情况,并且为了系统安全,每个公司都有自己独立的数据库。为了使系统的设计更为合理,在这种情况下,便可以使用享元模式为每个公司分别提供工资查询的接口,而一个公司下的所有员工可以共享一个查询。这样,系统只需要3个享元实例,就足以应付300个员工的查询请求了。

  

public interface IReportManager {
    public String createReport();
}

   

public class FinancialReportManager implements IReportManager{

    protected String tenantId = null;

    public FinancialReportManager(String tenantId) { // 租户ID
        this.tenantId = tenantId;
    }
    @Override
    public String createReport() {
        return "This is a financial report";
    }
}

   

public class EmployeeReportManager implements IReportManager{

    protected String tenantId = null;

    public EmployeeReportManager(String tenantId) {
        this.tenantId = tenantId;
    }

    @Override
    public String createReport() {
        return "This is a employee report";
    }
}
public class ReportManagerFactory {
    Map<String, FinancialReportManager> financialReportManager = new HashMap<>();
    Map<String, EmployeeReportManager> employeeReportManager = new HashMap<>();

    IReportManager getFinancialReportManager(String tenantId) {
        FinancialReportManager r = financialReportManager.get(tenantId);
        if(null == r) {
            r = new FinancialReportManager(tenantId);
            financialReportManager.put(tenantId, r);
        }
        return r;
    }

    IReportManager getEmployeeReportManager(String tenantId) {
        EmployeeReportManager r = employeeReportManager.get(tenantId);
        if(null == r) {
            r = new EmployeeReportManager(tenantId);
            employeeReportManager.put(tenantId, r);
        }
        return r;
    }

}

     ReportManagerFactory做为享元工厂,以租客ID为索引,维护了一个享元对象的集合,它确保相同租客的请求都返回同一个享元实例,确保享元对象的有效复用。

2.1.4装饰者模式(Decorator Pattern)

   装饰者模式拥有一个设计非常巧妙的机构,它可以动态添加对象功能。在基本的设计原则中, 有一个重要的设计准则叫做合成/聚合复用原则。根据该原则的思想,代码复用应该尽可能使用委托,而不是使用继承。因为继承是一种紧密耦合,任何父类的改动都会影响其子类,不利于系统维护。而委托则是松散耦合,只要接口不变,委托类的改动并不会影响其上层对象

  装饰者模式充分运用了这种思想, 通过委托机制,复用系统中的各个组件,在运行时,可以将这些功能组件进行叠加,从而构造一个“超级对象”,使其拥有所有这些组件的功能。而各个子功能模块,被很好地维护在各个组件的相关类中,拥有整洁的系统结构。

 △ 注意:装饰者模式可以有效分离性能组件和功能组件,从而提升模块的可维护性并增加模块的复用性

装饰者(Decorator)和被装饰者(ConcreteCompoonent)拥有相同的接口Component。被装饰者通常是系统的核心组件,完成待定的功能目标。而装饰者则可以在被装饰者的方法前后,加上特定的前置处理和后置处理,增强被装饰者的功能。

 装饰者模式的主要角色如下

组件接口:组件接口是装饰者和被装饰者的超类或者接口。它定义了被装饰者的核心功能和装饰者需要加强的功能点

具体组件:具体组件实现了组件接口的核心方法,完成某一个具体的业务逻辑。它也是被装饰的对象

装饰者:实现组件接口,并持有一个具体的被装饰者对象

具体装饰者:具体实现装饰的业务逻辑,即实现了被分离的各个增强功能点。各个具体装饰者是可以相互叠加的,从而可以构成一个功能强大的组件对象

      装饰者模式的一个典型案例就是对输出结果进行增强。比如,现在需要将某一个结果通过HTML进行发布,那么首先就需要将内容转化为一个HTML文本。同时,由于内容需要在网络上通过http流传,故,还需要为其增加http头。当然,做为一个更复杂的情况,可能还要为其安置TCP头等。但做为一个示例,这里做简化处理。

    装饰者模式的核心思想在于:无需将所有的逻辑,即,核心内容构建,HTML文本构造和HTTP头生成等3个功能模块粘合在一起实现 。通过装饰者模式,可以将它们分解为3个几乎完全独立的组件,并在使用时灵活地进行装配。

public interface IPacketCreator {

    public String handleContent(); // 用于内容处理
}
public class PacketBodyCreator implements IPacketCreator{

    @Override
    public String handleContent() {
        return "Contect of Packet";// 构造核心数据,但不包括格式
    }
}
/**
 * PacketHTMLHeaderCreator是具体的装饰器,它负责对核心发布的内容进行HTML格式化
 * 操作。需要特别注意的是,它委托了具体组件Component进行核心业务处理
 */
public class PacketHTMLHeaderCreator extends PacketDecorator{
    public PacketHTMLHeaderCreator(IPacketCreator c) {
        super(c);
    }

    @Override
    public String handleContent() {
        StringBuffer sb = new StringBuffer();
        sb.append("<html>");
        sb.append("<body>");
        sb.append(component.handleContent());
        sb.append("</body>");
        sb.append("</html>");
        return sb.toString();
    }
}
public class PacketHTTPHeaderCreator extends PacketDecorator{

    public PacketHTTPHeaderCreator(IPacketCreator c){
        super(c);
    }

    @Override
    public String handleContent() {
        StringBuffer sb = new StringBuffer();
        sb.append("Cache-Control:no-cache\n");
        sb.append("Date:Mon,31Dec201808:24:59GMT\n");
        sb.append(component.handleContent());
        return sb.toString();
    }
}
    public static void main(String[] args) {
        IPacketCreator pc = new PacketHTTPHeaderCreator(
                new PacketHTMLHeaderCreator(new PacketBodyCreator()));
        System.out.println(pc.handleContent());
    }

  可以看到, 通过装饰者的构造函数,将被装饰者对象传入。本例中,共生成3个对象实例,做为核心组件的PacketBodyCreator最先被构造,其次是PacketHTMLHeaderCreator和PacketHTTPHeaderCreator

  在JDK实现中,有不少组件也是用装饰者模式实现。其中,一个最典型的例子就是OutputStream和InputStream类的实现。以OutputStream为例,OutputStream对象提供的方法比较简单,功能也比较弱,但通过各种装饰者的增强,OutputStream对象可以被赋予强大的功能

2.1.5 观察者模式(Observer Pattern)

观察者模式是一种非常常用的设计模式。在软件系统中, 当一个对象的行为依赖于另一个对象的状态时,观察者模式相当有用。若不使用观察者模式提供的通用结构,而需要实现类似的功能,则只能在另一个线程中不停去监听对象依赖的状态。在一个复杂系统中,可能会因此开启很多线程来实现这一功能,这将使系统的性能产生额外的负担。观察者模式的意义就在此,他可以在单线程中,使用某一对象,及时得知自身所依赖的对象的状态变化。

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

    观察者模式的主要角色:

   主题接口:指被观察的对象。当其状态发生变化或者事件发生时,它会将这个变化通知给观察者。它维护了观察者所需要的依赖的状态

   具体主题:具体主题实现了主题接口中的方法。如新增,删除,和通知观察者。其内部维护一个观察者列表

   观察者接口:观察者接口定义了观察者的基本方法。当依赖的状态发生改变时,主题接口就会调用观察者的更新方法

   具体观察者:实现了观察者接口的方法,具体处理当被观察者状态发生改变或者某一事件发生时的业务逻辑

/***
 * 抽象观察者
 * 定义了一个update()方法,当被观察者调用notifyObservers()方法时,观察者的update()方法会被回调。
 *
 */
public interface IObserver {
    void update(String msg);
}
/***
 * 抽象被观察者接口
 * 声明了添加、删除、通知观察者方法
 */
public interface ISubject {
    void attach(IObserver observer);// 添加
    void detach(IObserver observer);// 删除
    void inform();
}
/**
 * 被观察者
 * 实现了ISubject接口,对ISubject接口的三个方法进行了具体实现
 */
public class ConcreteSubject implements ISubject{
    Vector<IObserver> observers = new Vector<>();
    private String msg;

    @Override
    public void attach(IObserver observer) {
        observers.addElement(observer);
    }

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

    @Override
    public void inform() {
        observers.forEach(o -> o.update(msg));
    }

    public void setInfromation(String s) {
        this.msg = s;
        System.out.println("信息更新:" + s);
        inform();
    }
}
/**
 * Created by depuli on 18-9-6.
 */
public class User implements IObserver{

    private String name;
    private String msg;

    public User(String name) {
        this.name = name;
    }

    @Override
    public void update(String msg) {
        this.msg = msg;
        read();
    }

    public void read() {
        System.out.println(name+"收到信息: " + msg);
    }

}
   public static void main(String[] args) {
        ConcreteSubject server = new ConcreteSubject();
        server.attach(new User("li"));
        server.attach(new User("wang"));
        server.attach(new User("chen"));

        server.setInfromation("java是世界上最好的语言");
    }
信息更新:java是世界上最好的语言
li收到信息: java是世界上最好的语言
wang收到信息: java是世界上最好的语言
chen收到信息: java是世界上最好的语言

观察者模式是如此常用,以至于JDK内部就已经为开发人员准备了一套观察者模式的实现。它位于java.util包中,包括Observable类和Observer接口。在JDK中,观察者模式也得到了普遍的应用。一个最典型的应用便是Swing框架的JButton实现。

2.1.6 Value Object模式

在J2ee软件开发中,通常会对系统模块进行分层。展示层主要负责数据的展示,定义数据库的UI组织模式;业务逻辑层负责具体的业务逻辑处理;持久层通常指数据库以及相关操作。在一个大型系统中,这些层次很有可能被分离,并部署在不同的服务器上。而在两个层次之间,可能通过远程过程调用RMI(远程方法调用(Remote Method Invocation))等方式进行通信。

 基于以上模式的通信方式是一种可行的解决方案,但是它存在两个严重的问题;

(1)对于获取一个订单对象而言, 这个操作模式略显繁琐,且不具备较好的可维护性。

  (2)  前后累计进行了3次客户端与服务端的通信,性能成本较高。

   为了解决这两个问题,就可以使用ValueObject模式。ValueObject模式提倡将一个对象的各个属性进行封装,将封装后的对象在网络中传递,从而使系统拥有更好的交互模型,并且减少网络同行数据,从而提高系统性能。使用ValueObject模式对以上结构进行改良,定于对象Order,由Order对象维护客户名,商品名和数量等信息,而Order对象也就是ValueObjec,它必须是一个可串行化的对象。

2.1.7 业务代理模式

Value Object模式是将远程调用的传递数据封装在一个可串行化的对象中进行传输,而业务代理模式则是将一组由远程方法调用构成的业务流程,封装在一个位于展示层的代理类中。比如,如果用户需要修改一个订单,订单修改操作可细分为3个自操作:

□校验用户

□获取旧的订单信息

□更新订单

以上结构存在两个问题:

(1)当展示层存在大量的并发线程时,这些线程都会直接进行远程方法调用,进而会家中网络负担;

(2)由于缺乏对订单修改操作流程的有效封装,如果将来修改流程发生变化,那么展示层需要修改。

为了有效解决以上两个问题,可以在展示层中加入业务代理对象,业务代理对象负责远程服务器通信,完成订单修改操作。而业务代理对象只暴露简单的update()方法,订单修改操作供展示层组件使用。

 

                                                    2.2 常用优化组件和方法

       本节主要介绍可用于系统性能优化的组件和性能优化思想,重点介绍缓冲和缓存这两个组件以及它们的使用方法。此外,还将介绍集中常见的优化思想,如池化对象,并行代理串行,负载均衡,以及时间换空间和空间换时间。

  2.2.1缓冲(Buffer)

       缓冲区是一块特定的内存区域。开辟缓冲区的目的是通过缓解应用程序上下层之间的性能差异,提高系统的性能。在日常生活中,缓冲的一个典型就是漏斗

       上层系统如茶壶,下层系统如水瓶,现在需要将茶壶中的水倒入到水瓶中去,这就有如将内存中的数据写入硬盘一样。茶壶的出水速度可以很快,但是水瓶的瓶口很细,因此形成性能瓶颈。要将水全部倒入瓶中,必须等待瓶口的水缓缓流下。为了加快速度,可以使用一个漏斗(缓冲)。漏斗的初始口径很大,并且拥有一定的容量,因此,茶壶中的水可以先倒入漏斗中,就有如内存数据先写入一块缓冲区。只要漏斗的容量够大,茶壶的水很快就能够倒完。至此,上层的系统已经完成工作,可以去处理其它的业务逻辑了,而此时,水并没有完全进入瓶中,而大部分被积压在漏斗中。这就是可以由下层系统慢慢去处理,直到水完全进入瓶中,漏斗(缓冲区)被清空

     △ 注意:缓冲可以协调上层组件和下层组件的性能差。当上层组件性能优于下层组件时,可以有效减少上层组件对下层组件的等待时间

      缓冲最常见的场景就是提高I/0的速度。因此,JDK不少I/O组件都提供了缓冲功能。

2.2.2缓存(Cache)

      缓存(Cache)也是一块为提升系统性能而开辟的内存空间。缓存的主要作用是暂存数据处理结果,并提供下次访问使用。在很多场合,数据的处理或者数据获取可能会非常费时,当对这个数据的请求量很大时,频繁的数据处理会耗尽CPU资源。缓存的作用就是将这些来之不易的数据处理结果暂存起来,当有其它线程或者客户端需要查询相同的数据资源时,可以省略对这些数据的处理流程,而直接从缓存中获取处理结果,并立即返回给请求组件,以此提高系统的响应时间。

     缓存的使用非常普遍,比如,目前流行的几种浏览器都会在本地缓存远程的页面,从而减少远程HTTP访问次数,加快网页的加载速度。又比如,在服务端的系统开发中,设计人员可以为一些核心API加上缓存,从而提高系统的整体性能

2.2.3对象复用-“池”

    对象池化,是目前非常常用的一种系统优化技术。它的核心思想是,如果一个类被频繁使用,那么不必每次都生成一个实例,可以将这个类的一些实例保存在一个“池”中,带需要使用的时候直接从池中获取。在实现细节上,它可能是一个数组,一个链表或者任何集合类

    对象池的使用也非常广泛,其中最为大家所熟悉的,就是线程池和数据库连接池。线程池中, 保存着可以被重用的线程对象,当有任务被提交到线程池时,系统并不需要创建新的线程,而是从池中获取一个可用的线程去执行这个任务。在任务结束后,也不关闭线程,而是将它返回到池中,等待下次被其它任务所调用。由于线程的创建和销毁是较为费时的工作,因此,在线程调度频繁的系统中,线程池可用很好的改善性能。        

2.2.4并行替代串行(易懂..略过..)

    参考第四章

2.2.5 负载均衡

   对于大型应用来说,系统负载可能非常重。以网站应用为例,如果并发数很多,则单台计算机就无法承受,此时,为保证应用程序的服务质量,需要使用多台计算机协同合作,将系统负载尽可能均匀地分配到各个计算机节点上面。

   一个典型的实现便是Tomcat集群。配置Tomcat集群实现负载均衡,可以通过Apache服务器实现。即,使用Apache服务器做为负载分配器,将请求转向各个服务器节点,从而实现负载均衡。在使用Tomcat集群时,有两种基本的Session共享模式。黏性Session模式和赋值Session模式

   在黏性的Session模式下,所有的session信息被均匀分配在各个节点上,以实现负载均衡,但是一旦一个节点宕机,它所维护的Session信息将丢失,不具备高可用性,且同一用户只能与一台Tomcat交互,因为其它节点不保存该用户信息。

   而使用复制Session模式,将使得所有的Session在所有的节点上保持一致。当一个节点上的Session信息被篡改,这个Session会被广播到其它的节点上,以保持Session同步。这样,当用户的下一次请求被分配到其它的Tomcat节点上时,将拥有足够信息系处理用户请求。这样做的坏处是,很容易引起网络繁忙,引起系统效率

2.2.6 时间换空间

    由于系统资源是有限的,为了在有限的资源内,达成某些特定的性能目标,就需要使用时间换空间或者空间换时间的方法。

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

     下例是一个简单的时间换空间的算法,实现了a,b两个变量的值交换。交换两个变量最常用的方法是使用一个中间变量,而引入额外的变量意味着要使用更多的空间。   

        int a = 3;
        int b = 2;

        // 实现a<=>b相互交互值
        int c = a;
        b = a;
        a = c;

        // 不浪费空间的算法如下
        a = a + b;
        b = a - b;
        a = a - b;

 

2.2.7 空间换时间(易懂..略过..)

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值