Java並發——線程安全、線程同步、線程通信


線程安全

進程間"共享"對象

多個“寫”線程同時訪問對象。

例:Timer實例的num成員,即add()方法是用的次數。即Timer實例是資源對象。

class TestSync implements Runnable {

    Timer timer = new Timer();

    public void run() {
        timer.add(Thread.currentThread().getName());
    }

}

class Timer {
    private static int num = 0;

    public void add(String name) {
        num++;
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {}
        System.out.println(name + ", 你是第" + num + "個使用timer的線程");
    }

}

public class TestMain {

    public static void main(String[] args) {
        TestSync test = new TestSync();

        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        t2.start();

    }

}

 

 

說明:

(1) 程序輸出顯示:

  t1你是第2個使用timer的線程。

  t2你是第2個使用timer的線程。

(2) 程序執行過程分析:

  線程t1,線程t2 均調用timer的add()方法。

a. 某一線程執行num++(num為1),該線程暫停了1ms。

b. 另一線程執行num++(num為2),該線程暫停了1ms。

c. 某一線程暫停結束,輸出"是第2個使用timer的線程"。

d. 另一線程暫停結束,輸出"是第2個使用timer的線程"。

(3) 導致以上輸出結果的原因:

  兩個線程對同一個Timer實例的num成員做操作。沒有線程之間協調運作。

 

 

線程安全

當多個線程訪問某個類時,該類始終都表現正確行為,則稱該類是線程安全的。 線程安全類中封裝了必要的同步機制,因此客戶端無需進一步采取同步措施。

無狀態對象是線程安全的。

寫Java程序的時候,何時需要進行並發控制,關鍵在於判斷這段程序或這個類是否是線程安全的。

由於多線程的執行條件而出現不正確的結果,被稱為Race Condition(競態條件)

 

 

 

線程同步

多個線程操作一個資源的情況下,導致資源數據前后不一致。這樣就需要協調線程的調度,即線程同步。 解決多個線程使用共通資源的方法是:線程操作資源時獨占資源,其他線程不能訪問資源。使用鎖可以保證在某一代碼段上只有一條線程訪問共用資源。

 

內置鎖

Java提供synchronized關鍵字來支持內在鎖。

 

 

synchronized關鍵字的使用

synchronized關鍵字可以放在方法的前面、對象的前面、類的前面。synchronized關鍵字用作鎖定當前對象。這種鎖又稱為"互斥鎖"。

使用方法:

1. 同步代碼塊

synchronized(obj){
    //.....同步代碼塊
} 

說明:執行同步代碼塊。JVM會鎖定obj對象。即鎖定對象obj只允許單個線程操作。

任何時刻,只能有一條線程可以獲得對同步監視器的鎖定,當同步代碼塊執行結束后,該線程自然釋放了對該同步監視器的鎖定。 同步監視器是為了阻止兩個線程對同一個共享資源進行並發訪問。

Java程序可以使用任何對象作為同步監視器。一般推薦使用可能被並發訪問的共享資源充當同步監視器。

 

2. 同步方法

public synchronized void xxx(){
    //.....
}

 

說明:執行同步方法,該方法只允許單個線程執行。同步方法的同步監視器是this,即對象本身,無須顯示指定同步監視器。

synchronized關鍵字修飾的代碼塊之運行單個線程占用。只有在線程執行完同步代碼塊后,其他線程才能占用該代碼塊。同步方法被某線程執行后排斥其他線程。同步方法會使得程序效率性能降低。

 

3. 同步類

把synchronized關鍵字放在類的前面,這個類中的所有方法都是同步方法。

 

4. 可重入同步

線程可以獲得他已經擁有的鎖,運行線程多次獲得同一個鎖,就是可以重入(reentrant)同步。這種情況通常是同步代碼直接或者間接的調用也包含了同步代碼的方法,並且兩個代碼集都使用同一個鎖。如果沒有可重入同步,那么,同步代碼就必須采取很多額外的預防措施避免線程阻塞自己。java java.util.concurrent 包中的 ReentrantLock 即為可重入鎖。

 

參考:

http://bbs.csdn.net/topics/80052746 該帖子被許多博文轉載與引用

