Heim  >  Artikel  >  Java  >  Lassen Sie uns über leistungsbezogene Entwurfsmuster in Java sprechen

Lassen Sie uns über leistungsbezogene Entwurfsmuster in Java sprechen

WBOY
WBOYnach vorne
2022-06-08 11:56:412105Durchsuche

Dieser Artikel vermittelt Ihnen relevantes Wissen über Java, in dem hauptsächlich Entwurfsmuster im Zusammenhang mit der Leistung vorgestellt werden. Die meisten Entwurfsmuster sind nur eine Möglichkeit, Code zu organisieren, und nur einige Entwurfsmuster beziehen sich auf die Leistung, einschließlich Agent-Modus, Singleton-Modus und Fliegengewicht Modus, Prototyp-Modus usw. Schauen wir sie uns gemeinsam an. Ich hoffe, dass es für alle hilfreich ist.

Lassen Sie uns über leistungsbezogene Entwurfsmuster in Java sprechen

Empfohlene Studie: „Java-Video-Tutorial

Die Struktur des Codes hat einen wichtigen Einfluss auf die Gesamtleistung der Anwendung. Code mit hervorragender Struktur kann viele potenzielle Leistungsprobleme vermeiden und eine große Rolle bei der Skalierbarkeit des Codes spielen. Code mit klarer Struktur und klaren Ebenen kann Ihnen auch dabei helfen, Engpässe im System zu finden und spezielle Optimierungen durchzuführen.

Designmuster ist eine Zusammenfassung gängiger Entwicklungstechniken. Es ermöglicht Programmierern, Probleme professioneller und bequemer zu kommunizieren.

Tatsächlich steigern die meisten Entwurfsmuster nicht die Leistung des Programms, sondern dienen lediglich der Organisation des Codes. In diesem Artikel geben wir nacheinander Beispiele, um verschiedene leistungsbezogene Entwurfsmuster zu erläutern, darunter Proxy-Modus, Singleton-Modus, Fliegengewichtsmodus, Prototyp-Modus usw.

Proxy-Modus

Der Proxy-Modus (Proxy) kann den Zugriff auf ein Objekt über eine Proxy-Klasse steuern.

Es gibt zwei Hauptmodi für die Implementierung dynamischer Proxys in Java: Der eine ist die Verwendung von JDK und der andere die Verwendung von CGLib. Unter diesen ist die JDK-Methode schnittstellenorientiert, und die wichtigsten verwandten Klassen sind InvocationHandler und Proxy. CGLib kann normale Klassen vertreten, und die wichtigsten verwandten Klassen sind MethodInterceptor und Enhancer.

Die Interviewfrequenz für diesen Wissenspunkt ist sehr hoch.

CGLib

package cn.wja.proxy.cglibproxy;import org.springframework.cglib.proxy.MethodInterceptor;import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.Method;public class CglibInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        return methodProxy.invokeSuper(o, objects);
    }}
package cn.wja.proxy.cglibproxy;import cn.wja.proxy.jdkproxy.Target;import cn.wja.proxy.jdkproxy.TargetImpl;import org.springframework.cglib.proxy.Enhancer;public class CglibFactory {

    public static Target newInstance() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(TargetImpl.class);
        enhancer.setCallback(new CglibInterceptor());
        return (Target) enhancer.create();
    }

    public static void main(String[] args) {
        Target target = newInstance();
        System.out.println(target.targetMetod(4));
    }}

JDK

package cn.wja.proxy.jdkproxy;public interface Target {
    int targetMethod(int i);}
package cn.wja.proxy.jdkproxy;public class TargetImpl implements Target {
    @Override
    public int targetMethod(int i) {
        return i * i;
    }}
package cn.wja.proxy.jdkproxy;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;public class JdkInvocationHandler implements InvocationHandler {
    private Target target;

    public JdkInvocationHandler(Target target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //before
        Object object = method.invoke(target, args);
        //after
        return object;
    }}
package cn.wja.proxy.jdkproxy;import java.lang.reflect.Proxy;public class JdkFactory {
    public static Target newInstance(Target target) {
        Object object = Proxy.newProxyInstance(JdkInvocationHandler.class.getClassLoader(),
                new Class>[]{Target.class},
                new JdkInvocationHandler(target));
        return Target.class.cast(object);
    }

    public static void main(String[] args) {
        Target t = new TargetImpl();
        Target target = newInstance(t);
        System.out.println(target.targetMethod(4));
    }}

Das Folgende sind die JMH-Testergebnisse des JDK-Modus und der CGLib-Modus-Proxy-Geschwindigkeit:

.
Benchmark Mode Cnt S-Kern Fehler Einheiten
ProxyBenchmark.cglib thrpt 10 78499.580 ±1771.148 ops/ms
ProxyBenchmark.jdk thrpt 10 88948.858 ±814.360 ops/ms

