2.6W + 字,徹底搞懂 JUC!


來源:blog.csdn.net/wangwenpeng0529/article/details/105769978

簡介

在 Java 5.0 提供了 java.util.concurrent(簡稱JUC)包,在此包中增加了在並發編程中很常用的工具類,用於定義類似於線程的自定義子系統,包括線程池,異步 IO 和輕量級任務框架;還提供了設計用於多線程上下文中的 Collection 實現等

volatile 關鍵字

內存可見性

內存可見性(Memory Visibility)是指當某個線程正在使用對象狀態而另一個線程在同時修改該狀態,需要確保當一個線程修改了對象狀態后,其他線程能夠看到發生的狀態變化。

可見性錯誤是指當讀操作與寫操作在不同的線程中執行時,我們無法確保執行讀操作的線程能適時地看到其他線程寫入的值,有時甚至是根本不可能的事情。

我們可以通過同步來保證對象被安全地發布。除此之外我們也可以使用一種更加輕量級的 volatile 變量。

Java 提供了一種稍弱的同步機制,即 volatile 變量,用來確保將變量的更新操作通知到其他線程。可以將 volatile 看做一個輕量級的鎖,但是又與鎖有些不同:

  • 對於多線程,不是一種互斥關系
  • 不能保證變量狀態的“原子性操作

問題代碼示例

/**
 * @ClassName TestVolatile
 * @Description: Thread 已經修改了flag,但是main線程還是拿到的false
 * @Author: WangWenpeng
 * @Version 1.0
 */
public class TestVolatile {
    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();
        while (true) {
            if (td.isFlag()) {
                System.out.println("______________");
                break;
            }
        }
    }
}

class ThreadDemo implements Runnable {
    private boolean flag = false;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        try {
            //增加這種出現問題的幾率
            Thread.sleep(200);
        } catch (Exception e) {
        }
        flag = true;
        System.out.println("flag=" + isFlag());
    }
}

兩個線程同時修改這一個flag,為什么main拿到的還是這種修改之前的值

內存分析

解決方法,加鎖

public class TestVolatile {
    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();
        while (true) {
            synchronized (td) {
                if (td.isFlag()) {
                    System.out.println("______________");
                    break;
                }
            }
        }
    }
}

加了鎖,就可以讓while循環每次都從主存中去讀取數據,這樣就能讀取到true了。但是一加鎖,每次只能有一個線程訪問,當一個線程持有鎖時,其他的就會阻塞,效率就非常低了。不想加鎖,又要解決內存可見性問題,那么就可以使用volatile關鍵字。

volatile

private volatile boolean flag = false;

volatile 關鍵字:當多個線程進行操作共享數據時,可以保證內存中的數據可見。相較於 synchronized 是一種較為輕量級的同步策略。

注意:

  • volatile 不具備“互斥性”
  • volatile 不能保證變量的“原子性”

原子性

所謂原子性就是操作不可再細分

問題代碼

package com.atguigu.juc;

/**
 * @ClassName TestAtomicDemo
 * @Description:
 * @Author: WangWenpeng
 * @Version 1.0
 */
public class TestAtomicDemo {
    public static void main(String[] args) {
        AtomicDemo ad = new AtomicDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(ad).start();
        }
    }
}

class AtomicDemo implements Runnable {

    private int serialNumber = 0;

    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
        }
        System.out.println(Thread.currentThread().getName() + ":" + getSerialNumber());
    }

    public int getSerialNumber() {
        return serialNumber++;
    }
}

看到這里,好像和上面的內存可見性問題一樣。是不是加個volatile關鍵字就可以了呢?其實不是的,因為加了volatile,只是相當於所有線程都是在主存中操作數據而已,但是不具備互斥性。比如兩個線程同時讀取主存中的0,然后又同時自增,同時寫入主存,結果還是會出現重復數據。

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @ClassName TestAtomicDemo
 *
 * 原子變量:在 java.util.concurrent.atomic 包下提供了一些原子變量。
 *  1. volatile 保證內存可見性
 *  2. CAS(Compare-And-Swap) 算法保證數據變量的原子性
 *   CAS 算法是硬件對於並發操作的支持
 *   CAS 包含了三個操作數:
 *   ①內存值  V
 *   ②預估值  A
 *   ③更新值  B
 *   當且僅當 V == A 時, V = B; 否則,不會執行任何操作。
 *
 * @Description:
 * @Author: WangWenpeng
 * @Version 1.0
 */
public class TestAtomicDemo {
    public static void main(String[] args) {
        AtomicDemo ad = new AtomicDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(ad).start();
        }
    }
}

class AtomicDemo implements Runnable {

