成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

復(fù)現(xiàn)一個(gè)典型的線上Spring Bean對(duì)象的線程安全問題(附三種解決辦法)

joy968 / 3522人閱讀

摘要:?jiǎn)栴}復(fù)現(xiàn)假設(shè)線上是一個(gè)典型的項(xiàng)目,某一塊業(yè)務(wù)的處理邏輯為接受一個(gè)字符串參數(shù),然后將該值賦予給一個(gè)注入的對(duì)象,修改對(duì)象的屬性后再返回,期間我們用了來(lái)模擬線上的高耗時(shí)業(yè)務(wù)代碼如下上述的也非常簡(jiǎn)單,一個(gè)普通的對(duì)象具體代碼如下所示相信使用過的伙伴們

問題復(fù)現(xiàn)

假設(shè)線上是一個(gè)典型的Spring Boot Web項(xiàng)目,某一塊業(yè)務(wù)的處理邏輯為:

接受一個(gè)name字符串參數(shù),然后將該值賦予給一個(gè)注入的bean對(duì)象,修改bean對(duì)象的name屬性后再返回,期間我們用了 Thread.sleep(300) 來(lái)模擬線上的高耗時(shí)業(yè)務(wù)

代碼如下:

@RestController
@RequestMapping("name")
public class NameController {

    @Autowired
    private NameService nameService;

    @RequestMapping("")
    public String changeAndReadName (@RequestParam String name) throws InterruptedException {
        System.out.println("get new request: " + name);
        nameService.setName(name);
        Thread.sleep(300);
        return nameService.getName();
    }

}

上述的nameService也非常簡(jiǎn)單,一個(gè)普通的Spring Service對(duì)象

具體代碼如下所示:

@Service
public class NameService {

    private String name;

    public NameService() {
    }

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

    public String getName() {
        return name;
    }

    public NameService setName(String name) {
        this.name = name;
        return this;
    }
}

相信使用過Spring Boot的伙伴們對(duì)這段代碼不會(huì)有什么疑問,實(shí)際運(yùn)行也沒有問題,測(cè)試也能跑通,但真的上線后,里面卻會(huì)產(chǎn)生一個(gè)線程安全問題

不相信的話,我們通過線程池,開200個(gè)線程來(lái)測(cè)試NameController就可以復(fù)現(xiàn)出來(lái)

測(cè)試代碼如下

    @Test
    public void changeAndReadName() throws Exception {
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(200, 300 , 2000, TimeUnit.SECONDS, new ArrayBlockingQueue(200));
        for (int i = 0; i < 200; i++) {
            poolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName() + " begin");
                        Map headers = new HashMap();
                        Map querys = new HashMap();

                        querys.put("name", Thread.currentThread().getName());
                        headers.put("Content-Type", "text/plain;charset=UTF-8");
                        HttpResponse response = HttpTool.doGet("http://localhost:8080",
                                "/name",
                                "GET",
                                headers,
                                querys);
                        String res = EntityUtils.toString(response.getEntity());

                        if (!Thread.currentThread().getName().equals(res)) {
                            System.out.println("WE FIND BUG !!!");
                            Assert.assertEquals(true, false);
                        } else {
                            System.out.println(Thread.currentThread().getName() + " get received " + res);
                        }
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        while(true) {
            Thread.sleep(100);
        }
    }

這段測(cè)試代碼,啟動(dòng)200個(gè)線程,對(duì)NameController進(jìn)行測(cè)試,每一個(gè)線程將自己的線程名作為參數(shù)提交,并對(duì)返回結(jié)果進(jìn)行斷言,如果返回的值與提交的值不匹配,那么拋出AssertNotEquals異常

實(shí)際測(cè)試后,我們可以發(fā)現(xiàn)200個(gè)線程近乎一半以上都會(huì)拋出異常

問題產(chǎn)生原因

