Java並發編程之線程之間的共享和協作

一、線程間的共享

1.1 ynchronized內置鎖

用處

  • Java支持多個線程同時訪問一個對象或者對象的成員變量
  • 關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用
  • 它主要確保多個線程在同一個時刻,隻能有一個線程處於方法或者同步塊中
  • 它保證瞭線程對變量訪問的可見性和排他性(原子性、可見性、有序性),又稱為內置鎖機制。

對象鎖和類鎖

  • 對象鎖是用於對象實例方法,或者一個對象實例上的
  • 類鎖是用於類的靜態方法或者一個類的class對象上的
  • 類的對象實例可以有很多個,但是每個類隻有一個class對象,所以不同對象實例的對象鎖是互不幹擾的,但是每個類隻有一個類鎖
  • 註意的是,其實類鎖隻是一個概念上的東西,並不是真實存在的,類鎖其實鎖的是每個類的對應的class對象
  • 類鎖和對象鎖之間也是互不幹擾的。

1.2 volatile關鍵字

  • 最輕量的同步機制,保證可見性,不保證原子性
  • volatile保證瞭不同線程對這個變量進行操作時的可見性,即一個線程修改瞭某個變量的值,這新值對其他線程來說是立即可見的。
  • volatile最適用的場景:隻有一線程寫,多個線程讀的場景

1.3 ThreadLocal

  • ThreadLocal 和 Synchonized 都用於解決多線程並發訪問。
  • 可是ThreadLocal與synchronized有本質的差別:
  • synchronized是利用鎖的機制,使變量或代碼塊在某一時該僅僅能被一個線程訪問。
  • 而ThreadLocal為每個線程都提供瞭變量的副本,使得每個線程在某一時間訪問到的並非同一個對象,這樣就隔離瞭多個線程對數據的數據共享。
  • Spring的事務就借助瞭ThreadLocal類。

1.4 Spring的事務借助ThreadLocal類

Spring會從數據庫連接池中獲得一個connection,然會把connection放進ThreadLocal中,也就和線程綁定瞭,事務需要提交或者回滾,隻要從ThreadLocal中拿到connection進行操作。

1.4.1 為何Spring的事務要借助ThreadLocal類?

以JDBC為例,正常的事務代碼可能如下:
dbc = new DataBaseConnection();//第1行
Connection con = dbc.getConnection();//第2行
con.setAutoCommit(false);// //第3行
con.executeUpdate(...);//第4行
con.executeUpdate(...);//第5行
con.executeUpdate(...);//第6行
con.commit();第7行
上述代碼,可以分成三個部分:
事務準備階段:第1~3行
業務處理階段:第4~6行
事務提交階段:第7行 
  • 不管我們開啟事務還是執行具體的sql都需要一個具體的數據庫連接。
  • 開發應用一般都采用三層結構,我們的Service會調用一系列的DAO對數據庫進行多次操作,那麼,這個時候我們就無法控制事務的邊界瞭,因為實際應用當中,我們的Service調用的DAO的個數是不確定的,可根據需求而變化,而且還可能出現Service調用Service的情況。
  • 如果不使用ThreadLocal,如何讓三個DAO使用同一個數據源連接呢?我們就必須為每個DAO傳遞同一個數據庫連接,要麼就是在DAO實例化的時候作為構造方法的參數傳遞,要麼在每個DAO的實例方法中作為方法的參數傳遞。
Connection conn = getConnection();
Dao1 dao1 = new Dao1(conn);
dao1.exec();
Dao2 dao2 = new Dao2(conn);
dao2.exec();
Dao3 dao3 = new Dao3(conn);
dao3.exec();
conn.commit();
  • 為瞭讓這個數據庫連接可以跨階段傳遞,又不顯式的進行參數傳遞,就必須使用別的辦法。
  • Web容器中,每個完整的請求周期會由一個線程來處理。因此,如果我們能將一些參數綁定到線程的話,就可以實現在軟件架構中跨層次的參數共享(是隱式的共享)。而JAVA中恰好提供瞭綁定的方法–使用ThreadLocal。
  • 結合使用Spring裡的IOC和AOP,就可以很好的解決這一點。
  • 隻要將一個數據庫連接放入ThreadLocal中,當前線程執行時隻要有使用數據庫連接的地方就從ThreadLocal獲得就行瞭。

1.4.2 ThreadLocal的使用

void set(Object value)

  • 設置當前線程的線程局部變量的值。

public Object get()

  • 該方法返回當前線程所對應的線程局部變量。

public void remove()

  • 將當前線程局部變量的值刪除,目的是為瞭減少內存的占用,該方法是JDK 5.0新增的方法。
  • 需要指出的是,當線程結束後,對應該線程的局部變量將自動被垃圾回收
  • 所以顯式調用該方法清除線程的局部變量並不是必須的操作,但它可以加快內存回收的速度。

