Java 線程和進程,併發解決之synchronized

  1. 什麼是進程?
    程序並不能單獨運行,只有將程序裝載到內存中,系統爲它分配資源才能運行,而這種執行的程序就稱之爲進程。程序和進程的區別就在於:程序是指令的集合,它是進程運行的靜態描述文本;進程是程序的一次執行活動,屬於動態概念。
    在多道編程中,我們允許多個程序同時加載到內存中,在操作系統的調度下,可以實現併發地執行。這是這樣的設計,大大提高了CPU的利用率。進程的出現讓每個用戶感覺到自己獨享CPU,因此,進程就是爲了在CPU上實現多道編程而提出的。

  2. 有了進程爲什麼還要線程?

    進程有很多優點,它提供了多道編程,讓我們感覺我們每個人都擁有自己的CPU和其他資源,可以提高計算機的利用率。很多人就不理解了,既然進程這麼優秀,爲什麼還要線程呢?其實,仔細觀察就會發現進程還是有很多缺陷的,主要體現在兩點上:
    1. 進程只能在一個時間幹一件事,如果想同時幹兩件事或多件事,進程就無能爲力了。
    2. 進程在執行的過程中如果阻塞,例如等待輸入,整個進程就會掛起,即使進程中有些工作不依
        賴於輸入的數據,也將無法執行。

  3. 線程和進程的區別
    1. 進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。
    2. 線程是進程的一個實體, 是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。
    3. 一個線程可以創建和撤銷另一個線程,同一個進程中的多個線程之間可以併發執行。

  4. 線程的優點

    因爲要併發,我們發明了進程,又進一步發明了線程。只不過進程和線程的併發層次不同:進程屬於在處理器這一層上提供的抽象;線程則屬於在進程這個層次上再提供了一層併發的抽象。如果我們進入計算機體系結構裏,就會發現,流水線提供的也是一種併發,不過是指令級的併發。這樣,流水線、線程、進程就從低到高在三個層次上提供我們所迫切需要的併發!

    除了提高進程的併發度,線程還有個好處,就是可以有效地利用多處理器和多核計算機。現在的處理器有個趨勢就是朝着多核方向發展,在沒有線程之前,多核並不能讓一個進程的執行速度提高,原因還是上面所有的兩點限制。但如果講一個進程分解爲若干個線程,則可以讓不同的線程運行在不同的核上,從而提高了進程的執行速度。
    總結線程優點如下:

    (1)多線程技術使程序的響應速度更快 ,用戶界面可以在進行其它工作的同時一直處於活動狀態;

    (2)當前沒有進行處理的任務時可以將處理器時間讓給其它任務;

    (3)佔用大量處理時間的任務可以定期將處理器時間讓給其它任務;

    (4)可以隨時停止任務(在Android的API中沒有提供stop方法,在stop方法中只是拋出一個異常去停止);

    (5)可以分別設置各個任務的優先級以優化性能。

  5. 線程的缺點

    (1)等候使用共享資源時造成程序的運行速度變慢。這些共享資源主要是獨佔性的資源 ,如打印機等。

    (2)對線程進行管理要求額外的 CPU開銷。線程的使用會給系統帶來上下文切換的額外負擔。當這種負擔超過一定程度時,多線程的特點主要表現在其缺點上,比如用獨立的線程來更新數組內每個元素。

    (3)線程的死鎖。即較長時間的等待或資源競爭以及死鎖等多線程症狀。

    (4)對公有變量的同時讀或寫造成數據混亂,比如啓動了A,B兩個線程操作同一個變量count,有可能你想讓A先操作變量,但是當A,B線程併發的時候,有可能B先去操作count,A,B也可能交換着操作count。導致最後的結果不是預期結果

  6. 併發的解決辦法
    synchronized是Java中的關鍵字,是一種同步鎖。它修飾的對象有以下幾種: 
    1. 修飾一個代碼塊,被修飾的代碼塊稱爲同步語句塊,其作用的範圍是大括號{}括起來的代碼, 作用的對象是調用這個代碼塊的對象(實例); 
    2. 修飾一非靜態個方法,被修飾的方法稱爲同步方法,其作用的範圍是整個方法,作用的對象是調用這個方法的對象(實例); 
    3. 修飾一個靜態的方法,其作用的範圍是整個靜態方法,作用的對象是這個類的所有對象; 
    4. 修飾一個類,其作用的範圍是synchronized後面括號括起來的部分,作用主的對象是這個類的所有對象(實例)。

    溫馨提示:建議在看下面的例子之前先去看看我的一片博文“多線程之原子性,可見性,有序性,併發問題解決”.本文會引用改文章爲A文在這篇文章中詳細解釋了爲什麼會出現併發問題,併發問題發生的時候是什麼狀況!搞懂這些問題之後再來看看下面的例子,就很簡單了。
    針對以上四種修飾詳解如下:

    定義了一個Person類,在這個類中寫了四個方法,對這四個方法用了synchronized的四種用法。這四個方法做的是同一件事兒,就是讓count做+1操作。每次調用該方法count就+1.我們會啓動10個線程來調用同一個方法,也就是說我們count最後的值應該是10.這是我們的預期結果,但是真正的結果是啥呢?我們拭目以待!

 

