帶着新人看java虛擬機06(多線程篇)


  其實多線程還有很多的東西要說,我們慢慢來,可能會有一些東西沒說到,那就沒辦法了,只能說盡量吧!

  

1.synchronized關鍵字

  說到多線程肯定離不開這個關鍵字,為什么呢?因為多線程之間雖然有各自的棧和PC計數器,但是也有一些區域是共享的(堆和方法區),這些共享的區域就不可避免的造成一些問題,比如一個線程對共享區的一個變量進行修改時,此時另外一個線程也要對這個數據進行修改,就會出現同步問題,到底是以哪個線程為主呢?

  最常見的可能就是銀行轉賬了,假如我就100塊,我要向朋友小明轉賬100塊,由於轉賬可能需要一些時間,所以這個時候我的賬戶余額顯示的還是100塊!於是我趁着這段時間立馬又向小紅轉賬100塊,也會成功,於是最后我的賬戶余額會變成負100塊,這顯然就是不對的。

  於是我們可以用.synchronized關鍵字來修飾這個方法,讓這個方法在多線程的情況下,一次只能被一個線程操作,在這個線程用完這個方法之前,其他的線程不可以調用這個方法,這就是鎖;

  你想一下,你回到你自己的房間里就把們反鎖了,別人肯定就進不來了啊!

  .關於synchronized關鍵字的用法大概就幾種

  1.1.同步方法

      同步方法很顯然是加在方法上面,注意位置,要放在返回類型前面:

  

  1.2.同步代碼塊

    我們可以用synchronized將方法中的代碼都包起來,這種和上面那種是等同的;

 

    但是有的時候我們不想整個方法都用synchronized包起來,因為這樣的效率很低(注意,synchronized關鍵字修飾的區域越小效率越高),我們可以把其中一些主要的邏輯給包起來:

  

  1.3.分析

    我們說一說那個this代表什么意思,也可改成其他的么?當然可以改成其他的,類型隨意,隨便什么都行,字符串,對象,或者User.class等等,那到底有什么用呢?

    怎么說呢,我就說說我的理解吧!我將synchronized(){}這樣的看作是一把鎖,而括號里面的this就是鎖芯,假如有兩個鎖芯相同的鎖,那么鑰匙肯定是一樣的!

    根據這個道理,我們說說多線程調用這個代碼塊的規則,首先,一個程序中有很多的同步代碼塊(也就是鎖),每一個鎖都對應有一個鎖芯,有可能多個鎖的鎖芯相同;而每個線程默認擁有全部鎖的鑰匙,這個時候一個線程打開一把鎖之后進去,立刻把門反鎖鎖死,而且這個反鎖比較牛逼,能把所有相同鎖芯的門都反鎖鎖死,其他線程即使擁有這種鎖芯的鑰匙也是打不開的,但是可以打開其他類型的鎖;

    反正我是這樣理解的,如果你有更好的理解方式最好用自己的理解方式;

    順便看一看下面的的簡單代碼:

package com.wyq.thread;

public class Bank {
    public void toMoney(int money){
        synchronized (this) {
            System.out.println("轉賬金額:"+money);
            
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
        }
        
    }
    public void save(int num){
        synchronized (this) {
            System.out.println("存錢:"+num);
        }
        
    }
    
    
    
    public static void main(String[] args) {
        Bank bank = new Bank();
        
        new Thread(new Runnable() {
            
            @Override
            public void run() {
                bank.toMoney(100);
            }
        }).start();
        
        new Thread(new Runnable() {
            
            @Override
            public void run() {
                bank.save(200);
            }
        }).start();
        
        
    }
}
View Code

 你們覺得執行的結果怎么是什么?答案:由於這兩個鎖的鎖芯是一個型號的,所以先是轉賬方法執行,執行完畢之后才是存錢方法

 

 

  現在我們把存錢方法的鎖芯換成Bank.class試試效果,可以看到這兩個互不影響;

 

  這說明了一個線程執行一個同步代碼塊時,該類中的其他鎖芯相同的同步代碼塊就會被鎖死;但是其他鎖芯的方法還是可以正常使用;

  但是你們有沒有想過假如把synchronized關鍵字加到靜態方法上會怎么樣呢?有興趣的可以試試,我直接說一下結論,下面兩種是等效的:

 