protected Object initialValue()

  • 返回該線程局部變量的初始值
  • 該方法是一個protected的方法,顯然是為瞭讓子類覆蓋而設計的。
  • 這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,並且僅執行1次。
  • ThreadLocal中的缺省實現直接返回一個null。

public final static ThreadLocal RESOURCE = new ThreadLocal()

  • RESOURCE代表一個能夠存放String類型的ThreadLocal對象。
  • 此時不論任何一個線程能夠並發訪問這個變量,對它進行寫入、讀取操作,都是線程安全的。

1.4.3 ThreadLocal實現解析

2.ThreadLocal原理

public class ThreadLocal<T> {
    //get方法,其實就是拿到每個線程獨有的ThreadLocalMap
    //然後再用ThreadLocal的當前實例,拿到Map中的相應的Entry,然後就可以拿到相應的值返回出去。
    //如果Map為空,還會先進行map的創建,初始化等工作。
    public T get() {
        //先取到當前線程,然後調用getMap方法獲取對應線程的ThreadLocalMap
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    // Thread類中有一個 ThreadLocalMap 類型成員,所以getMap是直接返回Thread的成員
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    // ThreadLocalMap是ThreadLocal的靜態內部類
    static class ThreadLocalMap {
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 用數組保存 Entry , 因為可能有多個變量需要線程隔離訪問,即聲明多個 ThreadLocal 變量
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // Entry 類似於 map 的 key-value 結構
            // key 就是 ThreadLocal, value 就是需要隔離訪問的變量
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
        ...
    }
    
    //Entry內部靜態類,它繼承瞭WeakReference,
    //總之它記錄瞭兩個信息,一個是ThreadLocal<?>類型,一個是Object類型的值
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
    
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    //getEntry方法則是獲取某個ThreadLocal對應的值
    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }
    
    //set方法就是更新或賦值相應的ThreadLocal對應的值
    private void set(ThreadLocal<?> key, Object value) {
        ...
    }
    ...
}

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    ...
}

1.5引用基礎知識

1.5.1 引用

  • 創建對象 Object o = new Object();
  • 這個o,我們可以稱之為對象引用,而new Object()我們可以稱之為在內存中產生瞭一個對象實例。
  • 當 o=null 時,隻是表示o不再指向堆中Object的對象實例,不代表這個對象實例不存在瞭。

1.5.2 強引用

  • 指在程序代碼之中普遍存在的,類似“Object obj=new Object()
  • 這類的引用,隻要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象實例。

1.5.3 軟引用

  •  用來描述一些還有用但並非必需的對象。
  • 對於軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象實例列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。
  • 在JDK 1.2之後,提供瞭SoftReference類來實現軟引用。

1.5.4 弱引用

  • 用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象實例隻能生存到下一次垃圾收集發生之前。
  • 當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉隻被弱引用關聯的對象實例。
  • 在JDK 1.2之後,提供瞭WeakReference類來實現弱引用。

1.5.5 虛引用

  •  也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。
  • 一個對象實例是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。
  • 為一個對象設置虛引用關聯的唯一目的就是能在這個對象實例被收集器回收時收到一個系統通知。
  • 在JDK 1.2之後,提供瞭PhantomReference類來實現虛引用。

1.6 使用 ThreadLocal 引發內存泄漏

1.6.1 準備

將堆內存大小設置為-Xmx256m

啟用一個線程池,大小固定為5個線程

//5M大小的數組
private static class LocalVariable {
    private byte[] value = new byte[1024*1024*5];
}

// 創建線程池,固定為5個線程
private static ThreadPoolExecutor poolExecutor
        = new ThreadPoolExecutor(5,5,1, TimeUnit.MINUTES,new LinkedBlockingQueue<>());
        
//ThreadLocal共享變量
private ThreadLocal<LocalVariable> data;

@Override
public void run() {
    //場景1:不執行任何有意義的代碼,當所有的任務提交執行完成後,查看內存占用情況,占用 25M 左右
    //System.out.println("hello ThreadLocal...");

    //場景2:創建 數據對象,執行完成後,查看內存占用情況,與場景1相同
    //new LocalVariable();

    //場景3:啟用 ThreadLocal,執行完成後,查看內存占用情況,占用 100M 左右
    ThreadLocalOOM obj = new ThreadLocalOOM();
    obj.data = new ThreadLocal<>();
    obj.data.set(new LocalVariable());
    System.out.println("update ThreadLocal data value..........");

    //場景4: 加入 remove(),執行完成後,查看內存占用情況,與場景1相同
    //obj.data.remove();

    //分析:在場景3中,當啟用瞭ThreadLocal以後確實發生瞭內存泄漏
}