    private AtomicInteger serialNumber = new AtomicInteger(0);

    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
        }
        System.out.println(Thread.currentThread().getName() + ":" + getSerialNumber());
    }

    public int getSerialNumber() {
        return serialNumber.getAndIncrement();
    }
}

AtomicInteger這個玩意是具有原子性的integer,用它替換后發現能保證線程安全

Connected to the target VM, address: '127.0.0.1:61323', transport: 'socket'
Thread-4:1
Thread-6:4
Thread-0:3
Thread-7:9
Thread-2:2
Thread-5:6
Thread-3:5
Thread-1:0
Thread-9:7
Thread-8:8
Disconnected from the target VM, address: '127.0.0.1:61323', transport: 'socket'

CAS 算法

解決了原子性問題,解決了內存可見性的問題

CAS (Compare-And-Swap) 是一種硬件對並發的支持,針對多處理器操作而設計的處理器中的一種特殊指令,用於管理對共享數據的並發訪問。CAS 是一種無鎖的非阻塞算法的實現。

CAS 包含了 3 個操作數:

  • 需要讀寫的內存值 V 進行比較的值 A 擬寫入的新值 B
  • 當且僅當 V 的值等於 A 時, CAS 通過原子方式用新值 B 來更新 V 的值,否則不會執行任何操作。
  • CAS比較失敗的時候不會放棄CPU,會反復執行,直到自己修改主內存的數據

模擬CAS算法

/**
 * @ClassName TestCompareAndSwap
 * @Description: cas模擬  模擬帶鎖,底層不是帶synchronized
 * cas 每次修改之前,都會執行獲取比較操作
 * @Author: WangWenpeng
 * @Version 1.0
 */
public class TestCompareAndSwap {

    public static void main(String[] args) {
        final CompareAndSwap cas = new CompareAndSwap();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int expectValue = cas.getValue();
                    System.out.println(cas.compareAndSet(expectValue, (int) Math.random() * 101));
                }
            }).start();
        }
    }
}

class CompareAndSwap {
    public int value;

    //獲取內存值
    public synchronized int getValue() {
        return value;
    }

    //比較並交換
    public synchronized int compareAndSwap(int expectValue, int newV) {
        int oldV = value;
        //內存值和預估值一致 就替換
        if (oldV == expectValue) {
            this.value = newV;
        }
        return oldV;
    }

    //設置 調用比較並交換  看期望值和原來的值是否一致
    public synchronized boolean compareAndSet(int expectValue, int newV) {
        return expectValue == compareAndSwap(expectValue, newV);
    }
}

推薦一個 Spring Boot 基礎教程及實戰示例:
https://github.com/javastacks/javastack

原子變量

小工具包,支持在單個變量上解除鎖的線程安全編程。事實上,此包中的類可將 volatile 值、字段和數組元素的概念擴展到那些也提供原子條件更新操作的類。

類 AtomicBoolean、 AtomicInteger、 AtomicLong 和 AtomicReference 的實例各自提供對相應類型單個變量的訪問和更新。每個類也為該類型提供適當的實用工具方法。

AtomicIntegerArray、 AtomicLongArray 和 AtomicReferenceArray 類進一步擴展了原子操作,對這些類型的數組提供了支持。這些類在為其數組元素提供 volatile 訪問語義方面也引人注目,這對於普通數組來說是不受支持的。

核心方法:boolean compareAndSet(expectedValue, updateValue)

java.util.concurrent.atomic 包下提供了一些原子操作的常用類:

AtomicBoolean 、
AtomicInteger 、
AtomicLong 、
AtomicReference
AtomicIntegerArray 、
AtomicLongArray
AtomicMarkableReference
AtomicReferenceArray
AtomicStampedReference3-ConcurrentHashMap

鎖分段機制ConcurrentHashMap

線程安全的hash表 每一段都是一個獨立的鎖

Java 5.0 在 java.util.concurrent 包中提供了多種並發容器類來改進同步容器的性能。

ConcurrentHashMap 同步容器類是Java 5 增加的一個線程安全的哈希表。對與多線程的操作,介於 HashMap 與 Hashtable 之間。內部采用“鎖分段”機制替代 Hashtable 的獨占鎖。進而提高性能。

此包還提供了設計用於多線程上下文中的 Collection 實現:ConcurrentHashMap、 ConcurrentSkipListMap、 ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。當期望許多線程訪問一個給定 collection 時, ConcurrentHashMap 通常優於同步的 HashMap,ConcurrentSkipListMap 通常優於同步的 TreeMap。當期望的讀數和遍歷遠遠大於列表的更新數時, CopyOnWriteArrayList 優於同步的 ArrayList。

ConcurrentHashMap就是一個線程安全的hash表。我們知道HashMap是線程不安全的,Hash Table加了鎖,是線程安全的,因此它效率低。HashTable加鎖就是將整個hash表鎖起來,當有多個線程訪問時,同一時間只能有一個線程訪問,並行變成串行,因此效率低。所以JDK1.5后提供了ConcurrentHashMap,它采用了鎖分段機制。

1.8以后底層又換成了CAS,把鎖分段機制放棄了。CAS基本就達到了無鎖的境界。

另外,Java 8+ 系列面試題和答案全部整理好了,微信搜索​Java技術棧,在后台發送:面試,​可以在線閱讀。

CopyOnWrite寫入並復制

package com.atguigu.juc;import java.util.*;import java.util.concurrent.CopyOnWriteArrayList;/* * CopyOnWriteArrayList/CopyOnWriteArraySet : “寫入並復制” * 注意:添加操作多時,效率低,因為每次添加時都會進行復制,開銷非常的大。並發迭代操作多時可以選擇。 */public class TestCopyOnWriteArrayList {    public static void main(String[] args) {        HelloThread ht = new HelloThread();        for (int i = 0; i < 10; i++) {            new Thread(ht).start();        }    }}class HelloThread implements Runnable {    //private static List<String> list = Collections.synchronizedList(new ArrayList<String>()); //每次修改都會復制  添加操作多時  不適合選這個    private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();    static {        list.add("AA");        list.add("BB");        list.add("CC");    }    @Override    public void run() {        Iterator<String> it = list.iterator();        while (it.hasNext()) {            System.out.println(it.next());            list.add("AA");//邊迭代邊添加  會出現並發修改異常        }    }}

CountDownLatch 閉鎖

閉鎖,在完成某些運算時,只有其他所有線程的運算全部完成,當前運算才繼續執行,Java 5.0 在 java.util.concurrent 包中提供了多種並發容器類來改進同步容器的性能。

CountDownLatch 一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。

閉鎖可以延遲線程的進度直到其到達終止狀態,閉鎖可以用來確保某些活動直到其他活動都完成才繼續執行:

  • 確保某個計算在其需要的所有資源都被初始化之后才繼續執行;
  • 確保某個服務在其依賴的所有其他服務都已經啟動之后才啟動;
  • 等待直到某個操作所有參與者都准備就緒再繼續執行。
import java.util.concurrent.CountDownLatch;/** * @ClassName TestCountDownLatch * @Description: 閉鎖操作 其他線程都執行完成后當前線程才能繼續執行 * @Author: WangWenpeng * @Version 1.0 */public class TestCountDownLatch {    public static void main(String[] args) throws InterruptedException {        final CountDownLatch latch = new CountDownLatch(5);        LatchDemo ld = new LatchDemo(latch);        //計算執行時間        long start = System.currentTimeMillis();        for (int i = 0; i < 5; i++) {            new Thread(ld).start();        }        //閉鎖 等待其他線程的執行        latch.await();        long end = System.currentTimeMillis();        System.out.println("執行時間===============================" + (end - start));    }}class LatchDemo implements Runnable {    private CountDownLatch latch;    public LatchDemo(CountDownLatch latch) {        this.latch = latch;    }    @Override    public void run() {        synchronized (this) {            try {                for (int i = 0; i < 1000; i++) {                    if (i % 2 == 0) {                        System.out.println(Thread.currentThread().getName() + "-------------" + i);                    }                }            } finally {                //線程執行完畢后  countdown 減一                latch.countDown();            }        }    }}

實現 Callable 接口

Java 5.0 在 java.util.concurrent 提供了一個新的創建執行線程的方式:Callable 接口

Callable 接口類似於 Runnable,兩者都是為那些其實例可能被另一個線程執行的類設計的。但是 Runnable 不會返回結果,並且無法拋出經過檢查的異常。

Callable 需要依賴FutureTask , FutureTask 也可以用作閉鎖。

package com.atguigu.juc;import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;/** * @ClassName TestCallable * @Description: * @Author: WangWenpeng * @Version 1.0 */public class TestCallable {    public static void main(String[] args) throws ExecutionException, InterruptedException {        CallableThreadDemo td = new CallableThreadDemo();        //futureTask 實現類的支持  用於接收運算結果        FutureTask<Integer> result = new FutureTask<>(td);        new Thread(result).start();//線程開始運行         Integer sum = result.get();//等待線程執行完成后 才能獲取到結果 也可以用於閉鎖操作作為等待項        System.out.println("總和" + sum);    }}/** * @Description 多了一個方法的返回值  並且可以拋出異常 * @Author WangWenpeng * @Param */class CallableThreadDemo implements Callable<Integer> {    @Override    public Integer call() throws Exception {        int sum = 0;        for (int i = 0; i < 100; i++) {            sum += i;        }        return sum;    }}//class ThreadDemo implements Runnable{//    @Override//    public void run() {////    }//}

同步鎖顯示鎖 Lock

在 Java 5.0 之前,協調共享對象的訪問時可以使用的機制只有 synchronized 和 volatile 。Java 5.0 后增加了一些新的機制,但並不是一種替代內置鎖的方法,而是當內置鎖不適用時,作為一種可選擇的高級功能。

ReentrantLock 實現了 Lock 接口,並提供了與synchronized 相同的互斥性和內存可見性。但相較於synchronized 提供了更高的處理鎖的靈活性。

package com.atguigu.juc;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * @ClassName TestLock * @Description: 同步鎖 更靈活的方式 * lock上鎖  unlock釋放鎖 * @Author: WangWenpeng * @Version 1.0 */public class TestLock {    public static void main(String[] args) {        Ticket ticket = new Ticket();        new Thread(ticket, "1號窗口").start();        new Thread(ticket, "2號窗口").start();        new Thread(ticket, "3號窗口").start();    }}class Ticket implements Runnable {    private int ticket = 100;    private Lock lock = new ReentrantLock();    @Override    public void run() {        //這樣買票沒有問題        //while (ticket > 0) {        //    System.out.println(Thread.currentThread().getName() + "完成售票,余票為" + --ticket);        //}        //放大問題出現的記錄 出現了負票號        //while (true) {        //    if (ticket > 0) {        //        try {        //            Thread.sleep(200);        //            System.out.println(Thread.currentThread().getName() + "完成售票,余票為" + --ticket);        //        } catch (InterruptedException e) {        //            e.printStackTrace();        //        }        //    }        //}        //顯式加鎖和釋放鎖        while (true) {            lock.lock();            try {                if (ticket > 0) {                    try {                        Thread.sleep(200);                        System.out.println(Thread.currentThread().getName() + "完成售票,余票為" + --ticket);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            } finally {                lock.unlock();            }        }    }}

lock的等待喚醒機制

/**
 * @ClassName TestProductorAndConsumer
 * @Description: 生產者消費者模型
 * @Author: WangWenpeng
 * @Version 1.0
 */
public class TestProductorAndConsumer {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Productor productor = new Productor(clerk);
        Consumer consumer = new Consumer(clerk);

        //沒有等待喚醒機制的時候
        //生產者一直生產 不考錄消費者  可能造成數據丟失
        //消費者一直消費 不考慮生產者  可能造成重復消費
        new Thread(productor, "生產者a").start();
        new Thread(consumer, "消費者a").start();
    }
}

/**
 * 店員
 */
class Clerk {
    //庫存共享數據 存在安全問題
    private int product = 0;

    //進貨
    public synchronized void get() {
        if (product >= 10) {
            System.out.println("產品已滿,無法添加");
            try {
                this.wait();
            } catch (InterruptedException e) {
            }
        } else {
            this.notifyAll();
            System.out.println(Thread.currentThread().getName() + "店員進貨1個產品 庫存為" + ++product);
        }
    }

    //賣貨
    public synchronized void sale() {
        if (product <= 0) {
            System.out.println("產品缺貨,無法售賣");
            try {
                this.wait();
            } catch (InterruptedException e) {
            }
        } else {
            System.out.println(Thread.currentThread().getName() + "店員銷售1個產品 庫存為" + --product);
            this.notifyAll();
        }
    }
}

/**
 * 生產者
 */
class Productor implements Runnable {
    private Clerk clerk;
    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            clerk.get();
        }
    }
}

/**
 * @Description 消費者
 * @Author WangWenpeng
 * @Date 6:45 2020/4/27
 * @Param
 */
class Consumer implements Runnable {
    private Clerk clerk;
    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            clerk.sale();
        }
    }
}

lock出問題的情況

生產者等待,增加出問題的幾率 庫存空位改成1

 if (product >= 1) {
            System.out.println("產品已滿,無法添加");
--------------------------------------------------------------------------------------

@Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
            }
            clerk.get();
        }
    }

消費者等於0的時候, 兩個消費者同時生產,之后停住了,沒有其他線程去喚醒,導致停在生產者這里。

解決方法,去掉else,讓他能走喚醒方法。

//進貨public synchronized void get() {    if (product >= 1) {        System.out.println("產品已滿,無法添加");        try {            this.wait();        } catch (InterruptedException e) {        }    }    this.notifyAll();    System.out.println(Thread.currentThread().getName() + "店員進貨1個產品 庫存為" + ++product);}//賣貨public synchronized void sale() {    if (product <= 0) {        System.out.println("產品缺貨,無法售賣");        try {            this.wait();        } catch (InterruptedException e) {        }    }    System.out.println(Thread.currentThread().getName() + "店員銷售1個產品 庫存為" + --product);    this.notifyAll();}

讓線程能走到notifyall,可以避免停止在生產者這里。

虛假喚醒

增加到兩個消費者兩個生產者之后,如果現在沒有庫存,兩個消費者都停止在wait,然后出現生產者將庫存加一,喚醒所有消費者,這時候就出現了兩個消費者同時去消費一個庫存,導致庫存變成負數,這就是虛假喚醒。

在object類的wait方法中,虛假喚醒是可能的,因此這個wait方法應該總被使用在循環中

解決方法

將代碼中的if換為while循環執行

 //進貨    public synchronized void get() {        while (product >= 1) {  //wait使用在循環中            System.out.println("產品已滿,無法添加");            try {                this.wait();            } catch (InterruptedException e) {            }        }        this.notifyAll();        System.out.println(Thread.currentThread().getName() + "店員進貨1個產品 庫存為" + ++product);    }    //賣貨    public synchronized void sale() {        while (product <= 0) {            System.out.println("產品缺貨,無法售賣");            try {                this.wait();            } catch (InterruptedException e) {            }        }        System.out.println(Thread.currentThread().getName() + "店員銷售1個產品 庫存為" + --product);        this.notifyAll();    }

控制線程通信Condition

Condition 接口描述了可能會與鎖有關聯的條件變量。這些變量在用法上與使用 Object.wait 訪問的隱式監視器類似,但提供了更強大的功能。需要特別指出的是,單個 Lock 可能與多個 Condition 對象關聯。為了避免兼容性問題, Condition 方法的名稱與對應的 Object 版本中的不同。

在 Condition 對象中,與 wait、 notify 和 notifyAll 方法對應的分別是await、 signal 和 signalAll。Condition 實例實質上被綁定到一個鎖上。要為特定 Lock 實例獲得Condition 實例,請使用其 newCondition()方法。

/** * 店員 */class ClerkLock {    //庫存共享數據 存在安全問題    private int product = 0;    //使用lock,去掉synchronized   this.wait和lock就是兩把鎖,用lock統一    private Lock lock = new ReentrantLock();    private Condition condition = lock.newCondition();    //進貨    public void get() {        lock.lock();        try {            while (product >= 1) {                System.out.println("產品已滿,無法添加");                try {                    condition.await();                } catch (InterruptedException e) {                }            }            condition.signalAll();            System.out.println(Thread.currentThread().getName() + "店員進貨1個產品 庫存為" + ++product);        } finally {            lock.unlock();        }    }    //賣貨    public synchronized void sale() {        lock.lock();        try {            while (product <= 0) {                System.out.println("產品缺貨,無法售賣");                try {                    condition.await();                } catch (InterruptedException e) {                }            }            System.out.println(Thread.currentThread().getName() + "店員銷售1個產品 庫存為" + --product);            condition.signalAll();        } finally {        }    }}

這里店員的代碼全部處理為condition,用他的方法實現線程的通信。

線程按序交替線程按序交替

編寫一個程序,開啟 3 個線程,這三個線程的 ID 分別為A、 B、 C,每個線程將自己的 ID 在屏幕上打印 10 遍,要求輸出的結果必須按順序顯示。

如:ABCABCABC…… 依次遞歸9-ReadWriteLock 讀寫鎖讀-寫鎖 ReadWriteLock

package com.atguigu.juc;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * @ClassName TestABCAlternate * @Description: 線程交替打印 * @Author: WangWenpeng * @Version 1.0 */public class TestABCAlternate {    public static void main(String[] args) {        Alternate alternate = new Alternate();        new Thread(new Runnable() {            @Override            public void run() {                for (int i = 0; i <= 20; i++) {                    alternate.loopA(i);                }            }        }, "A").start();        new Thread(new Runnable() {            @Override            public void run() {                for (int i = 0; i <= 20; i++) {                    alternate.loopB(i);                }            }        }, "B").start();        new Thread(new Runnable() {            @Override            public void run() {                for (int i = 0; i <= 20; i++) {                    alternate.loopC(i);                }            }        }, "C").start();    }}class Alternate {    private int number = 1;//當前正在執行的線程號    private Lock lock = new ReentrantLock();    private Condition condition1 = lock.newCondition();    private Condition condition2 = lock.newCondition();    private Condition condition3 = lock.newCondition();    public void loopA(int totalLoop) {        lock.lock();        try {            //1.判斷1號線程            if (number != 1) {                condition1.await();            }            //2.開始打印            for (int i = 0; i < 5; i++) {                System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);            }            //3.喚醒線程2            number = 2;            condition2.signal();        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }    public void loopB(int totalLoop) {        lock.lock();        try {            //1.判斷1號線程            if (number != 2) {                condition2.await();            }            //2.開始打印            for (int i = 0; i < 5; i++) {                System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);            }            //3.喚醒線程2            number = 3;            condition3.signal();        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }    public void loopC(int totalLoop) {        lock.lock();        try {            //1.判斷1號線程            if (number != 3) {                condition3.await();            }            //2.開始打印            for (int i = 0; i < 5; i++) {                System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop);            }            //3.喚醒線程2            number = 1;            condition1.signal();        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }}

ReadWriteLock 讀寫鎖

ReadWriteLock 維護了一對相關的鎖,一個用於只讀操作,另一個用於寫入操作。只要沒有 writer,讀取鎖可以由多個 reader 線程同時保持。寫入鎖是獨占的。

ReadWriteLock 讀取操作通常不會改變共享資源,但執行寫入操作時,必須獨占方式來獲取鎖。對於讀取操作占多數的數據結構。ReadWriteLock 能提供比獨占鎖更高的並發性。而對於只讀的數據結構,其中包含的不變性可以完全不需要考慮加鎖操作。

讀鎖是多個線程可以一起,寫鎖是獨占的

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @ClassName ReadWriteLock
 * @Description: 讀寫鎖    讀和寫之間不互斥  寫和寫之間互斥
 * @Author: WangWenpeng
 * @Version 1.0
 */
public class TestReadWriteLock {
    public static void main(String[] args) {
        ReadWriteLockDemo demo = new ReadWriteLockDemo();

        new Thread(new Runnable() {
            @Override
            public void run() {
                demo.set((int) (Math.random() * 101));
            }
        }, "writeLock").start();

        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.get();
                }
            }, "readLock-" + i).start();
        }
    }
}

class ReadWriteLockDemo {
    private int number = 0;
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    //讀
    public void get() {
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "讀:" + number);
        } finally {
            lock.readLock().unlock();
        }
    }

    //寫
    public void set(int number) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "寫:" + number);
            this.number = number;
        } finally {
            lock.writeLock().unlock();
        }
    }
}

線程八鎖

  • 一個對象里面如果有多個synchronized方法,某一個時刻內,只要一個線程去調用其中的一個synchronized方法了,其它的線程都只能等待,換句話說,某一個時刻內,只能有唯一一個線程去訪問這些synchronized方法
  • 鎖的是當前對象this,被鎖定后,其它的線程都不能進入到當前對象的其它的synchronized方法
  • 加個普通方法后發現和同步鎖無關
  • 換成兩個對象后,不是同一把鎖了,情況立刻變化。
  • 都換成靜態同步方法后,情況又變化
  • 所有的非靜態同步方法用的都是同一把鎖——實例對象本身,也就是說如果一個實例對象的非靜態同步方法獲取鎖后,該實例對象的其他非靜態同步方法必須等待獲取鎖的方法釋放鎖后才能獲取鎖,可是別的實例對象的非靜態同步方法因為跟該實例對象的非靜態同步方法用的是不同的鎖,所以毋須等待該實例對象已獲取鎖的非靜態同步方法釋放鎖就可以獲取他們自己的鎖。
  • 所有的靜態同步方法用的也是同一把鎖——類對象本身,這兩把鎖是兩個不同的對象,所以靜態同步方法與非靜態同步方法之間是不會有競態條件的。但是一旦一個靜態同步方法獲取鎖后,其他的靜態同步方法都必須等待該方法釋放鎖后才能獲取鎖,而不管是同一個實例對象的靜態同步方法之間,還是不同的實例對象的靜態同步方法之間,只要它們同一個類的實例對象!
/**
 * 1. 兩個普通同步方法,兩個線程,標准打印, 打印? //one  two
 */
public class TestThread8Monitor {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                number.getOne();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
    number.getTwo();
            }
        }).start();
    }
}
class Number {
    public synchronized void getOne() {
        System.out.println("one");
    }
    public synchronized void getTwo() {
        System.out.println("two");
    }
}
/**
2. 新增 Thread.sleep() 給 getOne() ,打印? //one  two
 */
public class TestThread8Monitor {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                number.getOne();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
    number.getTwo();
            }
        }).start();
    }
}
class Number {
    public synchronized void getOne() {
        try {
            Thread.sleep(3000);//讓one 睡3秒
        } catch (InterruptedException e) {
        }
        System.out.println("one");
    }
    public synchronized void getTwo() {
        System.out.println("two");
    }
}
/*
 *3. 新增普通方法 getThree() , 打印? //three  one   two
 */
public class TestThread8Monitor {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                number.getOne();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
    number.getTwo();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
    number.getThree();
            }
        }).start();
    }
}
class Number {
    public synchronized void getOne() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
        }
        System.out.println("one");
    }
    public synchronized void getTwo() {
        System.out.println("two");
    }
    //普通方法
    public void getThree(){
     System.out.println("three");
    }
}
/*
 * 4. 兩個普通同步方法,兩個 Number 對象,打印?  //two  one
 */
public class TestThread8Monitor {
    public static void main(String[] args) {
        Number number = new Number();
        Number number2 = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                number.getOne();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                number2.getTwo();
            }
        }).start();
    }
}

class Number {
    public synchronized void getOne() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
        }
        System.out.println("one");
    }
    public synchronized void getTwo() {
        System.out.println("two");
    }
}
/*
 * 5. 修改 getOne() 為靜態同步方法,打印?  //two   one
 */
public class TestThread8Monitor {
    public static void main(String[] args) {
        Number number = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                number.getOne();
            }//這樣其實不能通過類的實例訪問靜態,為演示這個問題
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                number.getTwo();
            }
        }).start();
    }
}
class Number {
    //靜態同步方法
    public static synchronized void getOne() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
        }
        System.out.println("one");
    }
    public synchronized void getTwo() {
        System.out.println("two");
    }
}
 /*
  * 6. 修改兩個方法均為靜態同步方法,一個 Number 對象?  //one   two
 */
public class TestThread8Monitor {

    public static void main(String[] args) {
        Number number = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                number.getOne();
            }//這樣其實不能通過類的實例訪問靜態,為演示這個問題
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                number.getTwo();
            }
        }).start();
    }
}
class Number {
    public static synchronized void getOne() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
        }
        System.out.println("one");
    }
    public static synchronized void getTwo() {
        System.out.println("two");
    }
}
 /*
 * 7. 一個靜態同步方法,一個非靜態同步方法,兩個 Number 對象?  //two  one
 */
public class TestThread8Monitor {

    public static void main(String[] args) {
        Number number = new Number();
        Number number2 = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                number.getOne();
            }//這樣其實不能通過類的實例訪問靜態,為演示這個問題
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                number2.getTwo();
            }
        }).start();
    }
}

class Number {
    public static synchronized void getOne() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
        }
        System.out.println("one");
    }
    public  synchronized void getTwo() {
        System.out.println("two");
    }
}
 /*
 * 8. 兩個靜態同步方法,兩個 Number 對象?   //one  two
 */
public class TestThread8Monitor {

    public static void main(String[] args) {
        Number number = new Number();
        Number number2 = new Number();
        new Thread(new Runnable() {
            @Override
            public void run() {
                number.getOne();
            }//這樣其實不能通過類的實例訪問靜態,為演示這個問題
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                number2.getTwo();
            }
        }).start();
    }
}
class Number {
    public static synchronized void getOne() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
        }
        System.out.println("one");
    }
    public static synchronized void getTwo() {
        System.out.println("two");
    }
}

線程八鎖的關鍵:

  • 非靜態方法的鎖默認為 this, 靜態方法的鎖為 對應的 Class 實例
  • 某一個時刻內,只能有一個線程持有鎖,無論幾個方法。

線程池

第四種獲取線程的方法:線程池,一個 ExecutorService,它使用可能的幾個池線程之一執行每個提交的任務,通常使用 Executors 工廠方法配置。

線程池可以解決兩個不同問題:由於減少了每個任務調用的開銷,它們通常可以在執行大量異步任務時提供增強的性能,並且還可以提供綁定和管理資源(包括執行任務集時使用的線程)的方法。每個 ThreadPoolExecutor 還維護着一些基本的統計數據,如完成的任務數。

為了便於跨大量上下文使用,此類提供了很多可調整的參數和擴展鈎子 (hook)。但是,強烈建議程序員使用較為方便的 Executors 工廠方法 :

  • Executors.newCachedThreadPool()(無界線程池,可以進行自動線程回收)
  • Executors.newFixedThreadPool(int)(固定大小線程池)
  • Executors.newSingleThreadExecutor()(單個后台線程)它們均為大多數使用場景預定義了設置。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

/**
 * @Description 一、線程池:提供了一個線程隊列,隊列中保存着所有等待狀態的線程。避免了創建與銷毀額外開銷,提高了響應的速度。
 * 二、線程池的體系結構:
 * java.util.concurrent.Executor : 負責線程的使用與調度的根接口
 *   |--**ExecutorService 子接口: 線程池的主要接口
 *    |--ThreadPoolExecutor 線程池的實現類
 *    |--ScheduledExecutorService 子接口:負責線程的調度
 *     |--ScheduledThreadPoolExecutor :繼承 ThreadPoolExecutor, 實現 ScheduledExecutorService
 * 三、工具類 : Executors
 * ExecutorService newFixedThreadPool() : 創建固定大小的線程池
 * ExecutorService newCachedThreadPool() : 緩存線程池,線程池的數量不固定,可以根據需求自動的更改數量。
 * ExecutorService newSingleThreadExecutor() : 創建單個線程池。線程池中只有一個線程
 * ScheduledExecutorService newScheduledThreadPool() : 創建固定大小的線程,可以延遲或定時的執行任務。
 * @Author WangWenpeng
 * @Param
 */
public class TestThreadPool {

    public static void main(String[] args) throws Exception {
        //1. 創建線程池
        ExecutorService pool = Executors.newFixedThreadPool(5);

        //submit Callable方法
        List<Future<Integer>> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Future<Integer> future = pool.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int sum = 0;
                    for (int i = 0; i <= 100; i++) {
                        sum += i;
                    }
                    return sum;
                }
            });
            list.add(future);
        }
        pool.shutdown();
        for (Future<Integer> future : list) {
            System.out.println(future.get());
        }

        //submit Runnable方法
        ThreadPoolDemo tpd = new ThreadPoolDemo();
        //2. 為線程池中的線程分配任務
        for (int i = 0; i < 10; i++) {
            pool.submit(tpd);
        }
        //3. 關閉線程池
        pool.shutdown();
    }
}

class ThreadPoolDemo implements Runnable {
    private int i = 0;

    @Override
    public void run() {
        while (i <= 100) {
            System.out.println(Thread.currentThread().getName() + " : " + i++);
        }
    }
}

線程調度ScheduledExecutorService

一個 ExecutorService,可安排在給定的延遲后運行或定期執行的命令。

public static void main(String[] args) throws Exception {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
        for (int i = 0; i < 5; i++) {
            Future<Integer> result = pool.schedule(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int num = new Random().nextInt(100);//生成隨機數
                    System.out.println(Thread.currentThread().getName() + " : " + num);
                    return num;
                }
            }, 1, TimeUnit.SECONDS);
            System.out.println(result.get());
        }
        pool.shutdown();
    }

ForkJoinPool 分支/合並框架

就是在必要的情況下,將一個大任務,進行拆分(fork)成若干個小任務(拆到不可再拆時),再將一個個的小任務運算的結果進行 join 匯總。

JoinFork/Join 框架與線程池的區別

采用 “工作竊取”模式(work-stealing):

  • 當執行新的任務時它可以將其拆分分成更小的任務執行,並將小任務加到線程隊列中,然后再從一個隨機線程的隊列中偷一個並把它放在自己的隊列中。
  • 相對於一般的線程池實現, fork/join框架的優勢體現在對其中包含的任務的處理方式上。在一般的線程池中, 如果一個線程正在執行的任務由於某些原因無法繼續運行, 那么該線程會處於等待狀態。而在fork/join框架實現中,如果某個子問題由於等待另外一個子問題的完成而無法繼續運行。那么處理該子問題的線程會主動尋找其他尚未運行的子問題來執行.這種方式減少了線程的等待時間, 提高了性能。
public class TestForkJoinPool {
    public static void main(String[] args) {
        Instant start = Instant.now();
        ForkJoinPool pool = new ForkJoinPool();
        ForkJoinTask<Long> task = new ForkJoinSumCalculate(0L, 50000000000L);
        Long sum = pool.invoke(task);
        System.out.println(sum);
        Instant end = Instant.now();
        System.out.println("耗費時間為:" + Duration.between(start, end).toMillis());//166-1996-10590
    }
}

class ForkJoinSumCalculate extends RecursiveTask<Long> {
    private static final long serialVersionUID = -259195479995561737L;

    private long start;
    private long end;

    private static final long THURSHOLD = 10000L;  //臨界值

    public ForkJoinSumCalculate(long start, long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        long length = end - start;
        if (length <= THURSHOLD) {
            long sum = 0L;
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
            long middle = (start + end) / 2;
            ForkJoinSumCalculate left = new ForkJoinSumCalculate(start, middle);
            left.fork(); //進行拆分,同時壓入線程隊列
            ForkJoinSumCalculate right = new ForkJoinSumCalculate(middle + 1, end);
            right.fork();
            return left.join() + right.join();
        }
    }
}

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2021最新版)

2.別在再滿屏的 if/ else 了,試試策略模式,真香!!

3.卧槽!Java 中的 xx ≠ null 是什么新語法?

4.Spring Boot 2.5 重磅發布,黑暗模式太炸了!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


免責聲明!

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



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