http://blog.163.com/hsh8523@126/blog/static/218935592011214114257822/

 

例:改進程序。

class TestSync implements Runnable {

    Timer timer = new Timer();

    public void run() {
        timer.add(Thread.currentThread().getName());
    }

}

class Timer {
    private static int num = 0;

    public void add(String name) {
        synchronized (this) {
            num++;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {}
            System.out.println(name + ", 你是第" + num + "個使用timer的線程");
        }
    }

}
/*
或者以下下形式
class Timer {
    private static int num = 0;

    public synchronized void add(String name) {
        num++;
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {}
        System.out.println(name + ", 你是第" + num + "個使用timer的線程");
    }
}
*/


public class TestMain {
    public static void main(String[] args) {
        TestSync test = new TestSync();

        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        t2.start();
    }

}

 

 

 

 

1.3.3 理解synchronized關鍵字

由於Java沒有類似於PV操作、進程互斥等相關的方法的。需要說明的是,Java的synchronized()方法類似於操作系統概念中的互斥內存塊,在Java中的Object類型中,都是帶有一個內存鎖的,在有線程獲取該內存鎖后,其它線程無法訪問該內存,從而實現Java中簡單的同步、互斥操作。明白這個原理,就能理解為什么synchronized(this)與synchronized(static XXX)的區別了,synchronized就是針對內存區塊申請內存鎖,this關鍵字代表類的一個對象,所以其內存鎖是針對相同對象的互斥操作,而static成員屬於類專有,其內存空間為該類所有成員共有,這就導致synchronized()對static成員加鎖,相當於對類加鎖,也就是在該類的所有成員間實現互斥,在同一時間只有一個線程可訪問該類的實例。

 

思考以下程序:當執行某個線程執行m1()方法,m2()方法還能執行嗎?

public class TestSynchronized {
    int b = 100;

    public synchronized void m1() throws Exception {
        b = 1000;
        Thread.sleep(5000);
        System.out.println("b = " + b);
    }

    public void m2() throws Exception {
        System.out.println(b);
    }
}

答案:m2() 方法可以執行。即synchronized方法的作用只是用於單個線程執行,並沒有真正鎖定方法對應的this對象。其他線程可以訪問沒有synchronized關鍵字的方法。

從上例中可以得到結論,類中的非同步方法可能會影響到類中的同步方法。所以若需要保證類對象的線程同步,則需要仔細考慮類方法是否需要添加synchronized關鍵字修飾。

關於該問題,自己又做了一個試驗,仔細研究了下。 請看:http://shijiaqi1066.iteye.com/admin/blogs/1886791

 

 

1.4 死鎖

當1號線程執行過程中需要使用(鎖定)對象A,同時還需要使用(鎖定)另一個對象B。但是2號線程使用(鎖定)了對象B,同時還需要使用(鎖定)對象A。

這種情況導致 1號線程等待2號線程執行完才能繼續執行;2號線程等待1號線程的執行完才能繼續執行。即1號線程、2號線程均無法繼續執行;其他線程無法所得資源,也無法繼續執行。整個程序無法繼續執行。即死鎖。

說明:出現死鎖現象需要多個鎖定對象。

 

例:模擬死鎖現象。以下程序會出現死鎖。

public class TestDeadLock implements Runnable {
    public int flag = 1;

    static Object o1 = new Object(), o2 = new Object();

    public void run() {
        System.out.println("flag=" + flag);
        if (flag == 1) {
            synchronized (o1) { // 線程鎖住o1。
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                synchronized (o2) { // 線程鎖住o2。
                    System.out.println("1");
                }
            }
        }

        if (flag == 0) {
            synchronized (o2) { // 線程鎖住o2。
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                synchronized (o1) { // 線程鎖住o1。
                    System.out.println("0");

                }
            }
        }
    }


    public static void main(String[] args) {

        TestDeadLock td1 = new TestDeadLock();
        TestDeadLock td2 = new TestDeadLock();

        td1.flag = 1;
        td2.flag = 0;

        Thread t1 = new Thread(td1); // flag1線程。
        Thread t2 = new Thread(td2); // flag2線程。

        t1.start();
        t2.start();
    }

}

 

說明:

a. flag1線程鎖住o1,睡眠;同時,flag2線程鎖住o2,睡眠。

b. flag1線程蘇醒需要鎖住o2,才能執行完畢,但是o2被flag2線程占用;同時,flag2線程蘇醒需要鎖住o1,才能執行完畢,但是o1被flag1線程占用。

c. 出現flag1線程等待flag2線程,flag2線程等待flag1線程。程序無法再繼續執行。

 

哲學家吃飯問題:5個哲學家圍繞圓桌吃飯。每個哲學家各左右手各拿一只筷子(即每個一雙筷子)。

WIKI :http://zh.wikipedia.org/wiki/%E5%93%B2%E5%AD%A6%E5%AE%B6%E5%B0%B1%E9%A4%90%E9%97%AE%E9%A2%98

百度百科 :http://baike.baidu.com/view/3446884.htm

 

解決死鎖問題

如果不需要寫接近底層的程序或很復雜的程序,死鎖問題在實際編程中比較難遇上。

如果出現死鎖問題:可以放大鎖定對象的粒度。即不要一次鎖定多個對象。盡量鎖定一個對象。

 

 

1.5 監視器與對象鎖

Java並發,會導致線程間共享的對象存在線程安全的問題。線程的同步。JVM提供了相關機制。鎖機制。

在JVM中,每個對象和類在邏輯上都是和一個監視器相關聯的,為了實現監視器的排他性監視能力,JVM為每一個對象和類都關聯一個鎖,鎖住了一個對象,就是獲得對象相關聯的監視器。

監視器好比一座建築,它有一個很特別的房間,房間里有一些數據,而且在同一時間只能被一個線程占據,進入這個建築叫做"進入監視器",進入建築中的那個特別的房間叫做"獲得監視器",占據房間叫做"持有監視器",離開房間叫做"釋放監視器",離開建築叫做"退出監視器"。

而一個鎖就像一種任何時候只允許一個線程擁有的特權。一個線程可以允許多次對同一對象上鎖。對於每一個對象來說,JVM維護一個計數器,記錄對象被加了多少次鎖,沒被鎖的對象的計數器是0,線程每加鎖一次,計數器就加1,每釋放一次,計數器就減1。當計數器跳到0的時候,鎖就被完全釋放了。

JVM中的一個線程在它到達監視區域開始處的時候請求一個鎖。Java程序中每一個監視區域都和一個對象引用相關聯。

   

 

 

1.6 原子操作和復雜操作

原子性:一個操作不會被其他線程打斷,能保證其從開始到結束獨享資源連續執行完這一操作。

很多看上去像是原子性的操作正式並發問題高災區。比如所熟知的計數器(count++)和check-then-act(先檢查后操作),這些都是很容易被忽視的。

例如大家所常用的惰性初始化(延遲初始化)模式,以下代碼就不是線程安全的:

public class LazyInitRace {
    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if (instance == null)
            instance = new ExpensiveObject();
        return instance;
    }
}

 

這段代碼具體問題在於沒有認識到if(instance==null)和instance = new ExpensiveObject();是兩條語句,放在一起就不是原子性的,就有可能當一個線程執行完if(instance==null)后會被中斷,另一個線程也去執行if(instance==null),這次兩個線程都會執行后面的instance = new ExpensiveObject();這也是這個程序所不希望發生的。

check-then-act從表面上看很簡單,普遍存在與我們日常的開發中,特別是在數據庫存取這一塊。比如我們需要在數據庫里存一個客戶的統計值,當統計值不存在時初始化,當存在時就去更新。如果不把這組邏輯設計為原子性的就很有可能產生出兩條這個客戶的統計值。

在單機環境下處理這個問題還算容易,通過鎖或者同步來把這組復合操作變為原子操作,但在分布式環境下就不適用了。一般情況下是通過在數據庫端做文章,比如通過唯一性索引或者悲觀鎖來保障其數據一致性。當然任何方案都是有代價的,這就需要具體情況下來權衡。

 

1.7 構建線程安全類

當多個線程訪問一個類時,如果不用考慮這些線程在運行時環境下的調度和交替執行,並且不需要額外的同步,這個類的行為仍然是正確的,那么稱這個類是線程安全的。我們設計類就是要在有潛在並發問題存在情況下,設計線程安全的類。線程安全的類可以通過以下手段來滿足:

  • 不跨線程共享變量。
  • 使狀態變量為不可變的。
  • 在任何訪問狀態變量的時候使用同步。
  • 每個共享的可變變量都需要由唯一一個確定的鎖保護。

 

滿足線程安全的一些思路

1. 從源頭避免並發問題

很多開發者一想到有並發的可能就通過底層技術來解決問題,其實往往可以通過上層的架構設計和業務分析來避免並發場景。比如我們需要用多線程或分布式集群來計算一堆客戶的相關統計值,由於客戶的統計值是共享數據,因此會有並發潛在可能。但從業務上我們可以分析出客戶與客戶之間數據是不共享的,因此可以設計一個規則來保證一個客戶的計算工作和數據訪問只會被一個線程或一台工作機完成,而不是把一個客戶的計算工作分配給多個線程去完成。這種規則很容易設計。當你從源頭就避免了並發問題的可能,下面的工作就完全可以不用擔心線程安全問題。

 

2. 無狀態就是線程安全

多線程編程或者分布式編程最忌諱有狀態,一有狀態就不但限制了其橫向擴展能力,也是產生並發問題的起源。當你設計的類是無狀態的,那么它永遠都是線程安全的。因此在設計階段需要考慮如何用無狀態的類來滿足你的業務需求。

 

3. 鎖的合理使用

大家都知道可以用鎖來解決並發問題,但在具體使用上還有很多講究,比如:

  • 每個共享的可變變量都需要由一個個確定的鎖保護。
  • 一旦使用了鎖,就意味着這段代碼的執行就喪失了操作系統多道程序的特性,會在一定程度上影響性能。
  • 鎖不能解決在分布式環境共享變量的並發問題。

   

4. 線程封閉

當訪問共享可變數據時,通常需要使用同步,同步是需要消耗性能的。

避免使用同步的方式就是不共享數據。若將數據都封閉在各自的線程之中,就不需要同步。這種通過將數據封閉在線程中而避免使用同步的技術稱為線程封閉。

一般存在3種線程封閉的方法:

  • Ad-hoc線程封閉
  • 棧封閉
  • ThreadLocal

其中ThreadLocal是Java提供線程封閉的規范。

 

1.8 ThreadLocal類

ThreadLocal用於實現線程內的數據共享,即對於相同的程序代碼,多個模塊在同一個線程中運行時要共享一份數據,而在另外線程中運行時又共享另外一份數據。

每個線程調用全局ThreadLocal對象的set方法,就相當於往其內部的map中增加一條記錄,key分別是各自的線程,value是各自的set方法傳進去的值。在線程結束時可以調用ThreadLocal.clear()方法,這樣會更快釋放內存,不調用也可以,因為線程結束后也可以自動釋放相關的ThreadLocal變量。

 

ThreadLocal的應用場景:

例:Strut2的ActionContext,同一段代碼被不同的線程調用運行時,該代碼操作的數據是每個線程各自的狀態和數據,對於不同的線程來說,getContext方法拿到的對象都不相同,對同一個線程來說,不管調用getContext方法多少次和在哪個模塊中getContext方法,拿到的都是同一個。

ThreadLocal提供了get與set訪問器,為每個使用它的線程維護一份單獨的拷貝。所以get總是返回由當前執行線程通過set設置的最新值。

 

例:使用ThreadLocal確保線程封閉性。

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    public Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    }
};

public static Connection getConnection() {
    return connectionHolder.get();
}

線程首次調用ThreadLocal.get方法時,會請求initialValue 提供一個初始值。概念上,可以將ThreadLocal<T>看作map< Thread,T>,線程終止后,這些值會被垃圾回收。

若需要將一個單線程的應用遷移到多線程環境中,可以將共享的全局變量都轉換為ThreadLocal類型,這樣可以確保線程安全。前提是全局共享(shared globals)的語義允許這樣。如果將應用級的緩存變成一堆線程本地緩沖,它將毫無價值。

一般使用,需要實現對ThreadLocal變量的封裝,讓外界不要直接操作ThreadLocal變量。線程本地變量會降低重用性,引入隱晦的類間的耦合。因此應該謹慎地使用。

 

 

