Java併發編程的重要以及難點(個人總結一)

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/Jet_Green/article/details/81189257

在學習Java併發編程的過程是要注意以下幾點:
1.java併發的三個特性
2.volatile、synchronized、lock,重入鎖、讀寫鎖的區別。
3.線程間的通信機制
4.線程池
5.阻塞隊列
6.ConcurrentHashMap原理以及幾個方法運用
7.sleep、wait、Thread.join的區別
這篇博文主要就是圍繞這七點進行總結

1.java併發的三個特性:

1. 原子性

即一個或者多個操作作爲一個整體,要麼全部執行,要麼都不執行,並且操作在執行過程中不會被線程調度機制打 斷;而且這種操作一旦開始,就一直運行到結束,中間不會有任何上下文切換(context switch)。

int i = 0;//語句1
i++;//語句2

語句1是一個原子性操作。
語句2的分解步驟是:
1)獲取 i 的值;
2)計算 i + 1 的值;
3)將 i + 1 的值賦給 i;
執行以上3個步驟的時候是可以進行線程切換的,因此語句2不是一個原子性操作

2. 可見性
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看到修改的值。

private int i = 0;
private int j = 0;
//線程一
i = 10;
//線程二
j = i;

線程1修改i的值爲10時的執行步驟:
1)將10賦給線程1工作內存中的 i 變量;
2)將線程1工作內存中的 i 變量的值賦給主內存中的 i 變量;
當線程2執行j = i時,線程2的執行步驟:
1)將主內存中的 i 變量的值讀取到線程2的工作內存中;
2)將主內存中的 j 變量的值讀取到線程2的工作內存中;
3)將線程2工作內存中的 i 變量的值賦給線程2工作內存中的 j 變量;
4)將線程2工作內存中的 j 變量的值賦給主內存中的 j 變量;
如果線程1執行完步驟1,線程2開始執行,此時主內存中 i 變量的值仍然爲 0,那麼線程2獲取到的 i 變量的值爲 0,而不是 10。
這就是可見性問題,線程1對 i 變量做了修改之後,線程2沒有立即看到線程1修改的值。
(哈哈 有點繞口啊)
3. 有序性

int i = 0;
int j = 0;
i = 11;
j = 3;

語句可能的執行順序如下:
1)語句1 語句2
2)語句2 語句1
語句1一定在語句2前面執行嗎?答案是否定的,這裏可能會發生執行重排(Instruction Reorder)。一般來說,處理器爲了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先後順序同代碼中的順序一致,但是它會保證程序在單線程環境下最終執行結果和代碼順序執行的結果是一致的。
比如上面的代碼中,語句1和語句2誰先執行對最終的程序結果並沒有影響,那麼就有可能在執行過程中,語句2先執行而語句1後執行。

總結:一個正確執行的併發程序,必須具備原子性、可見性、有序性。否則就有可能導致程序運行結果不正確,甚至引起死循環。

2.volatile、synchronized、lock,重入鎖、讀寫鎖的區別。

volatile:
一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之後,那麼就具備了兩層語義:
  1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
  2)禁止進行指令重排序。
使用volatile的場景:
1.對變量的寫操作不依賴於當前值
2.該變量沒有包含在具有其他變量的不變式中
實際上,這些條件表明,可以被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。
synchronized
synchronized是java典型的線程同步控制方法了。
synchronized兩種使用方法:
1.synchronized修飾方法
2.synchronized修飾代碼塊
具體表現爲:
對於普通同步方法,鎖是當前實例對象。
對於靜態同步方法,鎖是當前類的Class對象。
對於同步方法塊,鎖是Synchonized括號裏配置的對象。
實現方法:
java對象頭和monitor是實現synchronized的基礎
Hotspot虛擬機的對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)
Monitor 是線程私有的數據結構,每一個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。
關於鎖的比較,從書上有這麼一個表格。
鎖的比較
lock鎖
lock鎖擁有了鎖獲取與釋放的可操作性,可中斷的獲取鎖,超時獲取鎖。
lock鎖與synchronized的區別
1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現,synchronized是在JVM層面上實現的,不但可以通過一些監控工具監控synchronized的鎖定,而且在代碼執行時出現異常,JVM會自動釋放鎖定,但是使用Lock則不行,lock是通過代碼實現的,要保證鎖定一定會被釋放,就必須將 unLock()放到finally{} 中;
2)synchronized在發生異常時,會自動釋放線程佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
3)Lock可以讓等待鎖的線程響應中斷,線程可以中斷去幹別的事務,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;
4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
5)Lock可以提高多個線程進行讀操作的效率。
重入鎖
重入鎖ReentrantLock,顧名思義,就是支持重進入的鎖,它表示該鎖能夠支持一個線程對資源的重複加鎖。除此之外,該鎖的還支持獲取鎖時的公平和非公平性選擇。
讀寫鎖
之前提到鎖基本都是排他鎖,這些鎖在同一時刻只允許一個線程進行訪問,而讀寫鎖在同一時刻可以允許多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他寫線程均被堵塞。

3.線程間的通信機制

1.同步
同步就是上面所說的通過各種鎖實現線程的併發。
2.while輪詢的方式
while輪詢方式就是B線程中有個while死循環一直等待着A線程的消息。
3.wait/notify機制
wait/notif機制就是等待/通知機制
一個線程修改了一個對象的值,而另一個線程感知到了變化,然後進行相應的操作,整個過程開始於一個線程,而最終執行又是另一個線程。
等待/通知機制
ps(使用wiat()、notify()、notifyAll()時需要先調用對象加鎖)
4.管道通信
使用java.io.PipedInputStream 和 java.io.PipedOutputStream進行通信

4.線程池

使用線程池的三個好處:
1.降低資源消耗
2.提高響應速度
3.提高線程的可管理性
這裏寫圖片描述
這個圖可以反映線程池實現原理。
線程池創建有六個參數
new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,milliseconds,runnableTaskQueue,handler)
1>corePoolSize:線程池的基本大小
2>maximumPoolSize:線程池最大數量
3>keepAliveTime:線程活動保持時間
4>milliseconds:允許核心線程超時
5>runnableTaskQueue:任務隊列
6>handler:任務拒絕處理器
向線程池提交任務:
1.execute(無返回值)
2.submit(有返回值)
簡單的demo:


public class MyTask implements Runnable {
    private int taskNum;
    public MyTask(int num){
        this.taskNum = num;
    }
    @Override
    public void run() {
        System.out.println("正在執行task "+taskNum);
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task "+taskNum+"執行完畢");
    }

}

主類:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPool {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,200,TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(5));
        for(int i = 0; i < 15; i++) {
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);
            System.out.println("線程中線程數目"+executor.getPoolSize()+",隊列中等待執行的任務數目:"+executor.getQueue().size()+",已執行完別的任務數目:"+executor.getCompletedTaskCount());
        }
        executor.shutdown();
    }
}

5.阻塞隊列

幾種主要的阻塞隊列:
rrayBlockingQueue:基於數組實現的一個阻塞隊列,在創建ArrayBlockingQueue對象時必須制定容量大小。並且可以指定公平性與非公平性,默認情況下爲非公平的,即不保證等待時間最長的隊列最優先能夠訪問隊列。

LinkedBlockingQueue:基於鏈表實現的一個阻塞隊列,在創建LinkedBlockingQueue對象時如果不指定容量大小,則默認大小爲Integer.MAX_VALUE。

PriorityBlockingQueue:以上2種隊列都是先進先出隊列,而PriorityBlockingQueue卻不是,它會按照元素的優先級對元素進行排序,按照優先級順序出隊,每次出隊的元素都是優先級最高的元素。注意,此阻塞隊列爲無界阻塞隊列,即容量沒有上限(通過源碼就可以知道,它沒有容器滿的信號標誌),前面2種都是有界隊列。

DelayQueue:基於PriorityQueue,一種延時阻塞隊列,DelayQueue中的元素只有當其指定的延遲時間到了,才能夠從隊列中獲取到該元素。DelayQueue也是一個無界隊列,因此往隊列中插入數據的操作(生產者)永遠不會被阻塞,而只有獲取數據的操作(消費者)纔會被阻塞。
阻塞隊列解釋:
阻塞隊列是一個隊列,當您嘗試從隊列中出隊並且隊列爲空時,或者嘗試將項目排入隊列並且隊列已滿時,該隊列會被阻止。試圖從空隊列中出隊的線程被阻塞,直到其他線程將一個項插入到隊列中。嘗試將隊列排入隊列中的線程會被阻塞,直到某個其他線程在隊列中產生空間爲止,或者通過將一個或多個項目出隊或完全清除隊列。

6.ConcurrentHashMap原理以及幾個方法運用

由於本人實力有限,就不在解釋ConcurrentHashMap的源代碼了。
ConcurrentHashMap有hashmap的效率又有hashtable的線程安全,所以ConcurrentHashMap很優秀。ConcurrentHashMap是由Segment數組結構和HashEntry數據結構組成
Segment是一種可重入鎖,在ConcurrentHashMap中扮演鎖的角色,HashEntry則用於存儲鍵值對數據
ConcurrentHashMap的三個操作。
1.get操作
get操作的高效之處在於整個get過程中不用加鎖,除非讀到的值是空纔會加鎖重讀。爲什麼呢?因爲get方法裏將要用的共享變量都定義爲volatile類型,定義成volatile的變量,能夠在線程之間保持可見性,能夠被多線程同時讀,並且保證不會讀到過期的值,但是隻能被單線程寫(有一種情況可以被多線程寫,就是寫入的值不依賴於原值)、
2.put操作
由於put方法裏需要對共享變量進行寫入操作,所以爲了線程安全,在操作共享變量時必須加鎖,put方法首先定位到Segment,然後在Segment進行插入操作。
(1)是否需要擴容
在插入元素前會先判斷Segment裏的HashEntry數組是否超過容量,如果超過,就擴容。
(2)如何擴容
在擴容的時候首先會創建一個容量是原來容量兩倍的數組,然後將原數組裏的元素進行再散列後插入新的數組裏。
3.size操作
先嚐試兩次不鎖住Segment的方式來統計各個Segment大小,如果統計的過程中,容器的count發生了變化,則再採用加鎖的方式來統計所有Segment的大小。

7.sleep、wait、Thread.join的區別

1.sleep來自Thread類,wait來自Object類
2.sleep方法沒有釋放鎖,wait釋放鎖,使得其他進程可以使用同步控制塊或者方法。
3.wait、notify、notifyAll只能在同步控制方法或者同步控制塊裏使用,sleep可以在任何地方使用。
4.sleep必須捕獲異常,wait、notify、notifyAll不需要。

Thread.join的定義:
如果一個線程A執行了thread.join語句,其含義:當前線程A等待Thread線程終止後才從thread.join返回。
Thread類中的join方法的主要作用就是同步,它可以使得線程之間的並行執行變爲串行執行

以上就是本人對Java併發編程難點的一點點小總結,以後還會繼續更新,如果不足和錯誤還請多多指出,謝謝!

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