Die JDK-Version, die ich derzeit verwende, ist 1.8. Sie können sehen, dass die Geschwindigkeit von CGLib nicht so hoch ist (angeblich 10-mal höher).
Werfen wir einen Blick auf die Geschwindigkeit der Proxy-Erstellung. Die Ergebnisse sind wie folgt. Es ist ersichtlich, dass der Durchsatz von JDK in Bezug auf die Initialisierung der Proxy-Klasse doppelt so hoch ist wie der von CGLib.

Benchmark Mode Cnt Score Error Units
ProxyCreateBenchmark.cglib thrpt 10 7281.487 ± 1339.779 ops/ms
ProxyCreateBenchmark.jdk thrpt 10 15612.467 ± 268.362 ops/ms

Spring动态代理

Spring 广泛使用了代理模式,它使用 CGLIB 对 Java 的字节码进行了增强。在复杂的项目中,会有非常多的 AOP 代码,比如权限、日志等切面。在方便了编码的同时,AOP 也给不熟悉项目代码的同学带来了很多困扰。

下面我将分析一个使用 arthas 找到动态代理慢逻辑的具体原因,这种方式在复杂项目中,非常有效,你不需要熟悉项目的代码,就可以定位到性能瓶颈点。

首先,我们创建一个最简单的 Bean。

package cn.wja.spring;import org.springframework.stereotype.Component;@Componentpublic class ABean {
    public void method() {
        System.out.println("****ABean method*******************");
    }}

然后,我们使用 Aspect 注解,完成切面的书写,在前置方法里,我们让线程 sleep 了 1 秒钟。

package cn.wja.spring;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;@Aspect@Componentpublic class MyAspect {
    @Pointcut("execution(* cn.wja.spring.ABean.*(..)))")
    public void pointcut() {
    }

    @Before("pointcut()")
    public void before() {
        System.out.println("before");
        try {
            Thread.sleep(TimeUnit.SECONDS.toMillis(1));
        } catch (InterruptedException e) {
            throw new IllegalStateException();
        }
    }}

创建一个启动类,当访问 /aop 链接时,将会输出 Bean 的类名称,以及它的耗时。

package cn.wja.spring;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.scheduling.annotation.EnableAsync;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.ResponseBody;@SpringBootApplication@EnableAsync@Controllerpublic class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
    @Autowired
    private ABean aBean;

    @ResponseBody
    @GetMapping("/aop")
    public String aop() {
        long begin = System.currentTimeMillis();
        aBean.method();
        long cost = System.currentTimeMillis() - begin;
        String cls = aBean.getClass().toString();
        return cls + " | " + cost;
    }}

访问结果如下,可以看到 AOP 代理已经生效,内存里的 Bean 对象,已经变成了EnhancerBySpringCGLIB 类型,调用方法 method,耗时达到了1005ms。

Lassen Sie uns über leistungsbezogene Entwurfsmuster in Java sprechen
下面使用 arthas 分析这个执行过程,找出耗时最高的 AOP 方法。启动 arthas 后,可以从列表中看到我们的应用程序,在这里,输入 1 进入分析界面。

Lassen Sie uns über leistungsbezogene Entwurfsmuster in Java sprechen
在终端输入 trace 命令,然后访问 /aop 接口,终端将打印出一些 debug 信息,可以发现耗时操作就是 Spring 的代理类。

trace cn.wja.spring.ABean method

Lassen Sie uns über leistungsbezogene Entwurfsmuster in Java sprechen

单例模式

Spring 在创建组件的时候,可以通过 scope 注解指定它的作用域,用来标示这是一个prototype(多例)还是 singleton(单例)。

当指定为单例时(默认行为),在 Spring 容器中,组件有且只有一份,当你注入相关组件的时候,获取的组件实例也是同一份。

如果是普通的单例类,我们通常将单例的构造方法设置成私有的,单例有懒汉加载和饿汉加载模式。

饿汉模式

了解 JVM 类加载机制的同学都知道,一个类从加载到初始化,要经历 5 个步骤:加载、验证、准备、解析、初始化。
Lassen Sie uns über leistungsbezogene Entwurfsmuster in Java sprechen
其中,static 字段和 static 代码块,是属于类的,在类加载的初始化阶段就已经被执行。它在字节码中对应的是 方法,属于类的(构造方法)。因为类的初始化只有一次,所以它就能够保证这个加载动作是线程安全的。

根据以上原理,只要把单例的初始化动作,放在方法里,就能够实现饿汉模式。

private static Singleton instace = new Singleton();

理论上来说,饿汉模式它会造成资源的浪费,可能生成一些永远不会用到的对象,因此很多教程不建议用。但实际上来说,这存粹是脱裤子放屁,如果你真的永远用不到这个对象,你为何要创建这个类,写一个单例模式? 我觉得对于普通项目来说,饿汉模式就完全足够了。