首先我們來(lái)分析一下,當(dāng)一個(gè)線程,向 http://localhost:8080/name 發(fā)出請(qǐng)求時(shí),線上的Spring Boot服務(wù),會(huì)通過其內(nèi)置的Tomcat 8.5來(lái)接收這個(gè)請(qǐng)求

而在Tomcat 8.5中,默認(rèn)采用的是NIO的實(shí)現(xiàn)方式,及每次請(qǐng)求對(duì)應(yīng)一個(gè)服務(wù)端線程,然后這個(gè)服務(wù)端的線程,再分配到對(duì)應(yīng)的servlet來(lái)處理請(qǐng)求

所以我們可以認(rèn)為,這并發(fā)的200次客戶端請(qǐng)求,進(jìn)入NameController執(zhí)行請(qǐng)求的,也是分為200個(gè)不同的服務(wù)端線程來(lái)處理

但是Spring提供的Bean對(duì)象,并沒有默認(rèn)實(shí)現(xiàn)它的線程安全性,即默認(rèn)狀態(tài)下,我們的NameController跟NameService都屬于單例對(duì)象

這下應(yīng)該很好解釋了,200個(gè)線程同時(shí)操作2個(gè)單例對(duì)象(一個(gè)NameController對(duì)象,一個(gè)NameService對(duì)象),在沒有采用任何鎖機(jī)制的情況下,不產(chǎn)生線程安全問題是不可能的(除非是狀態(tài)無(wú)關(guān)性操作)

問題解決辦法

按照標(biāo)題說(shuō)明的,我這里提供三種解決辦法,分別是

synchronized修飾方法

synchronized代碼塊

改變bean對(duì)象的作用域

接下來(lái)對(duì)每個(gè)解決辦法進(jìn)行說(shuō)明,包括他們各自的優(yōu)缺點(diǎn)

synchronized修飾方法

使用synchronized來(lái)是修飾可能會(huì)產(chǎn)生線程安全問題的方法,應(yīng)該是我們最容易想到的,同時(shí)也是最簡(jiǎn)單的解決辦法,我們僅僅需要在 public String changeAndReadName (@RequestParam String name) 這個(gè)方法上,增加一個(gè)synchronized進(jìn)行修飾即可

實(shí)際測(cè)試,這樣確實(shí)能解決問題,但是各位是否可以再思考一個(gè)問題

我們?cè)賮?lái)運(yùn)行測(cè)試代碼的時(shí)候,發(fā)現(xiàn)程序運(yùn)行效率大大降低,因?yàn)槊恳粋€(gè)線程必須等待前一個(gè)線程完成changeAndReadName()方法的所有邏輯后才可以運(yùn)行,而這段邏輯中,就包含了我們用來(lái)模擬高耗時(shí)業(yè)務(wù)的 Thread.sleep(300) ,但它跟我們的線程安全沒有什么關(guān)系

這種情況下,我們就可以使用第二種方法來(lái)解決問題

synchronized代碼塊

實(shí)際的線上邏輯,經(jīng)常會(huì)遇到這樣的情況:我們需要確保線程安全的代碼,跟高耗時(shí)的代碼(比如說(shuō)調(diào)用第三方api),很不湊巧的寫在同一個(gè)方法中

那么這種情況下,使用synchronized代碼塊,而不是直接修飾方法會(huì)來(lái)得高效的多

具體解決代碼如下:

    @RequestMapping("")
    public String changeAndReadName (@RequestParam String name) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + " get new request: " + name);
        String result = "";
        synchronized (this) {
            nameService.setName(name);
            result = nameService.getName();
        }
        Thread.sleep(300);
        return result;
    }

再次運(yùn)行測(cè)試代碼,我們可以發(fā)現(xiàn)效率問題基本解決,但是缺點(diǎn)是需要我們自己把握好哪一塊是可能出現(xiàn)線程安全問題的代碼(而實(shí)際的線上邏輯可能非常復(fù)雜,這一塊不好把握)

改變bean對(duì)象的作用域