線程通信

2.1 對象的wait,notify/notifyAll

Object類對線程的控制

  • void wait()
  • void wait(long timeout)
  • void wait(long timeout, int nanos)

其他線程調用此對象的 notify() 方法或 notifyAll() 方法前,導致當前線程等待。

  • void notify() 喚醒在此對象監視器上等待的單個線程。
  • void notifyAll() 喚醒在此對象監視器上等待的所有線程。

 

在http://blog.csdn.net/zyplus/article/details/6672775看到的一些介紹 (語句略有改動)

如果需要在線程間相互喚醒的話就需要借助Object.wait(), Object.nofity() 。

Obj.wait(),與Obj.notify()必須要與synchronized(Obj)一起使用,也就是wait,與notify是針對已經獲取了Obj鎖進行操作,從語法角度來說就是Obj.wait(),Obj.notify必須在synchronized(Obj){...}語句塊內。

從功能上來說:

obj.wait()方法使得獲取對象鎖的線程主動釋放對象鎖,同時休眠線程;直到有其它線程調用對象的notify()喚醒該線程,才能繼續獲取對象鎖,並繼續執行。Thread.sleep()與Object.wait()二者都可以暫停當前線程,釋放CPU控制權,主要的區別在於Object.wait()在釋放CPU同時,釋放了對象鎖的控制。

obj.notify()方法喚醒因釋放obj的鎖而休眠的線程(喚醒obj對象上被wait()的線程)。注意:notify()調用后,並不是馬上就釋放對象鎖的,而是在相應的synchronized(){}語句塊執行結束,自動釋放鎖后,JVM會在wait()對象鎖的線程中隨機選取一線程,賦予其對象鎖,喚醒線程,繼續執行。這樣就提供了在線程間同步、喚醒的操作。

 

2.1 生產者與消費者問題

例:模擬生產者消費者問題的Java實現

public class ProducerConsumer {

    public static void main(String[] args) {
        SyncStack ss = new SyncStack();

        Producer p = new Producer(ss);
        Consumer c = new Consumer(ss);

        new Thread(p).start();
        new Thread(p).start();
        new Thread(p).start();
        new Thread(c).start();
    }
}


class WoTou {
    int id;

    WoTou(int id) {
        this.id = id;
    }

    public String toString() {
        return "WoTou : " + id;
    }
}


class SyncStack {
    int index = 0;
    WoTou[] arrWT = new WoTou[6];

    public synchronized void push(WoTou wt) {
        while (index == arrWT.length) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.notifyAll();
        arrWT[index] = wt;
        index++;
    }

    public synchronized WoTou pop() {
        while (index == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.notifyAll();
        index--;
        return arrWT[index];
    }
}


class Producer implements Runnable {
    SyncStack ss = null;

    Producer(SyncStack ss) {
        this.ss = ss;
    }

    public void run() {
        for (int i = 0; i < 20; i++) {
            WoTou wt = new WoTou(i);
            ss.push(wt);
            System.out.println("生產了:" + wt);

            try {
                Thread.sleep((int) (Math.random() * 200));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


class Consumer implements Runnable {
    SyncStack ss = null;

    Consumer(SyncStack ss) {
        this.ss = ss;
    }

    public void run() {
        for (int i = 0; i < 20; i++) {
            WoTou wt = ss.pop();
            System.out.println("消費了: " + wt);
            try {
                Thread.sleep((int) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

 

 

理解wait與notify

 

 

object.wait() 方法必須位於synchronized(object) 代碼塊中。
object.wait() 方法之后,線程會釋放object的監視器。線程被添加到object的wait隊列中並處於等待狀態。

object.notify()方法之前,必須調用synchronized(object) 。notify方法不會讓當前線程會釋放object的監視器。
object.notify()方法會通知其他wait於object實例上的一個線程,讓其重新去爭用object的監視器。之前wait的線程若沒有獲取到object的監視器其synchronized(object) 代碼塊不會繼續執行。

 

 

 

 

 

notify方法用於喚醒對象上的一個正在等待的線程。
notifyAll方法用於喚醒對象上的所有正在等待的線程。

 

 

 

 

 


免責聲明!

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



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