/**
* Created by PICO-USER dragon on 2017/2/23.  */  public class Person { private static int count = 0; private String name; public Person(String name) { this.name = name; } public String getName() { return name; } /**  * 普通方法沒有加synchronized修飾,存在併發問題  *  * @param person  */  public void say(Person person) { count++; System.out.print("say person :" + person.getName() + " " + Thread.currentThread().getName() + " count :" + count + "\n"); } public void say1_1(Person person) { //這兒鎖定的是this,代表着Person某一個實例的鎖,只對這個實例互斥。  synchronized (this) { count++; System.out.print("say1 person :" + person.getName() + " " + Thread.currentThread().getName() + " count :" + count + "\n"); } } //非靜態方法屬於實例所有,所以這兒跟say1_1方法一樣,也是Person某一個實例的鎖  public synchronized void say1(Person person) { count++; System.out.print("say1 person :" + person.getName() + " " + Thread.currentThread().getName() + " count :" + count + "\n"); } //靜態方法,歸類所有,是Person這個類的鎖,對所有Person類的實例互斥  public static synchronized void say2(Person person) { count++; System.out.print("say2 person :" + person.getName() + " " + Thread.currentThread().getName() + " count :" + count + "\n"); } public void say2_2(Person person) { //這兒是Person.class。也就是說跟say2一樣。  synchronized (Person.class) { System.out.print("say3 person :" + person.getName() + " " + Thread.currentThread().getName() + " count :" + count + "\n"); } } }

 

 

注意:一定要搞清楚,類和類的實例是不一樣的。如果不能搞清楚這一點,是沒法搞懂synchronized的。還有一點,靜態成員變量和靜態方法歸類所有。這些是很基礎的Java知識了,一定要搞清這些細節。一個類可以有很多很多的實例,比如說人類,地球上幾十億人口,全都是人類的實例。

上面的四個方法,分別是synchronized的四種不同的用法,其中say1和say1_1方法都是Person這個類的實例的鎖,所以它只是針對某一個實例有互斥作用。現在我們來看看調用say1方法的代碼和運行結果。然後再來解釋。

現在我們來看看調用say1方法的代碼和運行結果

 

再定義一個線程類,用於訪問Person類中的方法,這兒我們先訪問沒有併發處理的say方法。看看運行結果

 

 

/**
 * Created by PICO-USER dragon on 2017/2/23.
 */

public class MyThread extends Thread {

    private Person person;

    public MyThread(Person person) {

        this.person = person;
    }

    @Override
    public void run() {
        super.run();
        if (person != null) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //調用沒有併發處理的say方法
            person.say(person);
        }
    }
}

 

 

public class Run {

    public static void main(String[] args0) {

        //定義了一個Person類的實例,這個人的名字叫dragon
        Person person = new Person("dragon");

        //啓動了10個線程,傳入的實例是同一個,相當於dragon同時在不同的線程中調用同一個方法,讓Person類中的count做+1操作
        for (int i = 0; i < 10; i++) {

            new MyThread(person).start();
        }
    }
}

結果:

 

可以看出來,併發問題出現的很嚴重。並沒有得到我們預期的結果!至於爲什麼會出現這個情況,在A文中已經寫得很詳細了。那就需要去A文中找答案了!這兒就不細說了。

 

現在我們Main類不變,改改線程類MyThread中調用的方法爲say1

 

@Override
public void run() {
    super.run();
    if (person != null) {

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //改爲調用併發處理的say1
        person.say1(person);
    }
}

結果:

 

現在可以看到我們得到了預期的結果,這是因爲我們加了synchronized修飾say1方法,並且是對Person類的實例dragon上鎖。dragon在不同的線程中來訪問該方法的時候,必須保證同一時間只有同一個線程在訪問該代碼。其他的線程就在外面等着,直到正在執行該方法中的代碼的線程執行完了,釋放掉該對象的鎖.正在等待的線程才能去爭取dragon的鎖,拿到鎖的線程就能進來訪問代碼。

 

現在我們還是調用say1方法,但是我們改改Main類中的代碼,改爲不同的對象在不同的線程中去訪問該方法,再看看是什麼請款。

結果表明併發的問題發生了?爲什麼加了synchronized還是會放生呢?很簡單,前面我們着重講到,say1是對Person類的實例上鎖,也就是說對同一個dragon在不同的線程中起到互斥的作用。但是,現在是10個不同的dragon。比如說現在dragon0正在訪問say1,還沒用完呢。這時候dragon1來了,因爲這個方法不是是歸對象所有,也就是說dragon0有一個say1方法。dragon1也有一個say1方法。這個鎖對他們兩個來說,不是互斥的,並沒有什麼作用。所以synchronized並沒有起到作用。併發的問題沒有解決。

say1_1根say是一樣的。網友可以根據say1的測試方法測試進行驗證。這兒就不再多說了。下面隨便說一下say2和say2_2,對Person類上鎖的情況。

 

更改線程類MyThread中調用方法的代碼。

改爲調用併發處理並且是對Person類上鎖的say2

 

 

@Override
public void run() {
    super.run();
    if (person != null) {

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //改爲調用併發處理並且是對Person類上鎖的say2
        person.say2(person);
    }
}

 

 

 

再改一下Main類中啓動線程的代碼:

同一個對象,在不同的10個線程中去訪問say2方法。

 

public class Run {

    public static void main(String[] args0) {


        //定義了一個Person類的實例,這個人的名字叫dragon
        Person person = new Person("dragon");
        //啓動了10個線程,傳入的實例是同一個,相當於dragon同時在不同的線程中調用同一個方法,讓Person類中的count做+1操作
        for (int i = 0; i < 10; i++) {

            //   Person person = new Person("dragon" + i);

            new MyThread(person).start();
        }
    }
}

運行結果:

 

可以看到,是預期結果,因爲我們是對Person類上鎖。不論你是不是同一個對象,只要你是Person類的子類,Person類就能管理。就好比:現在父親說了,要將100年的功力傳給他的一堆兒子,每一次傳功力都必須是父親親手親爲,並且同一時間只能爲一個人傳功,否則大家都得死。現在上面的例子就是老大會分身術,變成兩個dragon跑去找他老子,他老子根本不會管是不是同一個人,他只知道,他同一時間只能給一個人傳功。不論是不是同一個人過來要功力,都會讓他們排隊。

   接下來我們再看看多個兒子同時跑去找他老子的例子;其實不用改都知道肯定是解決併發的。

   其他代碼不動,只需要更改Main類中的代碼,改成10個兒子就行:

 

public class Run {

    public static void main(String[] args0) {


        /*//定義了一個Person類的實例,這個人的名字叫dragon
        Person person = new Person("dragon");*/
        //啓動了10個線程,傳入的實例是同一個,相當於dragon同時在不同的線程中調用同一個方法,讓Person類中的count做+1操作
        for (int i = 0; i < 10; i++) {

            Person dragon = new Person("dragon" + i);

            new MyThread(dragon).start();
        }
    }
}

結果:

 

ok,這也是預料之中的結果,我這兒就不多說了,say2_2跟say2是同樣的效果。這兒也不講了。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章