主頁 > 後端開發 > java并發編程基礎

java并發編程基礎

2020-10-26 06:15:29 後端開發

內容簡介

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

這些內容基本都是來自《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 = https://www.cnblogs.com/floor/p/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 = https://www.cnblogs.com/floor/p/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筆記(看起來舒服,無法打開則請訪問下邊的地址)
  • gitbook筆記(沒有上一個好看)
  • gitbook筆記github地址

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

大雄和你一起學編程

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/191476.html

標籤:Java

上一篇:Spring Boot發送郵件

下一篇:pyjsonrpc的使用

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more