1. 程式人生 > >多執行緒引發執行緒安全問題的考慮和在javaWEB專案及SSM框架的java專案中的場景分析

多執行緒引發執行緒安全問題的考慮和在javaWEB專案及SSM框架的java專案中的場景分析

    當今世界是一個快速發展的社會,快速發展的好處就是我們不需要了解汽車原理,不需要知道怎麼樣去造輪子,只要你有錢,你就可以享用這一切。

    多執行緒的問題在我們初學者的世界裡就顯得尤為突出,看似不合理卻又合理的一個現象時,我們在初學java時多會接觸多執行緒,而且在面試的時候,面試官一定會問到多執行緒的問題,就像下面的場景一樣:

  • 面試官:瞭解多執行緒嗎?
  • A:瞭解。
  • 面試官:講講如何開啟一個執行緒?執行緒同步的方式
  • 這時候A巴拉巴拉講了一堆,什麼繼承Threa,實現Runable,程式碼塊加同步鎖等等。。。。。
  • 面試官點點頭,小夥骨骼清奇,不錯不錯,你被錄用了。

    然後A就開始來上班,每天沒日沒夜的加班寫業務程式碼,改bug,在某一個安靜的下午,A幹完了所有的事,然後開始沉思,回想自己面試之初的事,自己當時就是因為多執行緒掌握的好所以才被錄取,但是在實際中丁點多執行緒都沒用到,這是為什麼?於是A就開始查資料,備好了小本本做好了大幹一番的準備。

究竟什麼情況下會使用多執行緒?

    這個問題其實很好回答,在java的中這個東西還不太明顯,尤其是初學者,沒有效能方面的考慮根本不會考慮到使用多執行緒,心裡大都抱著這樣的念頭,明明這樣寫可以解決問題而且這麼簡單,為什麼非要去寫個多執行緒簡直是多此一舉。如果你開發過Android應用程式,那麼你就一定能理解為什麼要使用多執行緒了,同樣的Android使用java語言來編寫,嵌入式裝置同PC的差別在於網路資源(尤其是4G網路沒出現之前)和硬體資源的不足,當然現在的嵌入式裝置已經很強大了,但是這些規範卻一直保留。在Android中像網路訪問等一些耗時操作一定不能在主執行緒(UI執行緒)中執行,如果在主執行緒中發生網路訪問程式直接會拋異常。這是因為網訪問等一些耗時的操作存在不確定性,首先是耗時的問題,耗時會阻塞主執行緒導致介面無法顯示,互動不友好,耗時還好,等一會介面展示出來了,但是如果失敗了,等了一會,結果啥也沒有空白一片,這不就悲劇了,所以這個問題也可以衍生到我們j2ee開發之中,我們為了保證應用程式的健壯和穩定,把一些耗時操作,或者當前不需要立即執行的操作,如上傳圖片,使用者積分增加等這些操作在單獨的執行緒中執行。這些都是我們自己在程式碼中顯式的指定的。

什麼情況下會發生執行緒安全的問題?

    正是因為有了多執行緒所以才會出現執行緒安全的考慮,執行緒安全的問題是基於這樣的一個事實所引發的,只有當不同的執行緒在操作同一個資料或物件的時候才會發生,也就是說即使有多執行緒的操作如果執行緒之間資料不共享那麼就不會引發執行緒安全的問題。

    查了幾個小時這兩個問題都被解決了,但是A發現了一個奇怪的事,為什麼在用了框架和容器之後我之前學的東西都用不到了,而且網上關於這方面的資料少到可憐,A開始懷疑自己是不是百度的姿勢不對?

    但是功夫不負有心人,終於讓A查到了原因。於是乎A趕緊寫到了自己的小本本中以防自己忘記。

多執行緒在javaweb專案中的使用場景

應用會是上面介紹的一些通用場景,還有就是一些被容器和框架處理掉的一些場景。

我們來分析這樣一個場景:引發執行緒安全的一個例子。

新建一個SpringBoot專案,編寫測試的Controller。

@RestController
@RequestMapping("hello")
public class HelloController {

    private String username;
    private String password;