現(xiàn)在非常不幸的事情發(fā)生了,我們連高耗時(shí)代碼也是狀態(tài)相關(guān)性的,而同時(shí)也需要保證效率問題,那么這種情況下就只能通過犧牲少量的內(nèi)存來(lái)解決問題了

大概思路就是通過改變bean對(duì)象的作用域,讓每一個(gè)服務(wù)端線程對(duì)應(yīng)一個(gè)新的bean對(duì)象來(lái)處理邏輯,通過彼此之間互不相關(guān)來(lái)回避線程安全問題

首先我們需要知道bean對(duì)象的作用域有哪些,請(qǐng)見下表

作用域 說(shuō)明
singleton 默認(rèn)的作用域,這種情況下的bean都會(huì)被定義為一個(gè)單例對(duì)象,該對(duì)象的生命周期是與Spring IOC容器一致的(但出于Spring懶加載機(jī)制,只有在第一次被使用時(shí)才會(huì)創(chuàng)建)
prototype bean被定義為在每次注入時(shí)都會(huì)創(chuàng)建一個(gè)新的對(duì)象
request bean被定義為在每個(gè)HTTP請(qǐng)求中創(chuàng)建一個(gè)單例對(duì)象,也就是說(shuō)在單個(gè)請(qǐng)求中都會(huì)復(fù)用這一個(gè)單例對(duì)象
session bean被定義為在一個(gè)session的生命周期內(nèi)創(chuàng)建一個(gè)單例對(duì)象
application bean被定義為在ServletContext的生命周期中復(fù)用一個(gè)單例對(duì)象
? ? ? ? ? ? ? websocket ? ? ? ? ? ? ? bean被定義為在websocket的生命周期中復(fù)用一個(gè)單例對(duì)象

清楚bean對(duì)象的作用域后,接下來(lái)我們就只需要考慮一個(gè)問題:修改哪些bean的作用域?

前面我已經(jīng)解釋過,這個(gè)案例中,200個(gè)服務(wù)端線程,在默認(rèn)情況下是操作2個(gè)單例bean對(duì)象,分別是NameController和NameService(沒錯(cuò),在Spring Boot下,Controller默認(rèn)也是單例對(duì)象)

那么是不是直接將NameController和NameServie設(shè)置為prototype就可以了呢?

如果您的項(xiàng)目是用的Struts2,那么這樣做沒有任何問題,但是在Spring MVC下會(huì)嚴(yán)重影響性能,因?yàn)镾truts2對(duì)請(qǐng)求的攔截是基于類,而Spring MVC則是基于方法

所以我們應(yīng)該將NameController的作用域設(shè)置為request,將NameService設(shè)置為prototype來(lái)解決

具體操作代碼如下

@RestController
@RequestMapping("name")
@Scope("request")
public class NameController {

}
@Service
@Scope("prototype")
public class NameService {

}
參考文獻(xiàn)

https://dzone.com/articles/un...

https://dzone.com/articles/un...

https://medium.com/sipios/how...

原創(chuàng)不易,轉(zhuǎn)載請(qǐng)申明出處

案例項(xiàng)目代碼: github/liumapp/booklet

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/75824.html

