摘要:問題復現(xiàn)假設線上是一個典型的項目,某一塊業(yè)務的處理邏輯為接受一個字符串參數(shù),然后將該值賦予給一個注入的對象,修改對象的屬性后再返回,期間我們用了來模擬線上的高耗時業(yè)務代碼如下上述的也非常簡單,一個普通的對象具體代碼如下所示相信使用過的伙伴們
問題復現(xiàn)
假設線上是一個典型的Spring Boot Web項目,某一塊業(yè)務的處理邏輯為:
接受一個name字符串參數(shù),然后將該值賦予給一個注入的bean對象,修改bean對象的name屬性后再返回,期間我們用了 Thread.sleep(300) 來模擬線上的高耗時業(yè)務
代碼如下:
@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也非常簡單,一個普通的Spring Service對象
具體代碼如下所示:
@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的伙伴們對這段代碼不會有什么疑問,實際運行也沒有問題,測試也能跑通,但真的上線后,里面卻會產(chǎn)生一個線程安全問題
不相信的話,我們通過線程池,開200個線程來測試NameController就可以復現(xiàn)出來
測試代碼如下
@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); } }
這段測試代碼,啟動200個線程,對NameController進行測試,每一個線程將自己的線程名作為參數(shù)提交,并對返回結果進行斷言,如果返回的值與提交的值不匹配,那么拋出AssertNotEquals異常
實際測試后,我們可以發(fā)現(xiàn)200個線程近乎一半以上都會拋出異常
問題產(chǎn)生原因首先我們來分析一下,當一個線程,向 http://localhost:8080/name 發(fā)出請求時,線上的Spring Boot服務,會通過其內置的Tomcat 8.5來接收這個請求
而在Tomcat 8.5中,默認采用的是NIO的實現(xiàn)方式,及每次請求對應一個服務端線程,然后這個服務端的線程,再分配到對應的servlet來處理請求
所以我們可以認為,這并發(fā)的200次客戶端請求,進入NameController執(zhí)行請求的,也是分為200個不同的服務端線程來處理
但是Spring提供的Bean對象,并沒有默認實現(xiàn)它的線程安全性,即默認狀態(tài)下,我們的NameController跟NameService都屬于單例對象
這下應該很好解釋了,200個線程同時操作2個單例對象(一個NameController對象,一個NameService對象),在沒有采用任何鎖機制的情況下,不產(chǎn)生線程安全問題是不可能的(除非是狀態(tài)無關性操作)
問題解決辦法按照標題說明的,我這里提供三種解決辦法,分別是
synchronized修飾方法
synchronized代碼塊
改變bean對象的作用域
接下來對每個解決辦法進行說明,包括他們各自的優(yōu)缺點
synchronized修飾方法使用synchronized來是修飾可能會產(chǎn)生線程安全問題的方法,應該是我們最容易想到的,同時也是最簡單的解決辦法,我們僅僅需要在 public String changeAndReadName (@RequestParam String name) 這個方法上,增加一個synchronized進行修飾即可
實際測試,這樣確實能解決問題,但是各位是否可以再思考一個問題
我們再來運行測試代碼的時候,發(fā)現(xiàn)程序運行效率大大降低,因為每一個線程必須等待前一個線程完成changeAndReadName()方法的所有邏輯后才可以運行,而這段邏輯中,就包含了我們用來模擬高耗時業(yè)務的 Thread.sleep(300) ,但它跟我們的線程安全沒有什么關系
這種情況下,我們就可以使用第二種方法來解決問題
synchronized代碼塊實際的線上邏輯,經(jīng)常會遇到這樣的情況:我們需要確保線程安全的代碼,跟高耗時的代碼(比如說調用第三方api),很不湊巧的寫在同一個方法中
那么這種情況下,使用synchronized代碼塊,而不是直接修飾方法會來得高效的多
具體解決代碼如下:
@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; }
再次運行測試代碼,我們可以發(fā)現(xiàn)效率問題基本解決,但是缺點是需要我們自己把握好哪一塊是可能出現(xiàn)線程安全問題的代碼(而實際的線上邏輯可能非常復雜,這一塊不好把握)
改變bean對象的作用域現(xiàn)在非常不幸的事情發(fā)生了,我們連高耗時代碼也是狀態(tài)相關性的,而同時也需要保證效率問題,那么這種情況下就只能通過犧牲少量的內存來解決問題了
大概思路就是通過改變bean對象的作用域,讓每一個服務端線程對應一個新的bean對象來處理邏輯,通過彼此之間互不相關來回避線程安全問題
首先我們需要知道bean對象的作用域有哪些,請見下表
作用域 | 說明 |
---|---|
singleton | 默認的作用域,這種情況下的bean都會被定義為一個單例對象,該對象的生命周期是與Spring IOC容器一致的(但出于Spring懶加載機制,只有在第一次被使用時才會創(chuàng)建) |
prototype | bean被定義為在每次注入時都會創(chuàng)建一個新的對象 |
request | bean被定義為在每個HTTP請求中創(chuàng)建一個單例對象,也就是說在單個請求中都會復用這一個單例對象 |
session | bean被定義為在一個session的生命周期內創(chuàng)建一個單例對象 |
application | bean被定義為在ServletContext的生命周期中復用一個單例對象 |
? ? ? ? ? ? ? websocket ? ? ? ? ? ? ? | bean被定義為在websocket的生命周期中復用一個單例對象 |
清楚bean對象的作用域后,接下來我們就只需要考慮一個問題:修改哪些bean的作用域?
前面我已經(jīng)解釋過,這個案例中,200個服務端線程,在默認情況下是操作2個單例bean對象,分別是NameController和NameService(沒錯,在Spring Boot下,Controller默認也是單例對象)
那么是不是直接將NameController和NameServie設置為prototype就可以了呢?
如果您的項目是用的Struts2,那么這樣做沒有任何問題,但是在Spring MVC下會嚴重影響性能,因為Struts2對請求的攔截是基于類,而Spring MVC則是基于方法
所以我們應該將NameController的作用域設置為request,將NameService設置為prototype來解決
具體操作代碼如下
@RestController @RequestMapping("name") @Scope("request") public class NameController { }
@Service @Scope("prototype") public class NameService { }參考文獻
https://dzone.com/articles/un...
https://dzone.com/articles/un...
https://medium.com/sipios/how...
原創(chuàng)不易,轉載請申明出處
案例項目代碼: github/liumapp/booklet
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/75824.html
摘要:一直以來,前端的線上問題很難定位,因為它發(fā)生于用戶的一系列操作之后。當然,這些問題并非不能克服,讓我們來一起看看如何去定位線上的問題吧。地址參考一步一步搭建前端監(jiān)控系統(tǒng)錯誤監(jiān)控篇一步一步搭建前端監(jiān)控系統(tǒng)接口請求異常監(jiān)控篇 摘要: 記錄用戶行為,排查線上BUG。 作者:一步一個腳印一個坑 原文:如何定位前端線上問題(如何排查前端生產(chǎn)問題) Fundebug經(jīng)授權轉載,版權歸原作者所...
摘要:如問到是否使用某框架,實際是是問該框架的使用場景,有什么特點,和同類可框架對比一系列的問題。這兩個方向的區(qū)分點在于工作方向的側重點不同。 [TOC] 這是一份來自嗶哩嗶哩的Java面試Java面試 32個核心必考點完全解析(完) 課程預習 1.1 課程內容分為三個模塊 基礎模塊: 技術崗位與面試 計算機基礎 JVM原理 多線程 設計模式 數(shù)據(jù)結構與算法 應用模塊: 常用工具集 ...
摘要:是框架對的默認集成,我們在實際項目中,也經(jīng)常使用它的去操作,一般來說沒什么問題,但是細心一點的同學會發(fā)現(xiàn),經(jīng)過這種方法寫入的數(shù)據(jù)會出現(xiàn)亂碼問題問題復現(xiàn)項目依賴配置文件配置配置類注入設置數(shù)據(jù)存入的序列化方式實例化對象可以 org.springframework.data.redis是Spring框架對Redis的默認集成,我們在實際項目中,也經(jīng)常使用它的RedisTemplate去操作R...
摘要:使用的好處知乎的回答不用自己組裝,拿來就用。統(tǒng)一配置,便于修改。 前言 只有光頭才能變強 回顧前面: 給女朋友講解什么是代理模式 包裝模式就是這么簡單啦 單例模式你會幾種寫法? 工廠模式理解了沒有? 在刷Spring書籍的時候花了點時間去學習了單例模式和工廠模式,總的來說還是非常值得的! 本來想的是刷完《Spring 實戰(zhàn) (第4版)》和《精通Spring4.x 企業(yè)應用開發(fā)實戰(zhàn)》...
摘要:前言一些問題的整理,平時實際工作中可能會忽視的一些原理性問題,后續(xù)會選取一些有意思的點進行詳述。 前言 一些問題的整理,平時實際工作中可能會忽視的一些原理性問題,后續(xù)會選取一些有意思的點進行詳述。 JAVA多線程、并發(fā)相關 多個線程同時讀寫,讀線程的數(shù)量遠遠?于寫線程,你認為應該如何解決 并發(fā)的問題?你會選擇加什么樣的鎖? JAVA的AQS是否了解,它是?嘛的? 除了synchron...
閱讀 3058·2021-11-16 11:45
閱讀 3578·2021-09-29 09:34
閱讀 702·2021-08-16 10:50
閱讀 1569·2019-08-30 15:52
閱讀 1962·2019-08-30 15:45
閱讀 859·2019-08-29 15:23
閱讀 1923·2019-08-26 13:51
閱讀 3299·2019-08-26 12:23