這篇文章主要介紹“Java中性能相關(guān)的設(shè)計模式有哪些”,在日常操作中,相信很多人在Java中性能相關(guān)的設(shè)計模式有哪些問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Java中性能相關(guān)的設(shè)計模式有哪些”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
創(chuàng)新互聯(lián)是一家專業(yè)提供宿松企業(yè)網(wǎng)站建設(shè),專注與成都做網(wǎng)站、成都網(wǎng)站設(shè)計、H5高端網(wǎng)站建設(shè)、小程序制作等業(yè)務(wù)。10年已為宿松眾多企業(yè)、政府機(jī)構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專業(yè)網(wǎng)站建設(shè)公司優(yōu)惠進(jìn)行中。
代碼的結(jié)構(gòu)對應(yīng)用的整體性能,有著重要的影響。結(jié)構(gòu)優(yōu)秀的代碼,可以避免很多潛在的性能問題,在代碼的擴(kuò)展性上也有巨大的作用;結(jié)構(gòu)清晰、層次分明的代碼,也有助于幫你找到系統(tǒng)的瓶頸點(diǎn),進(jìn)行專項優(yōu)化。
設(shè)計模式就是對常用開發(fā)技巧進(jìn)行的總結(jié),它使得程序員之間交流問題,有了更專業(yè)、便捷的方式。
事實上,大多數(shù)設(shè)計模式并不能增加程序的性能,它只是代碼的一種組織方式。本文,我們將一一舉例講解和性能相關(guān)的幾個設(shè)計模式,包括代理模式、單例模式、享元模式、原型模式等。
代理模式(Proxy)可以通過一個代理類,來控制對一個對象的訪問。
Java 中實現(xiàn)動態(tài)代理主要有兩種模式:一種是使用 JDK,另外一種是使用 CGLib。 其中,JDK 方式是面向接口的,主要的相關(guān)類是 InvocationHandler 和 Proxy;CGLib 可以代理普通類,主要的相關(guān)類是 MethodInterceptor 和 Enhancer。
這個知識點(diǎn)面試頻率非常高。
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 測試結(jié)果:
Benchmark | Mode | Cnt | Score | Error | Units |
---|---|---|---|---|---|
ProxyBenchmark.cglib | thrpt | 10 | 78499.580 | ±1771.148 | ops/ms |
ProxyBenchmark.jdk | thrpt | 10 | 88948.858 | ±814.360 | ops/ms |
我現(xiàn)在用的 JDK 版本是 1.8,可以看到,CGLib 的速度并沒有傳得那么快(有傳言高出10 倍),相比較而言,它的速度甚至略有下降。
我們再來看下代理的創(chuàng)建速度,結(jié)果如下所示??梢钥吹?,在代理類初始化方面,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 的字節(jié)碼進(jìn)行了增強(qiáng)。在復(fù)雜的項目中,會有非常多的 AOP 代碼,比如權(quán)限、日志等切面。在方便了編碼的同時,AOP 也給不熟悉項目代碼的同學(xué)帶來了很多困擾。
下面我將分析一個使用 arthas 找到動態(tài)代理慢邏輯的具體原因,這種方式在復(fù)雜項目中,非常有效,你不需要熟悉項目的代碼,就可以定位到性能瓶頸點(diǎn)。
首先,我們創(chuàng)建一個最簡單的 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(); } }}
創(chuàng)建一個啟動類,當(dāng)訪問 /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; }}
訪問結(jié)果如下,可以看到 AOP 代理已經(jīng)生效,內(nèi)存里的 Bean 對象,已經(jīng)變成了EnhancerBySpringCGLIB 類型,調(diào)用方法 method,耗時達(dá)到了1005ms。
下面使用 arthas 分析這個執(zhí)行過程,找出耗時最高的 AOP 方法。啟動 arthas 后,可以從列表中看到我們的應(yīng)用程序,在這里,輸入 1 進(jìn)入分析界面。
在終端輸入 trace 命令,然后訪問 /aop 接口,終端將打印出一些 debug 信息,可以發(fā)現(xiàn)耗時操作就是 Spring 的代理類。
trace cn.wja.spring.ABean method
Spring 在創(chuàng)建組件的時候,可以通過 scope 注解指定它的作用域,用來標(biāo)示這是一個prototype(多例)還是 singleton(單例)。
當(dāng)指定為單例時(默認(rèn)行為),在 Spring 容器中,組件有且只有一份,當(dāng)你注入相關(guān)組件的時候,獲取的組件實例也是同一份。
如果是普通的單例類,我們通常將單例的構(gòu)造方法設(shè)置成私有的,單例有懶漢加載和餓漢加載模式。
了解 JVM 類加載機(jī)制的同學(xué)都知道,一個類從加載到初始化,要經(jīng)歷 5 個步驟:加載、驗證、準(zhǔn)備、解析、初始化。
其中,static 字段和 static 代碼塊,是屬于類的,在類加載的初始化階段就已經(jīng)被執(zhí)行。它在字節(jié)碼中對應(yīng)的是 方法,屬于類的(構(gòu)造方法)。因為類的初始化只有一次,所以它就能夠保證這個加載動作是線程安全的。
根據(jù)以上原理,只要把單例的初始化動作,放在方法里,就能夠?qū)崿F(xiàn)餓漢模式。
private static Singleton instace = new Singleton();
理論上來說,餓漢模式它會造成資源的浪費(fèi),可能生成一些永遠(yuǎn)不會用到的對象,因此很多教程不建議用。但實際上來說,這存粹是脫褲子放屁,如果你真的永遠(yuǎn)用不到這個對象,你為何要創(chuàng)建這個類,寫一個單例模式? 我覺得對于普通項目來說,餓漢模式就完全足夠了。
而對象初始化就不一樣了。通常,我們在 new 一個新對象的時候,都會調(diào)用它的構(gòu)造方法,就是,用來初始化對象的屬性。由于在同一時刻,多個線程可以同時調(diào)用函數(shù),我們就需要使用 synchronized 關(guān)鍵字對生成過程進(jìn)行同步。
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 的關(guān)鍵代碼,我們介紹一下四個關(guān)鍵點(diǎn):
第一次檢查,當(dāng) instance 為 null 的時候,進(jìn)入對象實例化邏輯,否則直接返回。
加同步鎖,這里是類鎖。
第二次檢查才是關(guān)鍵。如果不加這次判空動作,可能會有多個線程進(jìn)入同步代碼塊,進(jìn)而生成多個實例。
最后一個關(guān)鍵點(diǎn)是 volatile 關(guān)鍵字。在一些低版本的 Java 里,由于指令重排的緣故,可能會導(dǎo)致單例被 new 出來后,還沒來得及執(zhí)行構(gòu)造函數(shù),就被其他線程使用。 這個關(guān)鍵字,可以阻止字節(jié)碼指令的重排序,在寫 double check 代碼時,習(xí)慣性會加上 volatile。
可以看到,double check 的寫法繁雜,注意點(diǎn)很多,它現(xiàn)在其實是一種反模式,已經(jīng)不推薦使用了,我也不推薦你用在自己的代碼里。但它能夠考察面試者對并發(fā)的理解,所以這個問題經(jīng)常被問到。
代碼片段如下:
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 { //具體內(nèi)容}
享元模式(Flyweight)專門針對性能優(yōu)化的設(shè)計模式,它通過共享技術(shù),最大限度地復(fù)用對象。享元模式一般會使用唯一的標(biāo)識碼進(jìn)行判斷,然后返回對應(yīng)的對象,使用 HashMap 一類的集合存儲非常合適。
上面的描述,我們非常熟悉,因為本專欄的之前的博文中,我們就能看到很多享元模式的身影,比如博文 淺談Java中的池化技術(shù) 里的池化對象和博文 如何處理Java中的大對象 里的對象復(fù)用等。
在Java中,我們常見的Integer,為了提升效率,在創(chuàng)建[1,127]范圍內(nèi)的對象時也用了享元模式。通過下面的測試代碼可以驗證。
@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不是同一個對象");}
設(shè)計模式對這我們平常的編碼進(jìn)行了抽象,從不同的角度去解釋設(shè)計模式,都會找到設(shè)計思想的一些共通點(diǎn)。比如,單例模式就是享元模式的一種特殊情況,它通過共享單個實例,達(dá)到對象的復(fù)用。
值得一提的是,同樣的代碼,不同的解釋,會產(chǎn)生不同的效果。比如下面這段代碼:
Mapstrategys = new HashMap<>(); strategys.put("a",new AStrategy()); strategys.put("b",new BStrategy());
如果我們從對象復(fù)用的角度來說,它就是享元模式;如果我們從對象的功能角度來說,那它就是策略模式。所以大家在討論設(shè)計模式的時候,一定要注意上下文語境的這些差別。
原型模式(Prototype)比較類似于復(fù)制粘貼的思想,它可以首先創(chuàng)建一個實例,然后通過這個實例進(jìn)行新對象的創(chuàng)建。在 Java 中,最典型的就是 Object 類的 clone 方法。
但編碼中這個方法很少用,我們上面在代理模式提到的 prototype,并不是通過 clone 實現(xiàn)的,而是使用了更復(fù)雜的反射技術(shù)。
一個比較重要的原因就是 clone 如果只拷貝當(dāng)前層次的對象,實現(xiàn)的只是淺拷貝。在現(xiàn)實情況下,對象往往會非常復(fù)雜,想要實現(xiàn)深拷貝的話,需要在 clone 方法里做大量的編碼,遠(yuǎn)遠(yuǎn)不如調(diào)用 new 方法方便。
實現(xiàn)深拷貝,還有序列化等手段,比如實現(xiàn) Serializable 接口,或者把對象轉(zhuǎn)化成 JSON。
所以,在現(xiàn)實情況下,原型模式變成了一種思想,而不是加快對象創(chuàng)建速度的工具。
到此,關(guān)于“Java中性能相關(guān)的設(shè)計模式有哪些”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>
文章題目:Java中性能相關(guān)的設(shè)計模式有哪些
文章出自:http://weahome.cn/article/pcdocs.html