相關(guān)文章

  • 一步一步搭建前端監(jiān)控系統(tǒng):如何定位前端線上問題

    摘要:一直以來(lái),前端的線上問題很難定位,因?yàn)樗l(fā)生于用戶的一系列操作之后。當(dāng)然,這些問題并非不能克服,讓我們來(lái)一起看看如何去定位線上的問題吧。地址參考一步一步搭建前端監(jiān)控系統(tǒng)錯(cuò)誤監(jiān)控篇一步一步搭建前端監(jiān)控系統(tǒng)接口請(qǐng)求異常監(jiān)控篇 摘要: 記錄用戶行為,排查線上BUG。 作者:一步一個(gè)腳印一個(gè)坑 原文:如何定位前端線上問題(如何排查前端生產(chǎn)問題) Fundebug經(jīng)授權(quán)轉(zhuǎn)載,版權(quán)歸原作者所...

    aaron 評(píng)論0 收藏0
  • Java面試 32個(gè)核心必考點(diǎn)完全解析

    摘要:如問到是否使用某框架,實(shí)際是是問該框架的使用場(chǎng)景,有什么特點(diǎn),和同類可框架對(duì)比一系列的問題。這兩個(gè)方向的區(qū)分點(diǎn)在于工作方向的側(cè)重點(diǎn)不同。 [TOC] 這是一份來(lái)自嗶哩嗶哩的Java面試Java面試 32個(gè)核心必考點(diǎn)完全解析(完) 課程預(yù)習(xí) 1.1 課程內(nèi)容分為三個(gè)模塊 基礎(chǔ)模塊: 技術(shù)崗位與面試 計(jì)算機(jī)基礎(chǔ) JVM原理 多線程 設(shè)計(jì)模式 數(shù)據(jù)結(jié)構(gòu)與算法 應(yīng)用模塊: 常用工具集 ...

    JiaXinYi 評(píng)論0 收藏0
  • Spring-RedisTemplate寫入數(shù)據(jù)亂碼問題復(fù)現(xiàn)解決

    摘要:是框架對(duì)的默認(rèn)集成,我們?cè)趯?shí)際項(xiàng)目中,也經(jīng)常使用它的去操作,一般來(lái)說(shuō)沒什么問題,但是細(xì)心一點(diǎn)的同學(xué)會(huì)發(fā)現(xiàn),經(jīng)過這種方法寫入的數(shù)據(jù)會(huì)出現(xiàn)亂碼問題問題復(fù)現(xiàn)項(xiàng)目依賴配置文件配置配置類注入設(shè)置數(shù)據(jù)存入的序列化方式實(shí)例化對(duì)象可以 org.springframework.data.redis是Spring框架對(duì)Redis的默認(rèn)集成,我們?cè)趯?shí)際項(xiàng)目中,也經(jīng)常使用它的RedisTemplate去操作R...

    李世贊 評(píng)論0 收藏0
  • Spring IOC知識(shí)點(diǎn)一網(wǎng)打盡!

    摘要:使用的好處知乎的回答不用自己組裝,拿來(lái)就用。統(tǒng)一配置,便于修改。 前言 只有光頭才能變強(qiáng) 回顧前面: 給女朋友講解什么是代理模式 包裝模式就是這么簡(jiǎn)單啦 單例模式你會(huì)幾種寫法? 工廠模式理解了沒有? 在刷Spring書籍的時(shí)候花了點(diǎn)時(shí)間去學(xué)習(xí)了單例模式和工廠模式,總的來(lái)說(shuō)還是非常值得的! 本來(lái)想的是刷完《Spring 實(shí)戰(zhàn) (第4版)》和《精通Spring4.x 企業(yè)應(yīng)用開發(fā)實(shí)戰(zhàn)》...

    djfml 評(píng)論0 收藏0
  • 互聯(lián)網(wǎng)后端知識(shí)點(diǎn)整理

    摘要:前言一些問題的整理,平時(shí)實(shí)際工作中可能會(huì)忽視的一些原理性問題,后續(xù)會(huì)選取一些有意思的點(diǎn)進(jìn)行詳述。 前言 一些問題的整理,平時(shí)實(shí)際工作中可能會(huì)忽視的一些原理性問題,后續(xù)會(huì)選取一些有意思的點(diǎn)進(jìn)行詳述。 JAVA多線程、并發(fā)相關(guān) 多個(gè)線程同時(shí)讀寫,讀線程的數(shù)量遠(yuǎn)遠(yuǎn)?于寫線程,你認(rèn)為應(yīng)該如何解決 并發(fā)的問題?你會(huì)選擇加什么樣的鎖? JAVA的AQS是否了解,它是?嘛的? 除了synchron...

    kaka 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<