2.線程的生命周期

  前面我們說了這么多都是說的多線程的用法,我一直的理念就是學新知識先不要看概念什么的,先會熟練運用,用多了再理解概念會很深刻1

  我們就隨便看看線程的生命周期吧!

  我們每次都是調用xxx.start()方法表示本線程已經准備就緒,CPU可以隨時的過來運行這個線程,難道線程一創建就直接是准備就緒的嗎?話說線程是什么時候創建的啊?是是在new Thread()的時候?還是調用start()方法得時候?難道是CPU來調用這個線程的時候再創建線程嗎?emmmm,是不是感覺比較模糊啊,那么接下來我們就簡單看看一個線程從創建到死亡到底經歷了哪些階段?

  線程從出生到死亡分為五個狀態,我們根據圖來看看五個狀態分別是干什么的:

 

  新建(NEW):注意:我們在代碼中 new Thread(xxx)的時候只是創建一個普通的java對象,還沒有創建線程,只有調用的start()方法的時候jvm才會真正的開始新建線程;

  准備就緒(Runnable):調用start()方法線程也創建了之后,此線程不會馬上被CPU調用,會進入准備就緒狀態等待CPU過來;在start()方法內部才是真正開始創建線程!!!

  運行(Running):當CPU調用這個線程的時候這個線程就處於運行狀態,並且開始執行run()方法內部的邏輯

  阻塞(Blocked):正處於運行狀態的線程由於一些原因被終止了,線程就進入阻塞狀態,也可以我們人為的讓線程阻塞!注意:阻塞狀態下的線程無法被CPU在此調用,除非想辦法讓阻塞狀態變成准備就緒狀態才有可能被CPU調用。

  死亡(Terminated):線程死亡,要么是線程run()方法正常執行完畢,要么就是執行這個線程的時候出現了什么問題被迫死亡。。。

 隨意看看start()方法的源碼,非常少,很好理解:

 public synchronized void start() {
//線程新創建時threadStatus=0,這里是判斷當前線程是不是新建的,如果不是,就拋出一個異常
if (threadStatus != 0) throw new IllegalThreadStateException(); //將此線程添加到組中,這個組的類型是ThreadGroup,其實在這個組里面就是維持了一個Thread[]數組,用於保存我們將要運行的線程 group.add(this); boolean started = false; try {
//調用下面的那個本地方法,並設置運行的標志為true start0(); started
= true; } finally { try { if (!started) {
//假如線程運行的標志設置失敗,就拋出線程開始失敗異常 group.threadStartFailed(
this); } } catch (Throwable ignore) { } } } //這是一個本地方法也就是JNI方法,用C++實現的,所以我們這里看不到任何實現,但是可以猜想這里面會創建線程,分配線程的內存空間,
//然后就是等待CPU的調度,內部就會調用我們創建線程的run()方法
private native void start0();

  由於本人對C++不熟悉,那個JNI的方法源碼就不獻丑了!假如有小伙伴對那個JNI方法很有興趣的話,可以參考一個老哥的博客: https://www.jianshu.com/p/81a56497e073,沒興趣的話就算了。。。

  

  其實我感覺分析到了這一步就差不多了,稍微提一下,看了start()方法的源碼,問一個很有趣的問題,假如我們創建線程的時候多次調用start()方法會怎么樣呢?例如下面:

  

  答案是,多次調用start()方法就會報錯,看上面源碼的第一個注釋那里,試想一下,對於同一個Thread對象,第一次調用的start()方法之后線程可能就被CPU調用了,就不再是新建狀態了,我們再進行start()方法肯定就會拋異常啊!異常信息我也截一下圖看看:

  但是啊,假如要改的話怎么改呢?要么就把for循環去掉,要么for循環就把Thread thread = new Thread(xxx)這一段東西也包含進去,使得每一次都是新建一個線程去調用start()方法,那就不會報錯了!

 

 3.JMM和JVM的區別

  這兩個很像,但是對於新手來說第一次看到這兩個還真是懵懵分不清楚,首先jvm指的是java虛擬機(我們一般也叫做jvm的內存模型),一般指的是jvm組成部分,其實就是我們前面說的那幾個部分組成,java棧,java堆,方法區等等,這里就不多說了;

  那么JMM又是一個什么鬼呢?我們看一句話:Java線程之間的通信由Java內存模型(簡稱為JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見!這句話的意思就是對於多線程來說,線程之間的通信就是由JMM控制,而JMM是一個java內存模型,其實就是線程對共享區數據讀取和寫入的一個模型!

  我隨便借了一張java內存模型(簡稱JMM)示意圖:

 

  方便我們理解,我們可以把主內存看作jvm中的共享區(java堆+方法區),假如我們創建的一個線程要從共享區中的數據進行修改,看一下一個簡單的例子:

  想一想為什么不是按照順序打印的啊?你看,還有的打印居然重復了,有兩個0,這真是日了狗了,按理來說創建了20個線程,每一個線程都會拿到i進行+1錯作,並打印,每個數字應該只會出現一次啊!

  不是按照順序打印的很好理解,因為線程的執行順序是CPU隨機調度的嘛,但是重復的數據就不能理解了,於是這就要看看我們上面的JMM模型了;

  首先是每個線程都有自己的私有內存空間(棧+PC計數器),其實還有一個私有的空間叫做線程的工作空間,這其實就是起到一個緩存的作用,因為CPU在調度線程的時候由於CPU運算速度太快,從內存中讀取的話也很慢,很浪費時間,於是有了緩存這個作為CPU和內存之間的緩沖!

  假如一個線程要從共享區拿到數據,首先會將這個數據復制一份到這個線程的工作空間,然后CPU調用這個線程的時候其實就是CPU對這個線程工作空間的數據進行運算,將運算后的結果覆蓋工作空間原來的值,等這個線程進入死亡狀態的時候,線程工作空間的值就會回寫到主存中覆蓋原有的值。

  是不是覺得一切都很好,然而這里卻很有問題,假如線程A將共享區的變量I=0復制到線程A的工作空間,然后CPU操作,對i=i+1,此時i=1但是由於線程還有其他邏輯要處理,還沒有來得及寫入到共享區覆蓋原有值0,另外一個線程B也創建了,並且也將共享區的i=0讀到B線程的內存空間,然后也對i=i+1,此時i也是等於1,然后線程A、B都將自己的值寫入共享區覆蓋原來的i,此時共享區中i=1;很坑,明明進行了兩個線程的錯作,i的值卻只是增加了一次;

  要怎么解決這個問題呢?這里簡單介紹兩種方式,第一種就是前面說的synchronized關鍵字,我們可以看看效果:

  第二種方案是用volatile關鍵字,這個關鍵字修飾一個成員變量,存放在共享區,這個成員變量就相當於暴露在所有的線程眼中,只要有線程對這個變量進行什么修改,所有的線程都會知道,然后也會相應的進行修改;

  舉個例子:一個變量被volatile修飾,假如有一個線程讀取了這個變量的值,並在該線程的工作空間中進行了修改,那么馬上就會回寫到共享區,其他線程如果也要用到這個共享區變量,就要直接從從共享區中拿,這樣可以保證拿到的數據始終都是最新的,當然這樣做其實就是相當於去掉了線程工作區間的緩存作用,所以會影響性能。。。。

  我們看看一個例子:

 

  注意:從這里開始可能會有點不好理解了,友軍可以跳過!!!

  那么我們就要問了,這個線程的工作空間在哪里呢?是不是在線程私有空間的棧中還是PC計數器中呢?

  答案是:沒有這個所謂的線程工作空間,這是一個虛擬模型,實際系統中並沒有直接的對應;我找了很多的資料,我也是看的雲里霧里,我把那些資料給大家看看,看過就好,不要深究;

  答案1:“工作內存”是一個虛擬模型,實際系統中並沒有直接的對應。java官方文檔中也沒有“工作空間”這個概念,應該是有人為了讓讀者理解java支持的非常松散的內存一致性模型才提出來的。有點誤人子弟。這個“工作內存”是各種CPU架構支持的內存模型跟編譯器的各種優化而產生的一個效果,並沒有工作內存跟主內存相互拷貝的實際動作;

  答案2:“工作緩沖區”是一個抽象的概念,JVM只是規范了主存和線程內存的變量訪問時候的需要滿足的規范(如可見性等),並沒有對這個緩沖區做實現上的限制。也就是說,把共享變量從主存拷貝到線程的工作內存后,具體放在哪里,取決於具體的虛擬機實現,只要滿足JMM規范,至於具體放在哪里(寄存器,內存Cache等等),其實也沒關系的;

  答案3:對於JMM和JVM本身的內存模型,這兩者並沒有關系;如果一定要對應,那就從變量、主內存、工作空間的定義來看,主內存主要定義java堆中對象實例數據部分,而工作內存則對應於虛擬機棧中的部分區域;從更低的層次來看,主內存就是物理內存,而為了獲取更好的執行速度,虛擬機(甚至硬件系統本身的優化措施)可能會讓工作內存由於存儲與寄存器和高速緩存中,因為運行時主要訪問的是工作內存

 

 

總結:

   估計后面會繼續說說線程,還有好多東西啊,比如怎么人為的去將線程由運行狀態變為阻塞狀態,然后想辦法再把阻塞狀態變為准備就緒狀態,還有其他的各種鎖,以及一些概念性的東西,慢慢來吧!

  假如有小伙伴想看書學習多線程的話,可以參看一本叫做《JAVA多線程設計模式》,電子檔鏈接:https://pan.baidu.com/s/1ng_bAGE-ieNUZoczFHCnJA    提取碼:sqz3 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM