App下載

詳解Java并發(fā)編程中怎么實現(xiàn)線程之間的共享和協(xié)作

紓寒 2021-08-09 14:58:50 瀏覽數(shù) (2054)
反饋

一、線程間的共享

1.1 ynchronized內(nèi)置鎖

用處

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

對象鎖和類鎖

  • 對象鎖是用于對象實例方法,或者一個對象實例上的
  • 類鎖是用于類的靜態(tài)方法或者一個類的class對象上的
  • 類的對象實例可以有很多個,但是每個類只有一個class對象,所以不同對象實例的對象鎖是互不干擾的,但是每個類只有一個類鎖
  • 注意的是,其實類鎖只是一個概念上的東西,并不是真實存在的,類鎖其實鎖的是每個類的對應的class對象
  • 類鎖和對象鎖之間也是互不干擾的。

1.2 volatile關(guān)鍵字

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

1.3 ThreadLocal

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

1.4 Spring的事務借助ThreadLocal類

Spring會從數(shù)據(jù)庫連接池中獲得一個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行
業(yè)務處理階段:第4~6行
事務提交階段:第7行 
  • 不管我們開啟事務還是執(zhí)行具體的sql都需要一個具體的數(shù)據(jù)庫連接。
  • 開發(fā)應用一般都采用三層結(jié)構(gòu),我們的Service會調(diào)用一系列的DAO對數(shù)據(jù)庫進行多次操作,那么,這個時候我們就無法控制事務的邊界了,因為實際應用當中,我們的Service調(diào)用的DAO的個數(shù)是不確定的,可根據(jù)需求而變化,而且還可能出現(xiàn)Service調(diào)用Service的情況。
  • 如果不使用ThreadLocal,如何讓三個DAO使用同一個數(shù)據(jù)源連接呢?我們就必須為每個DAO傳遞同一個數(shù)據(jù)庫連接,要么就是在DAO實例化的時候作為構(gòu)造方法的參數(shù)傳遞,要么在每個DAO的實例方法中作為方法的參數(shù)傳遞。
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();
  • 為了讓這個數(shù)據(jù)庫連接可以跨階段傳遞,又不顯式的進行參數(shù)傳遞,就必須使用別的辦法。
  • Web容器中,每個完整的請求周期會由一個線程來處理。因此,如果我們能將一些參數(shù)綁定到線程的話,就可以實現(xiàn)在軟件架構(gòu)中跨層次的參數(shù)共享(是隱式的共享)。而JAVA中恰好提供了綁定的方法–使用ThreadLocal。
  • 結(jié)合使用Spring里的IOC和AOP,就可以很好的解決這一點。
  • 只要將一個數(shù)據(jù)庫連接放入ThreadLocal中,當前線程執(zhí)行時只要有使用數(shù)據(jù)庫連接的地方就從ThreadLocal獲得就行了。

1.4.2 ThreadLocal的使用

void set(Object value)

  • 設(shè)置當前線程的線程局部變量的值。

public Object get()

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

public void remove()

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

protected Object initialValue()

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

public final static ThreadLocal RESOURCE = new ThreadLocal()

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

1.4.3 ThreadLocal實現(xiàn)解析

2.ThreadLocal原理