    @GetMapping("/say")
    public String hello(String username, String password) throws InterruptedException {
        this.username = username;
        this.password = password;
        Thread.sleep(1000);
        System.out.println(this.username + this.password);
        return "hello world!";
    }

}

    使用jemeter來進行測試,jemeter是Apache的壓力測試軟體,這裡只是用兩個sampler各發起五次請求,來測試執行緒安全問題。具體如下圖
這裡寫圖片描述

測試結果如下:
這裡寫圖片描述

程式碼加鎖

private String username;
    private String password;

    @GetMapping("/say")
    public synchronized String hello(String username, String password) throws InterruptedException {
        this.username = username;
        this.password = password;
        Thread.sleep(1000);
        System.out.println(this.username + this.password);
        return "hello world!";
    }

測試結果如下:

這裡寫圖片描述

    沒有加鎖導致多個執行緒訪問了同一個物件的同一個成員,造成資料混亂。加鎖後只能一個一個去執行,所以資料正常。但是這裡產生了一個很大的問題是請求等待,加鎖後一個請求一定要等到另外一個請求結束後釋放鎖才能執行被加鎖的程式碼。

    那麼在我們的時間生產中,肯定不能出現這種場景,如果一千個使用者來登入,那麼都要一個個來排隊那這個程式基本就廢了。我們繼續看下面一個例子。

@RestController
@RequestMapping("hello")
public class HelloController {



    @Autowired
    private HelloService helloService;

    @GetMapping("/say")
    public String hello(String username, String password) throws InterruptedException {
        Thread.sleep(1000);
        System.out.println(helloService.hashCode());
        return helloService.login(username, password);
    }

}

然後再進行和上面相同的測試,測試結果如下圖:
這裡寫圖片描述

這時候我們發現沒有發生執行緒的問題,這到底是為什麼?
    我們從列印的hashcode中看到spring注入的bean是單列的,併發的十個請求過來都是同一個service在處理但是並沒有發生執行緒安全問題。

    可能聰明的你已經發現這兩塊程式碼之間的不同了,兩者都將物件共享給了其他的執行緒,前者是在方法中直接操作了兩個物件,後者是呼叫了共享物件的方法。

    這時候你不禁會問這又有什麼關係呢?確實好像沒什麼鳥關係,但是,我們要認識到我們每一次http都是被tomcat接收和處理,tomcat本身是支援多執行緒的,tomcat處理請求的機制是:每一次從外界流入的請求都必將經過connector,任何一次從本地流出的響應資料也都將經過connector,這正是聯結器的意義所在,連線客戶端和服務端servlet,下圖展示tomcat元件體系簡圖;
這裡寫圖片描述

    回到問題本身,在tomcat中servlet是單例項的,只有在服務啟動的時候建立,服務停止的時候銷燬,tomcat本身執行緒池的概念,預設最大支援200個連線,tomcat的執行緒池預設建立五個執行緒,儲存在一個長度200的執行緒陣列中,處理請求的過程遵循先到先得的原則,只有當併發請求的數量超過最大連線數(預設兩百)時,會進入執行緒等待。當請求到達時,servlet容器通過排程執行緒(dispatchaer Thread)排程它管理下的執行緒池中等待執行的執行緒(Work Thread)給請求者。

    當併發請求出現,並且訪問同一個servlet的時候,其實servlet只有一個,但是由於tomcat支援多執行緒的原因,每個客戶端執行的servlet中的函式都是在自己所支配的那一小段執行緒裡面執行的,對應程式碼中,都訪問hello的時候,雖然只有一個servlet,但是函式是放在各自執行緒中的,互不干擾。

    這裡需要注意的時只有servlet中的函式是被執行緒放到各自的執行緒中執行了,執行緒安全的本質是資料未同步,ssm框架這種設計使用的規範完美的規避了這個問題,一級一級往下,各司其職,通過方法呼叫和引數傳遞,讓每個請求的資料都在各自的執行緒中執行,不會引起多個請求的資料造成的混亂,而且Springmvc本身就是servlet額高度封裝。

在這種框架之下我們唯一要注意的就是資料庫了,對於資料庫的問題這裡暫不說明。