java併發編程基礎

来源:https://www.cnblogs.com/floor/archive/2020/05/17/12906861.html
-Advertisement-
Play Games

本文主要介紹線程的基本概念和意義、多線程程式開發需要註意的問題、創建線程的方式、線程同步、線程通信、線程的生命周期、原子類等java併發編程基礎內容 ...


內容簡介

本文比較長,主要介紹 線程的基本概念和意義、多線程程式開發需要註意的問題、創建線程的方式、線程同步、線程通信、線程的生命周期、原子類等內容。

這些內容基本都是來自《java併發編程藝術》一書,在此感謝,我是在微信讀書免費看的,所以算是白嫖了。部分源碼的解讀是筆者自己從jdk源碼扒下來的。


線程的定義與意義

線程的定義

  • 是輕量級的進程,線程的創建和切換成本比進程低
  • 同一進程中的多條線程將共用該進程中的全部系統資源,如虛擬地址空間,文件描述符和信號處理等等
  • 是操作系統能夠進行運算調度的最小單位
  • java程式至少有一個線程main,main線程由JVM創建

為什麼要有多線程

  • 可以充分利用多處理器核心
  • 更快的響應時間,可以將數據一致性要求不強的工作交給別的線程做
  • 更好的編程模型,例如可以使用生產者消費者模型進行解耦

併發編程需要註意的問題

上下文切換

cpu通過時間分片來執行任務,多個線程在cpu上爭搶時間片執行,線程切換需要保存一些狀態,再次切換回去需要恢復狀態,此為上下文切換成本。

因此並不是線程越多越快,頻繁的切換會損失性能

減少上下文切換的方法:

  • 無鎖併發編程:例如把一堆數據分為幾塊,交給不同線程執行,避免用鎖
  • 使用CAS:用自旋不用鎖可以減少線程競爭切換,但是可能會更加耗cpu
  • 使用最少的線程
  • 使用協程:在一個線程里執行多個任務

死鎖

死鎖就是線程之間因爭奪資源, 處理不當出現的相互等待現象

避免死鎖的方法:

  • 避免一個線程同時獲取多個鎖
  • 避免一個線程在鎖內同時占用多個資源,儘量保證每個鎖只占用一個資源
  • 嘗試使用定時鎖,lock.tryLock(timeout)
  • 對於資料庫鎖,加鎖和解鎖必須在一個資料庫連接里,否則會出現解鎖失敗的情況

資源限制

程式的執行需要資源,比如資料庫連接、帶寬,可能會由於資源的限制,多個線程並不是併發,而是串列,不僅無優勢,反而帶來不必要的上下文切換損耗

常見資源限制

  • 硬體資源限制
    • 帶寬
    • 磁碟讀寫速度
    • cpu處理速度
  • 軟體資源限制
    • 資料庫連接數
    • socket連接數

應對資源限制

  • 集群化,增加資源
  • 根據不同的資源限制調整程式的併發度,找到瓶頸,把瓶頸資源搞多一些,或者根據這個瓶頸調整線程數

創建線程的三種方式

廢話不說,直接上代碼

繼承Thread類

// 繼承Thread
class MyThread extends Thread {
    // 重寫run方法執行任務
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            // 可以通過this拿到當前線程
            System.out.println(this.getName()+"執行了"+i);
        }
    }
}

public class Demo_02_02_1_ThreadCreateWays {
    public static void main(String[] args) {
        // 先new出來,然後啟動
        MyThread myThread = new MyThread();
        myThread.start();
        for (int i = 0; i < 10; i++) {
            // 通過Thread的靜態方法拿到當前線程
            System.out.println(Thread.currentThread().getName()+"執行了"+i);
        }
    }
}

實現Runnable

// 實現Runnable介面
class MyThreadByRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            // 不能用this了
            System.out.println(Thread.currentThread().getName() + "執行了" + i);
        }
    }
}

public class Demo_02_02_1_ThreadCreateWays {
    public static void main(String[] args) {
        // 實現Runnable介面的方式啟動線程
        Thread thread = new Thread(new MyThreadByRunnable());
        thread.start();
        for (int i = 0; i < 10; i++) {
            // 通過Thread的靜態方法拿到當前線程
            System.out.println(Thread.currentThread().getName() + "執行了" + i);
        }
    }
}

因為Runnable是函數式介面,用lamba也可以

new Thread(() -> {
    System.out.println("Runnable是函數式介面, java8也可以使用lamba");
}).start();

使用Callable和Future

// 使用Callable
class MyThreadByCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"執行了"+i);
            sum+=i;
        }
        return sum;
    }
}
public class Demo_02_02_1_ThreadCreateWays {
    public static void main(String[] args) {
        // 用FutureTask包一層
        FutureTask<Integer> futureTask = new FutureTask<>(new MyThreadByCallable());
        new Thread(futureTask).start();
        try {
            // 調用futureTask的get能拿到返回的值
            System.out.println(futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

這是最複雜的一種方式,他可以有返回值,歸納一下步驟:

  1. 搞一個類實現Callable介面,重寫call方法,在call執行任務
  2. FutureTask包裝實現Callable介面類的實例
  3. FutureTask的實例作為Thread構造參數
  4. 調用FutureTask實例的get拿到返回值,調這一句會阻塞父線程

Callable也是函數式介面,所以也能用lamba

為啥Thread構造裡邊能放Runnable,也能放FutureTask? 其實FutureTask繼承RunnableFuture,而RunnableFuture繼承Runnable和Future,所以FutureTask也是Runnable

三種方式比較

方式 使用簡易程度 是否可以共用任務代碼 是否可以有返回值 是否可以聲明拋出異常 是否可以再繼承別的類
繼承Thread 簡單 不能 不能 不能 不能
Runnable 中等 可以 不能 不能 可以
Callable 複雜 可以 可以 可以 可以

繼承Thread是最容易的,但是也是最不靈活的

使用Callable時最複雜的,但是也是最靈活的

這裡說的共用任務代碼舉個例子:

還是上面那個MyThreadByRunnable

MyThreadByRunnable myThreadByRunnable = new MyThreadByRunnable();
Thread thread = new Thread(myThreadByRunnable);
thread.start();
// 再來一個,復用了任務代碼,繼承Thread就不行
Thread thread2 = new Thread(myThreadByRunnable);
thread2.start();

線程的一些屬性

名字

給以給線程取一個響亮的名字,便於排查問題,預設為Thread-${一個數字}這個樣子

  • 設置名字
threadA.setName("歡迎關註微信公號'大雄和你一起學編程'");
  • 獲取名字
threadA.getName();

是否是守護線程(daemon)

為其他線程服務的線程可以是守護線程,守護線程的特點是如果所有的前臺線程死亡,則守護線程自動死亡。

非守護線程創建的線程預設為非守護線程,守護線程創建的則預設為守護

  • set
threadA.setDaemon(true);
  • get
threadA.isDaemon();

線程優先順序(priority)

優先順序高的線程可以得到更多cpu資源, 級別是1-10,預設優先順序和創建他的父線程相同,main是5

set

threadA.setPriority(Thread.NORM_PRIORITY);

get

threadA.getPriority()

所屬線程組

可以把線程放到組裡,一起管理

設置線程組

Thread的構造裡邊可以指定

ThreadGroup threadGroup = new ThreadGroup("歡迎關註微信公號'大雄和你一起學編程'");
Thread thread = new Thread(threadGroup, () -> {
    System.out.println("歡迎關註微信公號'大雄和你一起學編程'");
});

拿到線程組

thread.getThreadGroup()

基於線程組的操作

ThreadGroup threadGroup1 = thread.getThreadGroup();
System.out.println(threadGroup1.activeCount()); // 有多少活的線程
threadGroup1.interrupt();                       // 中斷組裡所有線程
threadGroup1.setMaxPriority(10);                // 設置線程最高優先順序是多少

線程同步

多個線程訪問同一個資源可能會導致結果的不確定性,因此有時需要控制只有一個線程訪問共用資源,此為線程同步。

一個是可以使用synchronized同步,一個是可以使用Lock。synchronized是也是隱式的鎖。

同步方法

class Account {
    private Integer total;

    public Account(int total) {
        this.total = total;
    }

    public synchronized void draw(int money) {
        if (total >= money) {
            this.total = this.total - money;
            System.out.println(Thread.currentThread().getName() + "剩下" + this.total);
        } else {
            System.out.println(Thread.currentThread().getName() + "不夠了");
        }
    }

    public synchronized int getTotal() {
        return total;
    }
}

public class Demo_02_04_1_ThreadSync {
    public static void main(String[] args) {
        Account account = new Account(100);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (account.getTotal() >= 10) {
                    account.draw(10);
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread A = new Thread(runnable);
        A.setName("A");
        Thread B = new Thread(runnable);
        B.setName("B");
        A.start();
        B.start();
    }
}

假設AB兩個人從同一個賬戶里取錢,直接在draw這個方法加synchronized關鍵字,防止兩個人同時進入draw

sychronized加在普通方法上,鎖為當前實例對象

加在靜態方法上,鎖為當前類的Class

同步代碼塊

public  void draw(int money) {
    synchronized (total) {
        if (total >= money) {
            this.total = this.total - money;
            System.out.println(Thread.currentThread().getName() + "剩下" + this.total);
        } else {
            System.out.println(Thread.currentThread().getName() + "不夠了");
        }
    }
}

synchronized同步塊,鎖為()裡邊的對象

Lock lock = new ReentrantLock();
public void draw(int money) {
    lock.lock();
    try {
        if (total >= money) {
            this.total = this.total - money;
            System.out.println(Thread.currentThread().getName() + "剩下" + this.total);
        } else {
            System.out.println(Thread.currentThread().getName() + "不夠了");
        }
    } finally {
        lock.unlock();
    }
}

使用比較簡單,進方法加鎖,執行完釋放,後面會專門發一篇文章介紹鎖,包括AQS之類的東西,敬請關註。


線程間的通信

線程之間協調工作的方式

基於等待通知模型的通信

等待/通知的相關方法是任意Java對象都具備的,因為這些方法被定義在java.lang.Object上。

相關API

  • notify: 通知一個對象上等待的線程,使其從wait方法返回,而返回的前提是該線程獲取到了對象的鎖
  • notifyAll: 通知對象上所有等待的線程,使其從wait方法返回
  • wait: 使線程進入WAITING(後麵線程的生命周期裡邊有)狀態,只有等待另一個線程通知或者被中斷才返回,需要註意的是,調用wait方法後需要釋放對象的鎖
  • wait(long): 和wait類似,加入了超時時間,超時了還沒被通知就直接返回
  • wait(long, int): 納秒級,不常用

一些需要註意的點:

  • 使用wait()、notify()和notifyAll()時需要先對調用對象加鎖
  • 調用wait()方法後,線程狀態由RUNNING變為WAITING,並將當前線程放置到對象的等待隊列,釋放鎖
  • notify()或notifyAll()方法調用後,等待線程不會立即從wait()返回,需要調用notify()或notifAll()的線程釋放鎖之後,等待線程才有機會從wait()返回。
  • notify()方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll()方法則是將等待隊列中所有的線程全部移到同步隊列,被移動的線程狀態由WAITING變為BLOCKED。
  • 從wait()方法返回的前提是獲得了調用對象的鎖。

關於等待隊列和同步隊列

  • 同步隊列(鎖池):假設線程A已經擁有了某個對象(註意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),由於這些線程在進入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權,但是該對象的鎖目前正被線程A擁有,所以這些線程就進入了該對象的同步隊列(鎖池)中,這些線程狀態為Blocked
  • 等待隊列(等待池):假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖(因為wait()方法必須出現在synchronized中,這樣自然在執行wait()方法之前線程A就已經擁有了該對象的鎖),同時 線程A就進入到了該對象的等待隊列(等待池)中,此時線程A狀態為Waiting。如果另外的一個線程調用了相同對象的notifyAll()方法,那麼 處於該對象的等待池中的線程就會全部進入該對象的同步隊列(鎖池)中,準備爭奪鎖的擁有權。如果另外的一個線程調用了相同對象的notify()方法,那麼 僅僅有一個處於該對象的等待池中的線程(隨機)會進入該對象的同步隊列(鎖池)。

以上來自啃碎併發(二):Java線程的生命周期

等待通知模型的示例

class WaitNotifyModel {
    Object lock = new Object();
    boolean flag = false;

    public void start() {
        Thread A = new Thread(() -> {
            synchronized (lock) {
                while (!flag) {
                    try {
                        System.out.println(Thread.currentThread().getName()+":等待通知");
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName()+ ":收到通知,處理業務邏輯");
            }
        });
        A.setName("我是等待者");
        Thread B = new Thread(() -> {
            synchronized (lock) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                flag = true;
                System.out.println(Thread.currentThread().getName()+":發出通知");
                lock.notify();
            }
        });
        B.setName("通知者");
        A.start();
        B.start();
    }
}

模型歸納

等待者

 synchronized (對象) {
    while (不滿足條件) {
        對象.wait()
    }
    處理業務邏輯
}

通知者

synchronized (對象) {
    改變條件
    對象.notify();
}

基於Condition的通信

上述的這種等待通知需要使用synchronized, 如果使用Lock的話就要用Condition

Condition介面也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式

Condition與Object監視器的區別

項目 Object的監視器方法 Condition
前置條件 獲得對象的鎖 Lock.lock()獲取鎖
Lock.newCondition()獲取Condition
調用方式 obj.wait() condition.await()
等待隊列個數 一個 可以多個
當前線程釋放鎖併進入等待狀態 支持 支持
等待狀態中不響應中斷 不支持 支持
釋放鎖進入超時等待狀態 支持 支持
進入等待狀態到將來的某個時間 不支持 支持
喚醒等待中的一個或多個線程 支持 notify notifyAll 支持signal signalAll

這裡有一些線程的狀態,可以看完後邊的線程的生命周期再回過頭看看

示例

一般都會將Condition對象作為成員變數。當調用await()方法後,當前線程會釋放鎖併在此等待,而其他線程調用Condition對象的signal()方法,通知當前線程後,當前線程才從await()方法返回,並且在返回前已經獲取了鎖。

實現一個有界隊列,當隊列為空時阻塞消費線程,當隊列滿時阻塞生產線程

class BoundList<T> {
    private LinkedList<T> list;
    private int size;
    private Lock lock = new ReentrantLock();
    // 拿兩個condition,一個是非空,一個是不滿
    private Condition notEmpty = lock.newCondition();
    private Condition notFullCondition = lock.newCondition();

    public BoundList(int size) {
        this.size = size;
        list = new LinkedList<>();
    }

    public void push(T x) throws InterruptedException {
        lock.lock();
        try {
            while (list.size() >= size) {
                // 滿了就等待
                notFullCondition.await();
            }
            list.push(x);
            // 喚醒等待的消費者
            notEmpty.signalAll();
            
        } finally {
            lock.unlock();
        }
    }

    public T get() throws InterruptedException {
        lock.lock();
        try {
            while (list.isEmpty()) {
                // 空了就等
                notEmpty.await();
            }
            T x = list.poll();
            // 喚醒生產者
            notFullCondition.signalAll();
            return x;
        } finally {
            lock.unlock();
        }
    }

}

public class Demo_02_05_1_Condition {
    public static void main(String[] args) {
        BoundList<Integer> list = new BoundList<>(10);
        // 生產數據的線程
        new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                try {
                    Thread.sleep(1000);
                    list.push(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        
        // 消費數據的線程
        new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                try {
                    System.out.println(list.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

基於BlockingQueue實現線程通信

後面會專門發文介紹BlockingQueue, 敬請關註


控制線程

參考了《瘋狂java講義》的提法,將如下內容歸為控制線程的方式。

join

主線程join一個線程,那麼主線程會阻塞直到join進來的線程執行完,主線程繼續執行, join如果帶超時時間的話,那麼如果超時的話主線程也會不再等join進去的線程而繼續執行.

join實際就是判斷join進來的線程存活狀態,如果活著就調用wait(0),如果帶超時時間了的話,wait裡邊的時間會算出來

while (isAlive()) {
    wait(0);
}

API

  • public final void join() throws InterruptedException
  • public final synchronized void join(long millis, int nanos)
  • public final synchronized void join(long millis)

例子

public class Demo_02_06_1_join extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(this.getName() + "  " + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo_02_06_1_join joinThread = new Demo_02_06_1_join();
        for (int i = 0; i < 100; i++) {

            if (i == 10) {
                joinThread.start();
                joinThread.join();
            }
            // 打到9就停了,然後執行joinThread這裡邊的代碼,完事繼續從10打
            System.out.println(Thread.currentThread().getName()+"  "+i);
        }
    }
}

sleep

睡覺方法,使得線程暫停一段時間,進入阻塞狀態。

API

  • public static native void sleep(long millis) throws InterruptedException
  • public static void sleep(long millis, int nanos) throws InterruptedException

示例

public class Demo_02_06_2_sleep extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i == 5) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
            // 輸出到4停止, 5秒後繼續
            System.out.println(this.getName() + "  " + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo_02_06_2_sleep sleepThread = new Demo_02_06_2_sleep();
        sleepThread.start();
    }
}

yield

也是讓線程暫停一下,但是是進入就緒狀態,讓系統重新開始一次新的調度過程,下一次可能運氣好被yield的線程又被選中。

Thread.yield()

中斷

Java中斷機制是一種協作機制,也就是說通過中斷並不能直接終止另一個線程,而需要被中斷的線程自己處理中斷。

前面有一些方法聲明瞭InterruptedException, 這意味者他們可以被中斷,中斷後把異常拋給調用方,讓調用方自己處理.

被中斷的線程可以自已處理中斷,也可以不處理或者拋出去。

public class Demo_02_06_3_interrupt extends Thread {

    static class MyCallable implements Callable {
        @Override
        public Integer call() throws InterruptedException {
            for (int i = 0; i < 5000; i++) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("3333");
                    throw new InterruptedException("中斷我幹嘛,關註 微信號 大雄和你一起學編程 呀");
                }
            }
            return 0;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();
        for (int i = 0; i < 100; i++) {
            if (i == 3) {
                thread.interrupt();
            }
        }
        try {
            futureTask.get();
        } catch (ExecutionException e) {
            // 這裡會捕獲到異常
            e.printStackTrace();
        }

    }
}

線程的生命周期

啃碎併發(二):Java線程的生命周期 這篇文章寫的非常好,建議看一下。

要是早點發現這篇文章的話,大雄也不用費勁在《java併發編程藝術》和《瘋狂java講義》以及各種博客找資料了。

這裡我只想把這篇文章里一個圖改一下貼到這裡,細節部分大家可以參考上述這篇文章。

還是先說兩嘴,這個生命周期的圖我找到了不少版本,不僅圖的形式不一樣,裡邊的內容也有些出入

  • 《瘋狂java講義》裡邊只有5中狀態,缺少WAITING和TIMED_WAITING
  • 《java併發編程藝術》裡邊有7中狀態
  • 上邊的那篇文章,文字描述有7中狀態,但是圖裡邊只有6種

大雄也懵了,遂在源碼找到瞭如下一個枚舉, 裡面有一些註釋,翻譯了一下。

 public enum State {
        // 表示沒有開始的線程
        NEW,

        // 表示可運行(大家的翻譯應該是就緒)的線程
        // 表示在JVM正在運行,但是他可能需要等操作系統分配資源
        // 比如CPU
        RUNNABLE,

         // 表示線程在等待監視器鎖
         // 表示正在等待監視器鎖以便重新進進入同步塊或者同步方法 
         // OR 在調用了Object.wait重新進入同步塊或者同步方法
        BLOCKED,

         // 調用如下方法之一會進入WAITING
         // 1. Object.wait() 沒有加超時參數
         // 2. 調用join() 沒有加超時參數
         // 3. 調用LockSupport.park()
         // WAITING狀態的線程在等待別的線程做一個特殊的事情(action)例如
         // 1. 調用了wait的在等待其他線程調用notify或者notifyAll
         // 2. 調用了join的在等待指定線程結束
        WAITING,

         // 就是有一個特定等待時間的線程
         // 加上一個特定的正的超時時間調用如下方法會進入此狀態
         // 1. Thread.sleep
         // 2. Thread.join(long)
         // 3. LockSupport.parkNanos
         // 4. LockSupport.parkUntil
        TIMED_WAITING,

        // 執行完了結束的狀態
        TERMINATED;
    }

對於一個擁有8級英語水品的6級沒過的人來說,這段翻譯太難了,但是翻譯出來感覺很清晰了。

應該是 7種狀態!!!

大雄不去具體研究狀態的流轉了,直接參考一些資料及上述翻譯,搞一個前無古人、後有來者的線程生命周期圖

線程的生命周期

這個圖八成、沒準、大概是沒有太大問題的。此圖中,原諒色是線程狀態,紫色是引起狀態變化的原因。


ThereadLocal

就是綁定到線程上邊的一個存東西的地方。

使用示例

class Profiler {
    // ThreadLocal的創建
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<Long>(){
        @Override
        protected Long initialValue() {
            return System.currentTimeMillis();
        }

    };

    // 記錄開始時間
    public static void begin() {
        threadLocal.set(System.currentTimeMillis());
    }

    // 記錄耗時
    public static Long end() {
        return System.currentTimeMillis() - threadLocal.get();
    }
}
public class Demo_02_08_1_ThreadLocal {
    public static void main(String[] args) {
        new Thread(() -> {
            Profiler.begin();
            long sum = 1;
            for (int i = 1; i < 20; i++) {
                sum*=i;
            }
            System.out.println(sum);
            System.out.println(Thread.currentThread().getName()+"耗時="+Profiler.end());
        }).start();

        new Thread(() -> {
            Profiler.begin();
            int sum = 1;
            for (int i = 1; i < 1000; i++) {
                sum+=i;
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(sum);
            System.out.println(Thread.currentThread().getName()+"耗時="+Profiler.end());
        }).start();
    }
}

InheritableThreadLocal

這種ThreadLocal可以從父線程傳到子線程,也就是子線程能訪問父線程中的InheritableThreadLocal

public class Demo_02_08_2_ThreadLocalInherit {
    static class TestThreadLocalInherit extends Thread{
        @Override
        public void run() {
            System.out.println(threadLocal.get()); // null 
            System.out.println(inheritableThreadLocal.get()); // 歡迎關註微信公眾號 大雄和你一起學編程
        }
    }

    public static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>();
    public static InheritableThreadLocal<Object> inheritableThreadLocal = new InheritableThreadLocal<>();
    public static void main(String[] args) {
        inheritableThreadLocal.set("歡迎關註微信公眾號 大雄和你一起學編程");
        threadLocal.set("ddd");
        new TestThreadLocalInherit().start();
    }
}

實現原理

很容易想到,因為這個東西是跟著線程走的,所以應該是線程的一個屬性,事實上也是這樣,ThreadLocal和InheritableThreadLocal都是存儲在Thread裡面的。

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

/*
 * InheritableThreadLocal values pertaining to this thread. This map is
 * maintained by the InheritableThreadLocal class.
 */
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

上邊這個就是Thread的兩個成員變數,其實兩個是一樣的類型。

ThreadLocalMap是ThreadLocal的內部類,他裡邊是一個用一個Entry數組來存數據的。set時將ThreadLocal作為key,要存的值傳進去,他會對key做一個hash,構建Entry,放到Entry數組裡邊。

// 偽碼
static class ThreadLocalMap {
    // 內部的Entry結構
    static class Entry {...}
    // 存數據的
    private Entry[] table;
    // set
    private void set(ThreadLocal<?> key, Object value) {
        int i = key.threadLocalHashCode & (len-1);
        tab[i] = new Entry(key, value);
    }
    // get
    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);
    }
}

再來看看ThreadLocal的get方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); // 這個就是拿到的存在Thread的threadLocals這個變數
    if (map != null) {
        // 這裡就是毫無難度的事情了
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 這個也很簡單,他會調你重寫的initialValue方法,拿到一個值,set進去並且返回給你
    // 這個也很有趣,一般init在初始化完成,但是他是在你取的時候去調,應該算是一個小小優化吧
    return setInitialValue();
}

再來看看ThreadLocal的set, 超級簡單,不多說

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocal看完了,再來瞅瞅InheritableThreadLocals,看看他是怎麼可以從父線程那裡拿東西的

// 繼承了ThreadLocal, 重寫了三個方法
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    // 這個方法在ThreadLocal是直接拋出一個異常UnsupportedOperationException
    protected T childValue(T parentValue) {
        return parentValue;
    }
    // 超簡單,我們的Map不要threadLocals了,改為inheritableThreadLocals
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    // 同上
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

發現他和ThreadLocal長得差不多,就是重寫了三個方法,由此看來關鍵在inheritableThreadLocals是如何傳遞的

直接在Thread裡面搜inheritableThreadLocals

你會發現他是在init方法中賦值的,而init實在Thread的構造方法中調用的

// 這個parent就是 創建這個線程的那個線程,也就是父線程
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

看來現在得看看ThreadLocal.createInheritedMap這個方法了

// parentMap就是父線程的inheritableThreadLocals
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
// 發現很簡單,就是把父線程的東西到自己線程的inheritableThreadLocals裡邊
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

總結一下

ThreadLocal和InheritableThreadLocal是基於在Thread裡邊的兩個變數實現的,這兩個變數類似於一個HashMap的結構ThreadLocalMap,裡邊的Entry key為ThreadLocal, value為你存的值. InheritableThreadLocal的實現主要是線上程創建的時候,如果父線程有inheritableThreadLocal, 會被拷貝到子線程。


原子類

一個簡單的i++操作, 多線程環境下如果i是共用的,這個操作就不是原子的。

為此,java.util.concurrent.atomic這個包下邊提供了一些原子類,這些原子操作類提供了一種用法簡單、性能高效、線程安全地更新一個變數的方式。

atomic包下的類

一個使用的例子

public class Demo_04_01_1_Atomic {
    static class Counter {
        private AtomicInteger atomicInteger = new AtomicInteger(0);
        public int increment() {
            return atomicInteger.getAndIncrement();
        }
        public int get() {
            return atomicInteger.get();
        }
    }
    static class Counter2 {
        private int value = 0;
        public int increment() {
            return value++;
        }
        public int get() {
            return value;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 這個用了原子類
        Counter counter = new Counter();
        // 這個沒有用原子類
        Counter2 counter2 = new Counter2();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    counter.increment();
                    counter2.increment();
                }
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(counter.get());  // 一定是5000
        System.out.println(counter2.get()); // 可能少於5000
    }
}

超級簡單~

原子類的實現沒細看,貌似是CAS吧


章小結

併發編程基礎-總結

本圖源文件可以在github java-concurrent-programming-art-mini對應章下麵找到

參考文獻

相關資源

本文是筆者閱讀《java併發編程藝術》一書的筆記中的一部分,筆者將所有筆記已經整理成了一本gitbook電子書(還在完善中),閱讀體驗可能會好一些。若有需要可關註微信公眾號大雄和你一起學編程併在後臺回覆我愛java領取。

大雄和你一起學編程


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 自己用C語言實現的推箱子的游戲,在寫這個的期間瀏覽,查看了許多的博客和論壇。(寫於大一下學期) 這個游戲我用的是VS2010和EasyX圖形庫寫的。 如有錯誤,望指正。 代碼在最後。 游戲的效果圖 游戲界面 通關界面 這個3.0是因為,有過2次大的修改。 還有這個時間的數字是不動的,這裡不太懂怎麼弄 ...
  • 1 問題 對 排序,只要在 後面加欄位就可以了,可以通過加 或`asc`來選擇降序或升序。但排序規則是預設的,數字、時間、字元串等都有自己預設的排序規則。有時候需要按自己的想法來排序,而不是按欄位預設排序規則。 比如欄位值為英文欄位: 、`Tuesday Wednesday`等,如果按欄位預設排序規 ...
  • 項目簡介 項目來源於: "https://gitee.com/zzdoreen/SSMS" 本系統基於 JSP+Servlet+Mysql 一個基於JSP+Servlet+Jdbc的學生成績管理系統。涉及技術少,易於理解,適合 JavaWeb初學者 學習使用。 難度等級:入門 技術棧 編輯器 Ecl ...
  • #! python3import random ''' 目標:製作N份選項無序的試卷 步驟:1.創建文件(試卷文件和對應答案文件) 2.寫入題頭 3.寫入題目和選項 4.關閉文件 重點:1.無序選項如何實現 已有數據是字典形式,key是題目內容,對應的value是正確答案, 選項都是value,所以 ...
  • scala基礎 安裝scala(不推薦使用最新版本,2.11.x夠用了) "scala官網" "2.11.12版本下載頁面" 這裡我選擇2.11.12版本,在下載頁面往下拉,選擇scala 2.11.12.msi(windows用戶),msi安裝比較簡單,一直點點就行。如果下載速度慢,建議用迅雷。 ...
  • 今天學習了一下集合類的知識,練習Set的時候發現 1*Set集合允許重覆的值插入,但是重覆的值會被覆蓋; 2*List集合允許重覆的值插入,但是重覆的值會不會被覆蓋; import java.util.*; public class Test14702 { public static void ma ...
  • 你在山上看風景,看風景的人在山上看你。明月裝飾了你的窗子,你裝飾了別人的夢。 裝飾器模式(Decorator Pattern),別名又叫包裝者模式(wapper),允許向一個現有的對象添加新的功能,同時又不改變其結構。這種類型的設計模式屬於結構型模式,它是作為現有的類的一個包裝,不同於代理。 這種模 ...
  • 沒想到吧,我硬著頭皮來給你們更Python了,先上代碼 0 為了不讓你們作弊,我只截了圖,代碼多了一點,我也是手酸(無奈)╮(╯▽╰)╭ 。 我下周六更 bye~ ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...