이 기사에서는 성능과 관련된 디자인 패턴을 주로 소개하는 java에 대한 관련 지식을 제공합니다. 대부분의 디자인 패턴은 코드를 구성하는 방법일 뿐이며 Agent 모드, Singleton 모드, 플라이웨이트 모드, 프로토타입 모드 등 모두에게 도움이 되기를 바랍니다.
추천 학습: "java 비디오 튜토리얼"
코드 구조는 애플리케이션의 전반적인 성능에 중요한 영향을 미칩니다. 뛰어난 구조를 가진 코드는 많은 잠재적인 성능 문제를 방지하고 코드의 확장성에 큰 역할을 할 수 있습니다. 명확한 구조와 명확한 레이어를 가진 코드는 시스템의 병목 현상을 찾아 특별한 최적화를 수행하는 데도 도움이 될 수 있습니다.
디자인 패턴은 프로그래머가 보다 전문적이고 편리한 방식으로 문제를 전달할 수 있도록 하는 일반적인 개발 기술을 요약한 것입니다.
사실 대부분의 디자인 패턴은 프로그램의 성능을 높이는 것이 아니라 코드를 정리하는 방법일 뿐입니다. 이 기사에서는 프록시 모드, 싱글턴 모드, 플라이웨이트 모드, 프로토타입 모드 등 성능과 관련된 여러 디자인 패턴을 설명하기 위해 예제를 하나씩 제공합니다.
프록시 모드(프록시)는 프록시 클래스를 통해 객체에 대한 액세스를 제어할 수 있습니다.
Java에서 동적 프록시를 구현하는 데에는 두 가지 주요 모드가 있습니다. 하나는 JDK를 사용하는 것이고, 다른 하나는 CGLib를 사용하는 것입니다. 그 중 JDK 메소드는 인터페이스 지향적이며 주요 관련 클래스는 InvocationHandler 및 Proxy입니다. CGLib는 일반 클래스를 프록시할 수 있으며 주요 관련 클래스는 MethodInterceptor 및 Enhancer입니다.
이 지식 포인트에 대한 인터뷰 빈도가 매우 높습니다.
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)); }}
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)); }}
다음은 JDK 모드 및 CGLib 모드 프록시 속도에 대한 JMH 테스트 결과입니다.
Benchmark | Mode | Cnt | S 코어 | 오류 | 단위 |
---|---|---|---|---|---|
ProxyBenchmark.cglib | thrpt | 10 | 78499.580 | ±1771.148 | ops/ms |
ProxyBenchmark.jdk | thrpt | 10 | 88948.858 | ±814.360 | ops/ms |
현재 사용하고 있는 JDK 버전은 1.8입니다. CGLib의 속도는 그다지 빠르지 않은 것을 알 수 있습니다(10배 빠르다고 합니다).
프록시 생성 속도를 살펴보겠습니다. 프록시 클래스 초기화 측면에서 JDK의 처리량이 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 广泛使用了代理模式,它使用 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。
下面使用 arthas 分析这个执行过程,找出耗时最高的 AOP 方法。启动 arthas 后,可以从列表中看到我们的应用程序,在这里,输入 1 进入分析界面。
在终端输入 trace 命令,然后访问 /aop 接口,终端将打印出一些 debug 信息,可以发现耗时操作就是 Spring 的代理类。
trace cn.wja.spring.ABean method
Spring 在创建组件的时候,可以通过 scope 注解指定它的作用域,用来标示这是一个prototype(多例)还是 singleton(单例)。
当指定为单例时(默认行为),在 Spring 容器中,组件有且只有一份,当你注入相关组件的时候,获取的组件实例也是同一份。
如果是普通的单例类,我们通常将单例的构造方法设置成私有的,单例有懒汉加载和饿汉加载模式。
了解 JVM 类加载机制的同学都知道,一个类从加载到初始化,要经历 5 个步骤:加载、验证、准备、解析、初始化。
其中,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 的关键代码,我们介绍一下四个关键点:
可以看到,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中的大对象 里的对象复用等。
在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不是同一个对象");}
设计模式对这我们平常的编码进行了抽象,从不同的角度去解释设计模式,都会找到设计思想的一些共通点。比如,单例模式就是享元模式的一种特殊情况,它通过共享单个实例,达到对象的复用。
值得一提的是,同样的代码,不同的解释,会产生不同的效果。比如下面这段代码:
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视频教程》
위 내용은 Java의 성능 관련 디자인 패턴에 대해 이야기해 보겠습니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!