場景1:

  • 首先任務中不執行任何有意義的代碼,當所有的任務提交執行完成後,可以看見,我們這個應用的內存占用基本上為25M左右

場景2:

  • 然後我們隻簡單的在每個任務中new出一個數組,執行完成後我們可以看見,內存占用基本和場景1相同

場景3:

  • 當我們啟用瞭ThreadLocal以後,執行完成後我們可以看見,內存占用變為瞭100多M

場景4:

  • 我們加入一行代碼 obj.data.remove(); ,再執行,看看內存情況,可以看見,內存占用基本和場景1相同。

場景分析:

  • 這就充分說明,場景3,當我們啟用瞭ThreadLocal以後確實發生瞭內存泄漏。

1.6.2 內存泄漏分析

  •  通過對ThreadLocal的分析,我們可以知道每個Thread 維護一個 ThreadLocalMap,這個映射表的 key 是 ThreadLocal實例本身,value 是真正需要存儲的 Object,也就是說 ThreadLocal 本身並不存儲值,它隻是作為一個 key 來讓線程從 ThreadLocalMap 獲取 value。
  • 仔細觀察ThreadLocalMap,這個map是使用 ThreadLocal 的弱引用作為 Key 的,弱引用的對象在 GC 時會被回收。

2.ThreadLocal原理

  • 圖中的虛線表示弱引用。
  • 當把threadlocal變量置為null以後,沒有任何強引用指向threadlocal實例,所以threadlocal將會被gc回收
  • 這樣一來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法訪問這些key為null的Entry的value
  • 如果當前線程再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而這塊value永遠不會被訪問到瞭,所以存在著內存泄露。
  • 可以通過Debug模式,查看變量 poolExecutor->workers->0->thread->threadLocals,會發現線程的成員變量 threadLocals 的 size=1,map 中存放瞭一個 referent=null, value=data對象
  • 隻有當前thread結束以後,current thread就不會存在棧中,強引用斷開,Current Thread、Map value將全部被GC回收。
  • 最好的做法是在不需要使用ThreadLocal變量後,都調用它的remove()方法,清除數據。

場景3分析:

  • 在場景3中,雖然線程池裡面的任務執行完畢瞭,但是線程池裡面的5個線程會一直存在直到JVM退出,我們set瞭線程的localVariable變量後沒有調用localVariable.remove()方法,導致線程池裡面的5個線程的threadLocals變量裡面的new LocalVariable()實例沒有被釋放。

從表面上看內存泄漏的根源在於使用瞭弱引用。為什麼使用弱引用而不是強引用?下面我們分兩種情況討論:

  •  key 使用強引用:對ThreadLocal對象實例的引用被置為null瞭,但是ThreadLocalMap還持有這個ThreadLocal對象實例的強引用,如果沒有手動刪除,ThreadLocal的對象實例不會被回收,導致Entry內存泄漏。
  • key 使用弱引用:對ThreadLocal對象實例的引用被被置為null瞭,由於ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal的對象實例也會被回收。value在下一次ThreadLocalMap調用set,get,remove都有機會被回收。
  • 比較兩種情況,我們可以發現:由於ThreadLocalMap的生命周期跟Thread一樣長,如果都沒有手動刪除對應key,都會導致內存泄漏,但是使用弱引用可以多一層保障。

因此,ThreadLocal內存泄漏的根源是:

  • 由於ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因為弱引用。

總結:

  • JVM利用設置ThreadLocalMap的Key為弱引用,來避免內存泄露。
  • JVM利用調用remove、get、set方法的時候,回收弱引用。
  • 當ThreadLocal存儲很多Key為null的Entry的時候,而不再去調用remove、get、set方法,那麼將導致內存泄漏。
  • 使用線程池 + ThreadLocal時要小心,因為這種情況下,線程是一直在不斷的重復運行的,從而也就造成瞭value可能造成累積的情況。

錯誤使用ThreadLocal導致線程不安全:

  • 仔細考察ThreadLocal和Thead的代碼,我們發現ThreadLocalMap中保存的其實是對象的一個引用,這樣的話,當有其他線程對這個引用指向的對象實例做修改時,其實也同時影響瞭所有的線程持有的對象引用所指向的同一個對象實例。
  • 這也就是為什麼上面的程序為什麼會輸出一樣的結果:5個線程中保存的是同一個Number對象的引用,因此它們最終輸出的結果是相同的。
  • 正確的用法是讓每個線程中的ThreadLocal都應該持有一個新的Number對象。 線程間的協作