饱汉模式

而对象初始化就不一样了。通常,我们在 new 一个新对象的时候,都会调用它的构造方法,就是,用来初始化对象的属性。由于在同一时刻,多个线程可以同时调用函数,我们就需要使用 synchronized 关键字对生成过程进行同步。

package cn.wja.singleton;public class DoubleCheckSingleton {
    private volatile static DoubleCheckSingleton instance = null;
    private DoubleCheckSingleton() {
    }

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

如上面是 double check 的关键代码,我们介绍一下四个关键点:

  • 第一次检查,当 instance 为 null 的时候,进入对象实例化逻辑,否则直接返回。
  • 加同步锁,这里是类锁。
  • 第二次检查才是关键。如果不加这次判空动作,可能会有多个线程进入同步代码块,进而生成多个实例。
  • 最后一个关键点是 volatile 关键字。在一些低版本的 Java 里,由于指令重排的缘故,可能会导致单例被 new 出来后,还没来得及执行构造函数,就被其他线程使用。 这个关键字,可以阻止字节码指令的重排序,在写 double check 代码时,习惯性会加上 volatile。

可以看到,double check 的写法繁杂,注意点很多,它现在其实是一种反模式,已经不推荐使用了,我也不推荐你用在自己的代码里。但它能够考察面试者对并发的理解,所以这个问题经常被问到。

推荐使用 enum 实现懒加载的单例,《Effective Java》这本书也同样推荐了该方式。代码片段如下:

package cn.wja.singleton;public class EnumSingleton {
    private EnumSingleton() {
    }

    public static EnumSingleton getInstance() {
        return Holder.HOLDER.instance;
    }

    private enum Holder {
        HOLDER;
        private final EnumSingleton instance;
        Holder() {
            instance = new EnumSingleton();
        }
    }

    public static void main(String[] args) {
        System.out.println(getInstance());
    }}

如果要借助spring框架那就更简单了:

package cn.wja.singleton;import org.springframework.context.annotation.Scope;import org.springframework.stereotype.Component;@Component@Scope("singleton")public class SpringBean {
    //具体内容}

享元模式

享元模式(Flyweight)专门针对性能优化的设计模式,它通过共享技术,最大限度地复用对象。享元模式一般会使用唯一的标识码进行判断,然后返回对应的对象,使用 HashMap 一类的集合存储非常合适。

上面的描述,我们非常熟悉,因为本专栏的之前的博文中,我们就能看到很多享元模式的身影,比如博文 浅谈Java中的池化技术 里的池化对象和博文 如何处理Java中的大对象 里的对象复用等。

案例:Integer

在Java中,我们常见的Integer,为了提升效率,在创建[1,127]范围内的对象时也用了享元模式。通过下面的测试代码可以验证。

@Testpublic void myTest() throws Exception{
    Integer a=1;
    Integer b=1;
    System.out.println(a == b ? "a b同一个对象" : "a b不是同一个对象");

    Integer c=128;
    Integer d=128;
    System.out.println(c == d ? "c d同一个对象" : "c d不是同一个对象");}

Lassen Sie uns über leistungsbezogene Entwurfsmuster in Java sprechen

多视角看问题

设计模式对这我们平常的编码进行了抽象,从不同的角度去解释设计模式,都会找到设计思想的一些共通点。比如,单例模式就是享元模式的一种特殊情况,它通过共享单个实例,达到对象的复用。

值得一提的是,同样的代码,不同的解释,会产生不同的效果。比如下面这段代码:

Map<string> strategys = new HashMap(); strategys.put("a",new AStrategy()); strategys.put("b",new BStrategy());</string>

如果我们从对象复用的角度来说,它就是享元模式;如果我们从对象的功能角度来说,那它就是策略模式。所以大家在讨论设计模式的时候,一定要注意上下文语境的这些差别。

原型模式

原型模式(Prototype)比较类似于复制粘贴的思想,它可以首先创建一个实例,然后通过这个实例进行新对象的创建。在 Java 中,最典型的就是 Object 类的 clone 方法。

但编码中这个方法很少用,我们上面在代理模式提到的 prototype,并不是通过 clone 实现的,而是使用了更复杂的反射技术。

一个比较重要的原因就是 clone 如果只拷贝当前层次的对象,实现的只是浅拷贝。在现实情况下,对象往往会非常复杂,想要实现深拷贝的话,需要在 clone 方法里做大量的编码,远远不如调用 new 方法方便。

实现深拷贝,还有序列化等手段,比如实现 Serializable 接口,或者把对象转化成 JSON。

所以,在现实情况下,原型模式变成了一种思想,而不是加快对象创建速度的工具。

推荐学习:《java视频教程

Das obige ist der detaillierte Inhalt vonLassen Sie uns über leistungsbezogene Entwurfsmuster in Java sprechen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:csdn.net. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen