1. 程式人生 > >多執行緒、併發(很亂)

多執行緒、併發(很亂)

先複習下


我們先了解一下Thread的幾個重要方法。

a、start()方法,呼叫該方法開始執行該執行緒;

b、stop()方法,呼叫該方法強制結束該執行緒執行;

c、join方法,呼叫該方法等待該執行緒結束,用於主執行緒等待子執行緒執行結束。

d、sleep()方法,呼叫該方法該執行緒進入等待。

e、run()方法,呼叫該方法直接執行執行緒的run()方法,但是執行緒呼叫start()方法時也會執行run()方法,區別就是一個是由執行緒排程執行run()方法,一個是直接呼叫了執行緒中的run()方法!!

自己的理解:

    首先object提供了wait和notify,網上很多都講得非常奇怪

    這兩個方法是必須要跟synchronize一起用的

,否則會異常。

  • wait方法:被鎖的物件.wait()表示,正在使用該鎖的執行緒,不再擁有該鎖,所以執行緒也就停止,進入了等待notify喚醒的block區。
  • notify:被鎖的物件.notify()表示,在block區中找一個優先順序高的執行緒喚醒,進入runnable,等待獲得鎖

所以,它和sleep是沒有可比性的,sleep是操控執行緒,而它是操控物件

****鎖型別

  • 可重入鎖:在執行物件中所有同步方法不用再次獲得鎖

  • 可中斷鎖:在等待獲取鎖過程中可中斷

  • 公平鎖: 按等待獲取鎖的執行緒的等待時間進行獲取,等待時間長的具有優先獲取鎖權利

  • 讀寫鎖:對資源讀取和寫入的時候拆分為2部分處理,讀的時候可以多執行緒一起讀,寫的時候必須同步地寫

那麼lock和synchronized的區別對比如下:
1)synchronized 在成功完成功能或者丟擲異常時,虛擬機器會自動釋放執行緒佔有的鎖;而Lock物件在發生異常時,如果沒有主動呼叫unLock()方法去釋放鎖,則鎖物件會一直持有,因此使用Lock時需要在finally塊中釋放鎖;
2)lock介面鎖可以通過多種方法來嘗試獲取鎖包括立即返回是否成功的tryLock(),以及一直嘗試獲取的lock()方法和嘗試等待指定時間長度獲取的方法,相對靈活了許多比synchronized;
3) 通過在讀多,寫少的高併發情況下,我們用ReentrantReadWriteLock分別獲取讀鎖和寫鎖來提高系統的效能,因為讀鎖是共享鎖,即可以同時有多個執行緒讀取共享資源,而寫鎖則保證了對共享資源的修改只能是單執行緒的。

synchronized與Lock的區別

1、我把兩者的區別分類到了一個表中,方便大家對比:

類別synchronizedLock
存在層次Java的關鍵字,在jvm層面上是一個類
鎖的釋放1、以獲取鎖的執行緒執行完同步程式碼,釋放鎖 2、執行緒執行發生異常,jvm會讓執行緒釋放鎖在finally中必須釋放鎖,不然容易造成執行緒死鎖
鎖的獲取假設A執行緒獲得鎖,B執行緒等待。如果A執行緒阻塞,B執行緒會一直等待可以嘗試獲得鎖,執行緒可以不用一直等待
鎖狀態無法判斷可以判斷
鎖型別可重入 不可中斷 非公平可重入 、可判斷 、可公平(兩者皆可)、 可中斷
效能差不多差不多
***** synchronized不可中斷,所以效能有時會比較差,此時可以用Lock介面的ReentrantLock  用interrupt中斷

多執行緒中斷機制

在 java中啟動執行緒非常容易,大多數情況下是讓一個執行緒執行完自己的任務然後自己停掉。

一個執行緒在未正常結束之前, 被強制終止是很危險的事情. 因為它可能帶來完全預料不到的嚴重後果,比如會帶著自己所持有的鎖而永遠的休眠,遲遲不歸還鎖等

但是呼叫 interrupt() 方法會設定執行緒的中斷標記,此時呼叫 interrupted() 方法會返回 true。因此可以在迴圈體中使用 interrupted() 方法來判斷執行緒是否處於中斷狀態,從而提前結束執行緒

如果一個執行緒的 run() 方法執行一個無限迴圈,並且沒有執行 sleep() 等會丟擲 InterruptedException 的操作(阻塞、限期等待或者無限期等待狀態),那麼呼叫執行緒的 interrupt() 方法就無法使執行緒提前結束(如IO阻塞、sync操作)

執行緒中斷是一種協作機制,呼叫執行緒物件的interrupt方法並不一定就中斷了正在執行的執行緒,它只是要求執行緒自己在合適的時間中斷自己。

await() signal() signalAll()

java.util.concurrent 類庫中提供了 Condition 類來實現執行緒之間的協調,可以在 Condition 上呼叫 await() 方法使執行緒等待,其它執行緒呼叫 signal() 或 signalAll() 方法喚醒等待的執行緒。相比於 wait() 這種等待方式,await() 可以指定等待的條件,因此更加靈活。

使用 Lock 來獲取一個 Condition 物件。

public class AwaitSignalExample {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    AwaitSignalExample example = new AwaitSignalExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
before
after
很明顯,相對於synchronized,Lock這種可以控制多個condition的方式,更加方便。

Java記憶體模型

Java 記憶體模型試圖遮蔽各種硬體和作業系統的記憶體訪問差異,以實現讓 Java 程式在各種平臺下都能達到一致的記憶體訪問效果。

主記憶體與工作記憶體

處理器上的暫存器的讀寫的速度比記憶體快幾個數量級,為了解決這種速度矛盾,在它們之間加入了快取記憶體。

加入快取記憶體帶來了一個新的問題:快取一致性。如果多個快取共享同一塊主記憶體區域,那麼多個快取的資料可能會不一致,需要一些協議來解決這個問題。


所有的變數都儲存在主記憶體中,每個執行緒還有自己的工作記憶體,工作記憶體儲存在快取記憶體或者暫存器中,儲存了該執行緒使用的變數的主記憶體副本拷貝。

執行緒只能直接操作工作記憶體中的變數,不同執行緒之間的變數值傳遞需要通過主記憶體來完成。


記憶體間互動操作

Java 記憶體模型定義了 8 個操作來完成主記憶體和工作記憶體的互動操作。


  • read:把一個變數的值從主記憶體傳輸到工作記憶體中
  • load:在 read 之後執行,把 read 得到的值放入工作記憶體的變數副本中
  • use:把工作記憶體中一個變數的值傳遞給執行引擎
  • assign:把一個從執行引擎接收到的值賦給工作記憶體的變數
  • store:把工作記憶體的一個變數的值傳送到主記憶體中
  • write:在 store 之後執行,把 store 得到的值放入主記憶體的變數中
  • lock:作用於主記憶體的變數
  • unlock

記憶體模型三大特性

1.原子性            

Java 記憶體模型保證了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如對一個 int 型別的變數執行 assign 賦值操作,這個操作就是原子性的。但是 Java 記憶體模型允許虛擬機器將沒有被 volatile 修飾的 64 位資料(long,double)的讀寫操作劃分為兩次 32 位的操作來進行,即 load、store、read 和 write 操作可以不具備原子性

有一個錯誤認識就是,int 等原子性的變數在多執行緒環境中不會出現執行緒安全問題。前面的執行緒不安全示例程式碼中,cnt 變數屬於 int 型別變數,1000 個執行緒對它進行自增操作之後,得到的值為 997 而不是 1000。

2. 可見性

可見性指當一個執行緒修改了共享變數的值,其它執行緒能夠立即得知這個修改。Java 記憶體模型是通過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值來實現可見性的。

volatile 可保證可見性。synchronized 也能夠保證可見性,對一個變數執行 unlock 操作之前,必須把變數值同步回主記憶體。final 關鍵字也能保證可見性:被 final 關鍵字修飾的欄位在構造器中一旦初始化完成,並且沒有發生 this 逃逸(其它執行緒可以通過 this 引用訪問到初始化了一半的物件),那麼其它執行緒就能看見 final 欄位的值。

對前面的執行緒不安全示例中的 cnt 變數用 volatile 修飾,不能解決執行緒不安全問題,因為 volatile 並不能保證操作的原子性。

3. 有序性

有序性是指:在本執行緒內觀察,所有操作都是有序的。在一個執行緒觀察另一個執行緒,所有操作都是無序的,無序是因為發生了指令重排序。

在 Java 記憶體模型中,允許編譯器和處理器對指令進行重排序,重排序過程不會影響到單執行緒程式的執行,卻會影響到多執行緒併發執行的正確性。

volatile 關鍵字通過新增記憶體屏障的方式來禁止指令重排,即重排序時不能把後面的指令放到記憶體屏障之前。

也可以通過 synchronized 來保證有序性,它保證每個時刻只有一個執行緒執行同步程式碼,相當於是讓執行緒順序執行同步程式碼。


J.U.C - AQS

java.util.concurrent(J.U.C)大大提高了併發效能,AQS 被認為是 J.U.C 的核心。

CountdownLatch

用來控制一個執行緒等待多個執行緒。

維護了一個計數器 cnt,每次呼叫 countDown() 方法會讓計數器的值減 1,減到 0 的時候,那些因為呼叫 await() 方法而在等待的執行緒就會被喚醒。

public class CountdownLatchExample {

    public static void main(String[] args) throws InterruptedException {
        final int totalThread = 10;
        CountDownLatch countDownLatch = new CountDownLatch(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(() -> {
                System.out.print("run..");
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        System.out.println("end");
        executorService.shutdown();
    }
}
run..run..run..run..run..run..run..run..run..run..end

volatile關鍵字

1.volatile保證可見性

一旦一個共享變數(類的成員變數、類的靜態成員變數)被volatile修飾之後,那麼就具備了兩層語義:

保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。

  • 第一:使用volatile關鍵字會強制將修改的值立即寫入主存;
  • 第二:使用volatile關鍵字的話,當執行緒2進行修改時,會導致執行緒1的工作記憶體中快取變數stop的快取行無效(反映到硬體層的話,就是CPU的L1或者L2快取中對應的快取行無效);
  • 第三:由於執行緒1的工作記憶體中快取變數stop的快取行無效,所以執行緒1再次讀取變數stop的值時會去主存讀取。

2.volatile不能確保原子性

3.volatile保證有序性 

volatile關鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。

  • 當程式執行到volatile變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;
  • 在進行指令優化時,不能將在對volatile變數的讀操作或者寫操作的語句放在其後面執行,也不能把volatile變數後面的語句放到其前面執行。
123456789//執行緒1:context = loadContext();   //語句1inited = true;             //語句2//執行緒2:while(!inited ){sleep()}doSomethingwithconfig(context);

有可能語句2會在語句1之前執行,那麼久可能導致context還沒被初始化,而執行緒2中就使用未初始化的context去進行操作,導致程式出錯。

這裡如果用volatile關鍵字對inited變數進行修飾,就不會出現這種問題了,因為當執行到語句2時,必定能保證context已經初始化完畢。

volatile的應用場景

synchronized關鍵字是防止多個執行緒同時執行一段程式碼,那麼就會很影響程式執行效率,而volatile關鍵字在某些情況下效能要優於synchronized,但是要注意volatile關鍵字是無法替代synchronized關鍵字的,因為volatile關鍵字無法保證操作的原子性。通常來說,使用volatile必須具備以下2個條件:

1)對變數的寫操作不依賴於當前值

2)該變數沒有包含在具有其他變數的不變式中

下面列舉幾個Java中使用volatile的幾個場景。

①.狀態標記量

123456789volatile boolean flag = false;//執行緒1while(!flag){doSomething();}//執行緒2public void setFlag() {

相關推薦

執行併發

先複習下我們先了解一下Thread的幾個重要方法。a、start()方法,呼叫該方法開始執行該執行緒;b、stop()方法,呼叫該方法強制結束該執行緒執行;c、join方法,呼叫該方法等待該執行緒結束,用於主執行緒等待子執行緒執行結束。d、sleep()方法,呼叫該方法該執行

iOS 執行使用總結實用

每次準備開始新的航行,總是要複習一遍演算法啊,多執行緒啊,記憶體管理啊之類的理論和應用知識,這次把他們整理成文件,方便以後的學習和不斷的積累進步。         多執行緒給我留下的是痛苦的記憶,當時在上家創業公司的最後階段,就是被Feature Phone上面的多執行緒方案導致bug叢生,搞的焦頭爛額。  

Java基礎學習總結104——執行併發工具類相關的面試題

執行緒的概念 執行緒是程式執行的最小單位,也是作業系統排程和分派CPU的最小單元,是程序中的一個實體,是程序中的實際運作單位。可以在一個程序中啟動多個執行緒來完成不同的任務,這些執行緒共享該程序擁有的資源。 執行緒程序區別 程序是程式的實體,也是執行緒的容器,一個程序可以包含多個執行緒,程序是資源分配的基本單

Java執行併發

Condition等待和喚醒 在我們的並行程式中,避免不了某些寫成要預先規定好的順序執行,例如:先新增後修改,先買後賣,先進後出,對於這些場景,使用JUC的Conditon物件再合適不過了。 JUC中提供了Condition物件,用於讓指定執行緒等待與喚

Java執行併發

Synchronized執行緒同步機制 很多執行緒同時對同一個資料或者檔案進行訪問的時候,對於這個檔案如果進行併發讀寫可能會產生問題。 多執行緒機制來保證同一個時間,只有一個執行緒對這個資源進行讀寫,來保證多執行緒環境下是健壯的。 程式碼案例:

Java執行併發

多執行緒與併發的基礎問題 併發就是指程式同時處理多個任務的能力(一個程式被多個使用者訪問都能看到自己預期的結果) 併發的根源在於對多工情況下訪問資源的有效控制! 併發背後的問題 public class DownloadSimple {

【Java併發程式設計】之六:Runnable和Thread實現執行的區別含程式碼

    Java中實現多執行緒有兩種方法:繼承Thread類、實現Runnable介面,在程式開發中只要是多執行緒,肯定永遠以實現Runnable介面為主,因為實現Runnable介面相比繼承Th

綜合應用題:執行複製檔案知識點:執行隨機讀寫流

要求:使用多執行緒複製一個檔案(使用多執行緒複製一個檔案可以加快檔案的複製速度) 程式碼: package 多執行緒複製檔案; import java.io.File; import java.io.FileNotFoundException; impor

執行併發使用join方法讓執行按順序執行

一.執行緒的join方法作用 join方法把指定的執行緒新增到當前執行緒中,可以不給引數直接thread.join(),也可以給一個時間引數,單位為毫秒thread.join(500)。事實上join方法是通過wait方法來實現的。比如執行緒A中加入了執行緒B.join方法

Java 執行併發:AQS

我們前面幾張提到過,JUC 這個包裡面的工具類的底層就是使用 CAS 和 volatile 來保證執行緒安全的,整個 JUC 包裡面的類都是基於它們構建的。今天我們介紹一個非常重要的同步器,這個類是 JDK 在 CAS 和 volatile 的基礎上為我們提供的一個同步工具類。 背景 AbstractQueu

面試必備——Java執行併發

1.程序和執行緒的 (1)由來 1)序列 最初的計算機只能接受一些特定的指令,使用者輸入一個指令,計算機就做出一個操作。當用戶在思考或者輸入時,計算機就在等待。顯然這樣效率低下,在很多時候,計算機都處在等待狀態。 2)批處理 提高計算機的效率,不用等待使用者的輸入,把一系列需要操作的指令寫下來,形成一個清單

python爬蟲——執行+協程threading+gevent

以下摘自這篇文章:https://blog.csdn.net/qq_23926575/article/details/76375337 在爬蟲中廣泛運用的多執行緒+協程的解決方案,親測可提高效率至少十倍以上。 本文既然提到了執行緒和協程,我覺得有必要在此對程序、執行緒、協程做一個簡單的對

java執行快速入門十一

在方法上面加synchonizd用的是this鎖 package com.cppdy; class MyThread7 implements Runnable { private Integer ticketCount = 100; public boolean falg = tr

java執行快速入門十二

在靜態方法上面加synchonizd用的是位元組碼檔案鎖 package com.cppdy; class MyThread8 implements Runnable { private static Integer ticketCount = 100; public boolea

java執行快速入門十六

ThreadLocal關鍵字實現每個執行緒有自己的變數 package com.cppdy; class Number { private int num; public static ThreadLocal<Integer> threadLocal = new Th

java執行快速入門十八

Lock鎖是JDK1.5之後推出的併發包裡面的關鍵字(注意捕獲異常,釋放鎖) Lock與synchronized的區別   Lock鎖可以人為的釋放鎖(相當於汽車中的手動擋)   synchronized當執行緒執行完畢或者丟擲異常的話,鎖自動釋放(相當於汽車中的自動擋) Condition用法  

Java定時任務Timer排程器【二】 執行原始碼分析圖文版

  上一節通過一個小例子分析了Timer執行過程,牽涉的執行執行緒雖然只有兩個,但實際場景會比上面複雜一些。 首先通過一張簡單類圖(只列出簡單的依賴關係)看一下Timer暴露的介面。   為了演示Timer所暴露的介面,下面舉一個極端的例子(每一個介面方法面

CVI執行資料保護安全佇列

一個執行緒產生資料,加入安全佇列;一個執行緒顯示資料,讀取安全佇列。 #include <windows.h> #include <stdio.h> #include <utility.h> int CVICALLBACK Thre

Java 執行學習筆記十一 單例設計模式延遲載入/懶漢模式DCL解決執行安全問題

package extthread; import test.MyObject; public class MyThread extends Thread { @Override publi

執行優化思路轉載的

樣例程式 程式功能:求從1一直到 APPLE_MAX_VALUE (100000000) 相加累計的和,並賦值給 apple 的a 和b ;求 orange 資料結構中的 a[i]+b[i ] 的和,迴圈 ORANGE_MAX_VALUE(1000000) 次。