二、線程間的協作

  • 線程之間相互配合,完成某項工作;
  • 比如一個線程修改瞭一個對象的值,而另一個線程感知到瞭變化,然後進行相應的操作;
  • 前者是生產者,後者就是消費者,這種模式隔離瞭“做什麼”(what)和“怎麼做”(How);
  • 常見的方法是讓消費者線程不斷地循環檢查變量是否符合預期在while循環中設置不滿足的條件,如果條件滿足則退出while循環,從而完成消費者的工作。

存在如下問題:

  • 1)難以確保及時性;
  • 2)難以降低開銷。如果降低睡眠的時間,比如休眠1毫秒,這樣消費者能更加迅速地發現條件變化,但是卻可能消耗更多的處理器資源,造成瞭無端的浪費。

2.1等待和通知機制

是指一個線程A調用瞭對象O的wait()方法進入等待狀態,而另一個線程B調用瞭對象O的notify()或者notifyAll()方法,線程A收到通知後從對象O的wait()方法返回,進而執行後續操作。

上述兩個線程通過對象O來完成交互,而對象上的wait()和notify/notifyAll()的關系就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。

notify():

通知一個在對象上等待的線程,使其從wait方法返回,而返回的前提是該線程獲取到瞭對象的鎖,沒有獲得鎖的線程重新進入WAITING狀態。

notifyAll():

通知所有等待在該對象上的線程。

wait():

調用該方法的線程進入 WAITING狀態,隻有等待另外線程的通知或被中斷才會返回.需要註意,調用wait()方法後,會釋放對象的鎖。

wait(long):

超時等待一段時間,這裡的參數時間是毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回;

wait (long,int):

對於超時時間更細粒度的控制,可以達到納秒;

2.2等待和通知的標準范式

等待方遵循如下原則:

  •  1.獲取對象的鎖
  • 2.循環裡判斷條件是否滿足,如果條件不滿足,那麼調用對象的wait()方法,被通知後仍要檢查條件。
  • 條件滿足則執行對應的邏輯。
synchronized(對象){
    while(條件不滿足){
        對象.wait();
    }
    對應的邏輯
}

通知方遵循如下原則:

  •  1.獲取對象的鎖。
  • 2.改變條件。
  • 3.通知所有等待在對象上的線程。
synchronized(對象){
    改變條件
    對象.notifyAll();
}

在調用wait()、notify()系列方法之前,線程必須要獲得該對象的對象級別鎖,即隻能在同步方法或同步塊中調用wait() 方法、notify()系列方法;

  • 進入wait() 方法後,當前線程釋放鎖,在從wait() 返回前,線程與其他線程競爭重新獲得鎖,執行notify()系列方法的線程退出synchronized代碼塊的時候後,他們就會去競爭。
  • 如果其中一個線程獲得瞭該對象鎖,它就會繼續往下執行,在它退出synchronized代碼塊,釋放鎖後,其他的已經被喚醒的線程將會繼續競爭獲取該鎖,一直進行下去,直到所有被喚醒的線程都執行完畢。

notify() 和 notifyAll() 應該用誰?

  • 盡量用 notifyAll()
  • 謹慎使用notify(),因為notify()隻會喚醒一個線程,我們無法確保被喚醒的這個線程一定就是我們需要喚醒的線程;

2.3等待超時模式實現一個連接池

調用場景:

  •  調用一個方法時等待一段時間(一般來說是給定一個時間段),如果該方法能夠在給定的時間段之內得到結果,那麼將結果立刻返回,反之,超時返回默認結果。
  • 假設等待時間段是T,那麼可以推斷出在當前時間now+T之後就會超時
  • 等待持續時間:REMAINING=T ;
  • 超時時間:FUTURE=now+T ;
  • 客戶端獲取連接的過程被設定為等待超時的模式,也就是在1000毫秒內如果無法獲取到可用連接,將會返回給客戶端一個null。
  • 設定連接池的大小為10個,然後通過調節客戶端的線程數來模擬無法獲取連接的場景。
  • 通過構造函數初始化連接的最大上限,通過一個雙向隊列來維護連接,調用方需要先調用fetchConnection(long)方法來指定在多少毫秒內超時獲取連接,當連接使用完成後,需要調用releaseConnection(Connection)方法將連接放回線程池

調用yield() 、sleep()、wait()、notify()等方法對鎖有何影響?

  • yield() 、sleep()被調用後,都不會釋放當前線程所持有的鎖。
  • 調用wait()方法後,會釋放當前線程持有的鎖,而且當前被喚醒後,會重新去競爭鎖,鎖競爭到後才會執行wait方法後面的代碼。
  • 調用notify()系列方法後,對鎖無影響,線程隻有在synchronized同步代碼執行完後才會自然而然的釋放鎖,所以notify()系列方法一般都是synchronized同步代碼的最後一行。

到此這篇關於Java並發編程之線程之間的共享和協作的文章就介紹到這瞭,更多相關java線程之間的共享和協作內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!