熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java高級技術 >> 正文

Java多線程初學者指南(9):為什麼要進行數據同步

2022-06-13   來源: Java高級技術 

  Java中的變量分為兩類局部變量和類變量局部變量是指在方法內定義的變量如在run方法中定義的變量對於這些變量來說並不存在線程之間共享的問題因此它們不需要進行數據同步類變量是在類中定義的變量作用域是整個類這類變量可以被多個線程共享因此我們需要對這類變量進行數據同步

  數據同步就是指在同一時間只能由一個線程來訪問被同步的類變量當前線程訪問完這些變量後其他線程才能繼續訪問這裡說的訪問是指有寫操作的訪問如果所有訪問類變量的線程都是讀操作一般是不需要數據同步的

  那麼如果不對共享的類變量進行數據同步會發生什麼情況呢?讓我們先看看下面的代碼會發生什麼樣的事情

   package test;

public class MyThread extends Thread
{
    public static int n = ;

    public void run()
    {
        int m = n;
        yield();
        m++;
        n = m;
    }
    public static void main(String[] args) throws Exception
    {
        MyThread myThread = new MyThread ();
        Thread threads[] = new Thread[];
        for (int i = ; i < threadslength; i++)
            threads[i] = new Thread(myThread);
        for (int i = ; i < threadslength; i++)
            threads[i]start();
        for (int i = ; i < threadslength; i++)
            threads[i]join();
        Systemoutprintln(n =  + MyThreadn);
    }
}

  在執行上面代碼的可能結果如下

   n = 

  看到這個結果可能很多讀者會感到奇怪這個程序明明是啟動了個線程然後每個線程將靜態變量n加最後使用join方法使這個線程都運行完後再輸出這個n值按正常來講結果應該是n = 可偏偏結果小於

  其實產生這種結果的罪魁禍首就是我們經常提到的髒數據而run方法中的yield()語句就是產生髒數據的始作俑者(不加yield語句也可能會產生髒數據但不會這麼明顯只有將改成更大的數才會經常產生髒數據在本例中調用yield就是為了放大髒數據的效果)yield方法的作用是使線程暫停也就是使調用yield方法的線程暫時放棄CPU資源使CPU有機會來執行其他的線程為了說明這個程序如何產生髒數據我們假設只創建了兩個線程thread和thread由於先調用了thread的start方法因此thread的run方法一般會先運行當thread的run方法運行到第一行(int m = n)時將n的值賦給m當執行到第二行的yield方法後thread就會暫時停止執行而當thread暫停時thread獲得了CPU資源後開始運行(之前thread一直處於就緒狀態)當thread執行到第一行(int m = n)時由於thread在執行到yield時n仍然是因此thread中的m獲得的值也是這樣就造成了thread和thread的m獲得的都是在它們執行完yield方法後都是從開始加因此無論誰先執行完最後n的值都是只是這個n被thread和thread各賦了一遍值這個過程如下圖如示

  

  也許有人會問如果只有n++會產生髒數據嗎?答案是肯定的那麼n++只是一條語句又如何在執行過程中將CPU交給其他的線程呢?其實這只是表面現象n++在被Java編譯器編譯成中間語言(也叫做字節碼)後並不是一條語言讓我們看看下面的Java代碼將會被編譯成什麼樣的Java中間語言

  Java源代碼

   public void run()
{
    n++;
}

  被編譯後的中間語言代碼

       public void run()
    {
        aload_         
        dup             
        getfield
        iconst_        
        iadd            
        putfield       
        return          
    }

  大家可以看到在run方法中只有n++一條語句而在編譯後卻有條中間語言語句我們並不需要知道這些語句的功能是什麼只看一下第行語句行是getfield根據它的英文含義可知是要得到某個值因為這裡只有一個n所以毫無疑問是要得到n的值而在行的iadd也不難猜測是將這個得到的n值加行的putfield的含義我想大家可能已經猜出來了它負責將這個加後的n再更新回類變量n說到這可能大家還有一個疑惑執行n++時直接將n加不就行了為什麼要如此費周折其實這裡涉及到一個Java內存模型的問題

  Java的內存模型分為主存儲區和工作存儲區主存儲區保存了Java中所有的實例也就是說在我們使用new來建立一個對象後這個對象及它內部的方法變量等都保存在這一區域在MyThread類中的n就保存在這個區域主存儲區可以被所有線程共享而工作存儲區就是我們前面所講的線程棧在這個區域裡保存了在run方法以及run方法所調用的方法中定義的變量也就是方法變量在線程要修改主存儲區中的變量時並不是直接修改這些變量而是將它們先復制到當前線程的工作存儲區在修改完後再將這個變量值覆蓋主存儲區的相應的變量值

  在了解了Java的內存模型後就不難理解為什麼n++也不是原子操作了它必須經過一個拷貝和覆蓋的過程這個過程和在MyThread類中模擬的過程類似大家可以想象如果在執行到getfield時thread由於某種原因被中斷那麼就會發生和MyThread類的執行結果類似的情況要想徹底解決這個問題就必須使用某種方法對n進行同步也就是在同一時間只能有一個線程操作n這也稱為對n的原子操作


From:http://tw.wingwit.com/Article/program/Java/gj/201311/27623.html
  • 上一篇文章:

  • 下一篇文章:
  • 推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.