public class ThreadLocal<T> {
    //get方法,其實就是拿到每個線程獨有的ThreadLocalMap
    //然后再用ThreadLocal的當前實例,拿到Map中的相應的Entry,然后就可以拿到相應的值返回出去。
    //如果Map為空,還會先進行map的創(chuàng)建,初始化等工作。
    public T get() {
        //先取到當前線程,然后調(diào)用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的靜態(tài)內(nèi)部類
    static class ThreadLocalMap {
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 用數(shù)組保存 Entry , 因為可能有多個變量需要線程隔離訪問,即聲明多個 ThreadLocal 變量
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // Entry 類似于 map 的 key-value 結(jié)構(gòu)
            // key 就是 ThreadLocal, value 就是需要隔離訪問的變量
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
        ...
    }
    
    //Entry內(nèi)部靜態(tài)類,它繼承了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引用基礎(chǔ)知識

1.5.1 引用

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

1.5.2 強引用

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

1.5.3 軟引用

  •  用來描述一些還有用但并非必需的對象。
  • 對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象實例列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。
  • 在JDK 1.2之后,提供了SoftReference類來實現(xiàn)軟引用。

1.5.4 弱引用

  • 用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象實例只能生存到下一次垃圾收集發(fā)生之前。
  • 當垃圾收集器工作時,無論當前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象實例。
  • 在JDK 1.2之后,提供了WeakReference類來實現(xiàn)弱引用。

1.5.5 虛引用

  •  也稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系。
  • 一個對象實例是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例。
  • 為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象實例被收集器回收時收到一個系統(tǒng)通知。
  • 在JDK 1.2之后,提供了PhantomReference類來實現(xiàn)虛引用。

1.6 使用 ThreadLocal 引發(fā)內(nèi)存泄漏

1.6.1 準備

將堆內(nèi)存大小設(shè)置為-Xmx256m

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

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

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

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

    //場景2:創(chuàng)建 數(shù)據(jù)對象,執(zhí)行完成后,查看內(nèi)存占用情況,與場景1相同
    //new LocalVariable();

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

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

    //分析:在場景3中,當啟用了ThreadLocal以后確實發(fā)生了內(nèi)存泄漏
}

場景1:

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

場景2:

  • 然后我們只簡單的在每個任務中new出一個數(shù)組,執(zhí)行完成后我們可以看見,內(nèi)存占用基本和場景1相同

場景3:

  • 當我們啟用了ThreadLocal以后,執(zhí)行完成后我們可以看見,內(nèi)存占用變?yōu)榱?00多M

場景4:

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

場景分析:

  • 這就充分說明,場景3,當我們啟用了ThreadLocal以后確實發(fā)生了內(nèi)存泄漏。

1.6.2 內(nèi)存泄漏分析

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

2.ThreadLocal原理

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

場景3分析:

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

從表面上看內(nèi)存泄漏的根源在于使用了弱引用。為什么使用弱引用而不是強引用?下面我們分兩種情況討論:

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

因此,ThreadLocal內(nèi)存泄漏的根源是:

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

總結(jié):

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

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

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

二、線程間的協(xié)作

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

存在如下問題:

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

2.1等待和通知機制

是指一個線程A調(diào)用了對象O的wait()方法進入等待狀態(tài),而另一個線程B調(diào)用了對象O的notify()或者notifyAll()方法,線程A收到通知后從對象O的wait()方法返回,進而執(zhí)行后續(xù)操作。

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

notify():

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

notifyAll():

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

wait():

調(diào)用該方法的線程進入 WAITING狀態(tài),只有等待另外線程的通知或被中斷才會返回.需要注意,調(diào)用wait()方法后,會釋放對象的鎖。

wait(long):

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

wait (long,int):

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

2.2等待和通知的標準范式

等待方遵循如下原則:

  •  1.獲取對象的鎖
  • 2.循環(huán)里判斷條件是否滿足,如果條件不滿足,那么調(diào)用對象的wait()方法,被通知后仍要檢查條件。
  • 條件滿足則執(zhí)行對應的邏輯。
synchronized(對象){
    while(條件不滿足){
        對象.wait();
    }
    對應的邏輯
}

通知方遵循如下原則:

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

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

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

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

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

2.3等待超時模式實現(xiàn)一個連接池

調(diào)用場景:

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

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

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

本篇文章關(guān)于Java并發(fā)編程中線程之間的共享和協(xié)作的詳細內(nèi)容就介紹結(jié)束了,想要了解更多相關(guān)Java并發(fā)編程和Java線程的知識,搜索W3Cschool以前的文章或繼續(xù)瀏覽下面的相關(guān)文章!也希望本篇文章能夠?qū)Υ蠹业膶W習和工作能夠有所幫助!


0 人點贊