首頁 > Java > java教程 > 歸納整理Java並發知識點

歸納整理Java並發知識點

WBOY
發布: 2022-03-25 19:16:26
轉載
3252 人瀏覽過

本篇文章為大家帶來了關於java的相關知識,其中主要介紹了java並發的相關問題,總結了一些問題,大家來看一下會多少,希望對大家有幫助。

歸納整理Java並發知識點

推薦學習:《java教學

1.並行跟並發有什麼不同?

從作業系統的角度來看,執行緒是CPU分配的最小單位。

  • 並行就是同一時刻,兩個執行緒都在執行。這就要求有兩個CPU去分別執行兩個執行緒。
  • 並發就是同一時刻,只有一個執行,但是一個時間段內,兩個執行緒都執行了。並發的實作依賴CPU切換線程,因為切換的時間特別短,所以基本上對使用者來說是無感知的。

歸納整理Java並發知識點

就好像我們去食堂打飯,並行就是我們在多個窗口排隊,幾個阿姨同時打菜;並發就是我們擠在一個窗口,阿姨給這個打一匙,又手忙腳亂地給那個打一匙。

歸納整理Java並發知識點

2.說說什麼是行程和執行緒?

要說線程,必須先說說進程。

  • 進程:進程是程式碼在資料集合上的一次運行活動,是系統進行資源分配和調度的基本單位。
  • 線程:線程是進程的一個執行路徑,一個行程中至少有一個線程,進程中的多個線程共享進程的資源。

作業系統在分配資源時是把資源分配給進程的, 但是CPU 資源比較特殊,它是被分配到線程的,因為真正要佔用CPU運行的是線程,所以也說線程是CPU分配的基本單位。

例如在Java中,當我們啟動 main 函數其實就啟動了一個JVM進程,而 main 函數在的線程就是這個進程中的一個線程,也稱為主線程。

歸納整理Java並發知識點

一個行程中有多個線程,多個執行緒共用行程的堆疊和方法區資源,但是每個執行緒有自己的程式計數器和堆疊。

3.說說執行緒有幾種創建方式?

Java中建立執行緒主要有三種方式,分別為繼承Thread類別、實作Runnable介面、實作Callable介面。

歸納整理Java並發知識點

  • 繼承Thread類,重寫run()方法,呼叫start()方法啟動執行緒
public class ThreadTest {

    /**
     * 继承Thread类
     */
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("This is child thread");
        }
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }}
登入後複製
  • 實現Runnable 接口,重寫run()方法
public class RunnableTask implements Runnable {
    public void run() {
        System.out.println("Runnable!");
    }

    public static void main(String[] args) {
        RunnableTask task = new RunnableTask();
        new Thread(task).start();
    }}
登入後複製

上面兩個都是沒有回傳值的,但是如果我們需要取得執行緒的執行結果,該怎麼辦呢?

  • 實作Callable接口,重寫call()方法,這種方式可以透過FutureTask取得任務執行的回傳值
public class CallerTask implements Callable<string> {
    public String call() throws Exception {
        return "Hello,i am running!";
    }

    public static void main(String[] args) {
        //创建异步任务
        FutureTask<string> task=new FutureTask<string>(new CallerTask());
        //启动线程
        new Thread(task).start();
        try {
            //等待执行完成,并获取返回结果
            String result=task.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }}</string></string></string>
登入後複製

4.為什麼呼叫start()方法時會執行run()方法,那怎麼不直接呼叫run()方法?

JVM執行歸納整理Java並發知識點,會先建立一條線程,由創建出來的新線程去執行thread的run方法,這才起到多線程的效果。

歸納整理Java並發知識點

**為什麼我們不能直接呼叫run()方法? **也很清楚, 如果直接呼叫Thread的run()方法,那麼run方法還是運行在主執行緒中,相當於順序執行,就起不到多執行緒的效果。

5.執行緒有哪些常用的調度方法?

歸納整理Java並發知識點

執行緒等待與通知

在Object類別中有一些函數可以用於執行緒的等待與通知。

  • wait():當一個執行緒A呼叫一個共享變數的wait()方法時, 執行緒A會被阻塞掛起, 發生下面幾種情況才會回傳:

    • (1) 線程A呼叫了共享物件notify()或notifyAll()方法;

    • (2)其他線程呼叫了線程A的interrupt() 方法,執行緒A拋出InterruptedException異常回傳。

  • wait(long timeout) :這個方法相比wait() 方法多了一個超時參數,它的不同之處在於,如果線程A呼叫共享物件的wait(long timeout)方法後,沒有在指定的timeout ms時間內被其它執行緒喚醒,那麼這個方法還是會因為逾時而回傳。

  • wait(long timeout, int nanos),其內部呼叫的是 wait(long timout)函數。

上面是執行緒等待的方法,而喚醒執行緒主要是下面兩個方法:

  • notify() : 一個執行緒A呼叫共享物件的 notify() 方法後,會喚醒一個在這個共享變數上呼叫 wait 系列方法後被掛起的執行緒。一個共享變數上可能會有多個執行緒在等待,具體喚醒哪個等待的執行緒是隨機的。
  • notifyAll() :不同於在共享變數上呼叫notify() 函數會喚醒被阻塞到該共享變數上的一個線程,notifyAll()方法則會喚醒所有在該共享變數上由於呼叫wait系列方法而被掛起的線程。

Thread類別也提供了一個方法用於等待的方法:

  • #join():如果一個執行緒A執行了thread.join()語句,其意義是:當前執行緒A等待thread執行緒​​終止之後才

    從thread.join()返回。

線程休眠

  • sleep(long millis) :Thread類別中的靜態方法,當一個執行中的線程A調用了Thread 的sleep方法後,執行緒A會暫時讓出指定時間的執行權,但是執行緒A所擁有的監視器資源,例如鎖定還是持有不讓出的。指定的睡眠時間到了後函數會正常返回,接著參與 CPU 的調度,取得到 CPU 資源後就可以繼續運作。

讓出優先權

  • yield() :Thread類別中的靜態方法,當一個執行緒呼叫yield 方法時,實際上就是在暗示執行緒調度器當前執行緒請求讓出自己的CPU ,但是執行緒調度器可以無條件忽略這個暗示。

執行緒中斷

Java 中的執行緒中斷是一種執行緒間的協作模式,透過設定執行緒的中斷標誌並不能直接終止該執行緒的執行,而是被中斷的執行緒根據中斷狀態自行處理。

  • void interrupt() :中斷線程,例如,當線程A運行時,線程B可以呼叫錢程interrupt() 方法來設定線程的中斷標誌為true 並立即返回。設定標誌只是設定標誌, 線程A實際上並沒有被中斷, 會繼續往下執行。
  • boolean isInterrupted() 方法: 偵測目前執行緒是否已中斷。
  • boolean interrupted() 方法: 偵測目前執行緒是否已中斷,與 isInterrupted 不同的是,該方法如果發現目前執行緒被中斷,則會清除中斷標誌。

6.線程有幾種狀態?

在Java中,執行緒共有六種狀態:

NEW初始狀態:執行緒被創建,但還沒有呼叫start()方法RUNNABLE運行狀態:Java執行緒將作業系統中的就緒和執行兩種狀態籠統的稱為「運行」#BLOCKED阻塞狀態:表示執行緒阻塞於鎖定WAITING等待狀態:表示執行緒進入等待狀態,進入該狀態表示目前執行緒需要等待其他執行緒做出一些特定動作(通知或中斷)
#說明
# ########TIME_WAITING######逾時等待狀態:此狀態不同於WAITIND,它是可以在指定的時間自行回傳的############TERMINATED## ####終止狀態:表示目前執行緒已經執行完畢############

執行緒在自身的生命週期中, 並不是固定地處於某個狀態,而是隨著程式碼的執行在不同的狀態之間進行切換,Java執行緒狀態變化如圖示:

歸納整理Java並發知識點

7.什麼是執行緒上下文切換?

使用多執行緒的目的是為了充分利用CPU,但我們知道,並發其實是一個CPU來應付多個執行緒。

歸納整理Java並發知識點

為了讓使用者感覺多個執行緒是在同時執行的, CPU 資源的分配採用了時間片輪轉也就是給每個執行緒分配一個時間片,執行緒在時間片內佔用CPU 執行任務。當執行緒使用完時間片後,就會處於就緒狀態並讓出 CPU 讓其他執行緒佔用,這就是上下文切換。

歸納整理Java並發知識點

8.守護線程了解嗎?

Java中的執行緒分為兩類,分別為 daemon 執行緒(守護執行緒)和 user 執行緒(使用者執行緒)。

在JVM 啟動時會呼叫 main 函數,main函數所在的錢程就是一個使用者執行緒。其實在 JVM 內部同時也啟動了許多守護線程,像是垃圾回收線程。

那麼守護線程和使用者線程有什麼差別呢?其中一個差異是當最後一個非守護線程束時, JVM會正常退出,而不管當前是否存在守護線程,也就是說守護線程是否結束並不會影響 JVM退出。換而言之,只要有一個使用者執行緒還沒結束,正常情況下JVM就不會退出。

9.線程間有哪些通訊方式?

歸納整理Java並發知識點

  • volatile和synchronized關鍵字

關鍵字volatile可以用來修飾欄位(成員變數) ,就是告知程式任何對該變數的存取均需要從共享記憶體中獲取,而對它的改變必須同步刷新回共享內存,它能保證所有執行緒對變數存取的可見性。

關鍵字synchronized可以修飾方法或以同步區塊的形式來進行使用,它主要確保多個執行緒在同一個時刻,只能有一個執行緒處於方法或同步區塊中,它保證了執行緒對變數存取的可見性和排他性。

  • 等待/通知機制

可以透過Java內建的等待/通知機制(wait()/notify())實作一個執行緒修改一個物件的值,而另一個執行緒感知到了變化,然後進行相應的操作。

  • 管道輸入/輸出流

管道輸入/輸出流和普通的檔案輸入/輸出流或網路輸入/輸出流不同之處在於,它主要用於線程之間的資料傳輸,而傳輸的媒介為記憶體。

管道輸入/輸出流主要包括如下4種具體實作:PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前兩種面向字節,而後兩種面向字元。

  • 使用Thread.join()

#如果一個執行緒A執行了thread.join()語句,其意義是:目前執行緒A等待thread執行緒​​終止之後才從thread.join()返回。 。執行緒Thread除了提供join()方法之外,還提供了join(long millis)和join(long millis,int nanos)兩個具備超時特性的方法。

  • 使用ThreadLocal

ThreadLocal,即執行緒變數,是一個以ThreadLocal物件為鍵、任意物件為值的儲存結構。這個結構被附帶在線程上,也就是說一個線程可以根據一個ThreadLocal物件查詢到綁定在這個線程上的一個值。

可以透過set(T)方法來設定一個值,在目前執行緒下再透過get()方法取得到原先設定的值。

關於多線程,其實很大機率還會出一些筆試題,例如交替列印、銀行轉帳、生產消費模型等等,後面老三會單獨出一期來盤點一下常見的多執行緒筆試題。

ThreadLocal

ThreadLocal其實應用場景不是很多,但卻是被炸了千百遍的面試老油條,涉及到多線程、資料結構、JVM,可問的點比較多,一定要拿下。

10.ThreadLocal是什麼?

ThreadLocal,也就是執行緒本地變數。如果你創建了一個ThreadLocal變量,那麼訪問這個變量的每個線程都會有這個變量的一個本地拷貝,多個線程操作這個變量的時候,實際上是操作自己本地內存裡面的變量,從而起到線程隔離的作用,避免了線程安全問題。

歸納整理Java並發知識點

  • 创建

创建了一个ThreadLoca变量localVariable,任何一个线程都能并发访问localVariable。

//创建一个ThreadLocal变量public static ThreadLocal<string> localVariable = new ThreadLocal();</string>
登入後複製
  • 写入

线程可以在任何地方使用localVariable,写入变量。

localVariable.set("鄙人三某”);
登入後複製
  • 读取

线程在任何地方读取的都是它写入的变量。

localVariable.get();
登入後複製

11.你在工作中用到过ThreadLocal吗?

有用到过的,用来做用户信息上下文的存储。

我们的系统应用是一个典型的MVC架构,登录后的用户每次访问接口,都会在请求头中携带一个token,在控制层可以根据这个token,解析出用户的基本信息。那么问题来了,假如在服务层和持久层都要用到用户信息,比如rpc调用、更新用户获取等等,那应该怎么办呢?

一种办法是显式定义用户相关的参数,比如账号、用户名……这样一来,我们可能需要大面积地修改代码,多少有点瓜皮,那该怎么办呢?

这时候我们就可以用到ThreadLocal,在控制层拦截请求把用户信息存入ThreadLocal,这样我们在任何一个地方,都可以取出ThreadLocal中存的用户数据。

歸納整理Java並發知識點

很多其它场景的cookie、session等等数据隔离也都可以通过ThreadLocal去实现。

我们常用的数据库连接池也用到了ThreadLocal:

  • 数据库连接池的连接交给ThreadLoca进行管理,保证当前线程的操作都是同一个Connnection。

12.ThreadLocal怎么实现的呢?

我们看一下ThreadLocal的set(T)方法,发现先获取到当前线程,再获取ThreadLocalMap,然后把元素存到这个map中。

    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //讲当前元素存入map
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
登入後複製

ThreadLocal实现的秘密都在这个ThreadLocalMap了,可以Thread类中定义了一个类型为ThreadLocal.ThreadLocalMap的成员变量threadLocals

public class Thread implements Runnable {
   //ThreadLocal.ThreadLocalMap是Thread的属性
   ThreadLocal.ThreadLocalMap threadLocals = null;}
登入後複製

ThreadLocalMap既然被称为Map,那么毫无疑问它是型的数据结构。我们都知道map的本质是一个个形式的节点组成的数组,那ThreadLocalMap的节点是什么样的呢?

        static class Entry extends WeakReference<threadlocal>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            //节点类
            Entry(ThreadLocal> k, Object v) {
                //key赋值
                super(k);
                //value赋值
                value = v;
            }
        }</threadlocal>
登入後複製

这里的节点,key可以简单低视作ThreadLocal,value为代码中放入的值,当然实际上key并不是ThreadLocal本身,而是它的一个弱引用,可以看到Entry的key继承了 WeakReference(弱引用),再来看一下key怎么赋值的:

    public WeakReference(T referent) {
        super(referent);
    }
登入後複製

key的赋值,使用的是WeakReference的赋值。

歸納整理Java並發知識點

所以,怎么回答ThreadLocal原理?要答出这几个点:

  • Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,每个线程都有一个属于自己的ThreadLocalMap。
  • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值。
  • 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
  • ThreadLocal本身不存储值,它只是作为一个key来让线程往ThreadLocalMap里存取值。

13.ThreadLocal 内存泄露是怎么回事?

我们先来分析一下使用ThreadLocal时的内存,我们都知道,在JVM中,栈内存线程私有,存储了对象的引用,堆内存线程共享,存储了对象实例。

所以呢,栈中存储了ThreadLocal、Thread的引用,堆中存储了它们的具体实例。

歸納整理Java並發知識點

ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用。

“弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存。”

那么现在问题就来了,弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会造成了内存泄漏问题

那怎么解决内存泄漏问题呢?

很简单,使用完ThreadLocal后,及时调用remove()方法释放内存空间。

ThreadLocal localVariable = new ThreadLocal();try {
    localVariable.set("鄙人三某”);
    ……} finally {
    localVariable.remove();}
登入後複製

那为什么key还要设计成弱引用?

key设计成弱引用同样是为了防止内存泄漏。

假如key被设计成强引用,如果ThreadLocal Reference被销毁,此时它指向ThreadLoca的强引用就没有了,但是此时key还强引用指向ThreadLoca,就会导致ThreadLocal不能被回收,这时候就发生了内存泄漏的问题。

14.ThreadLocalMap的结构了解吗?

ThreadLocalMap虽然被叫做Map,其实它是没有实现Map接口的,但是结构还是和HashMap比较类似的,主要关注的是两个要素:元素数组散列方法

歸納整理Java並發知識點

  • 元素数组

    一个table数组,存储Entry类型的元素,Entry是ThreaLocal弱引用作为key,Object作为value的结构。

 private Entry[] table;
登入後複製
  • 散列方法

    散列方法就是怎么把对应的key映射到table数组的相应下标,ThreadLocalMap用的是哈希取余法,取出key的threadLocalHashCode,然后和table数组长度减一&运算(相当于取余)。

int i = key.threadLocalHashCode & (table.length - 1);
登入後複製

这里的threadLocalHashCode计算有点东西,每创建一个ThreadLocal对象,它就会新增0x61c88647,这个值很特殊,它是斐波那契数 也叫 黄金分割数hash增量为 这个数字,带来的好处就是 hash 分布非常均匀

    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
登入後複製

15.ThreadLocalMap怎么解决Hash冲突的?

我们可能都知道HashMap使用了链表来解决冲突,也就是所谓的链地址法。

ThreadLocalMap没有使用链表,自然也不是用链地址法来解决冲突了,它用的是另外一种方式——开放定址法。开放定址法是什么意思呢?简单来说,就是这个坑被人占了,那就接着去找空着的坑。

歸納整理Java並發知識點

如上图所示,如果我们插入一个value=27的数据,通过 hash计算后应该落入第 4 个槽位中,而槽位 4 已经有了 Entry数据,而且Entry数据的key和当前不相等。此时就会线性向后查找,一直找到 Entry为 null的槽位才会停止查找,把元素放到空的槽中。

在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该槽位Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置。

16.歸納整理Java並發知識點机制了解吗?

在ThreadLocalMap.set()方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()逻辑:

if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
登入後複製

再着看rehash()具体实现:这里会先去清理过期的Entry,然后还要根据条件判断size >= threshold - threshold / 4 也就是size >= threshold* 3/4来决定是否需要扩容。

private void rehash() {
    //清理过期Entry
    expungeStaleEntries();

    //扩容
    if (size >= threshold - threshold / 4)
        resize();}//清理过期Entryprivate void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j <p>接着看看具体的<code>resize()</code>方法,扩容后的<code>newTab</code>的大小为老数组的两倍,然后遍历老的table数组,散列方法重新计算位置,开放地址解决冲突,然后放到新的<code>newTab</code>,遍历完成之后,<code>oldTab</code>中所有的<code>entry</code>数据都已经放入到<code>newTab</code>中了,然后table引用指向<code>newTab</code></p><p><img src="https://img.php.cn/upload/article/000/000/067/6137d48077cbb320beee2007e8763d69-16.png" alt="歸納整理Java並發知識點"></p><p>具体代码:</p><p><img src="https://img.php.cn/upload/article/000/000/067/85310a4f8d2bb86283cd76fb2542424d-17.png" alt="ThreadLocalMap resize"></p><h2>17.父子线程怎么共享数据?</h2><p>父线程能用ThreadLocal来给子线程传值吗?毫无疑问,不能。那该怎么办?</p><p>这时候可以用到另外一个类——<code>InheritableThreadLocal</code>。</p><p>使用起来很简单,在主线程的InheritableThreadLocal实例设置值,在子线程中就可以拿到了。</p><pre class="brush:php;toolbar:false">public class InheritableThreadLocalTest {
    
    public static void main(String[] args) {
        final ThreadLocal threadLocal = new InheritableThreadLocal();
        // 主线程
        threadLocal.set("不擅技术");
        //子线程
        Thread t = new Thread() {
            @Override
            public void run() {
                super.run();
                System.out.println("鄙人三某 ," + threadLocal.get());
            }
        };
        t.start();
    }}
登入後複製

那原理是什么呢?

原理很简单,在Thread类里还有另外一个变量:

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
登入後複製

在Thread.init的时候,如果父线程的inheritableThreadLocals不为空,就把它赋给当前线程(子线程)的inheritableThreadLocals

        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)
登入後複製

18.说一下你对歸納整理Java並發知識點(JMM)的理解?

歸納整理Java並發知識點(Java Memory Model,JMM),是一种抽象的模型,被定义出来屏蔽各种硬件和操作系统的内存访问差异。

JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。

歸納整理Java並發知識點的抽象图:

歸納整理Java並發知識點

本地内存是JMM的 一个抽象概念,并不真实存在。它其实涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

歸納整理Java並發知識點

图里面的是一个双核 CPU 系统架构 ,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有 CPU 共享的二级缓存。 那么 Java 内存模型里面的工作内存,就对应这里的 Ll 缓存或者 L2 缓存或者 CPU 寄存器。

19.说说你对原子性、可见性、有序性的理解?

原子性、有序性、可见性是并发编程中非常重要的基础概念,JMM的很多技术都是围绕着这三大特性展开。

  • 原子性:原子性指的是一个操作是不可分割、不可中断的,要么全部执行并且执行的过程不会被任何因素打断,要么就全不执行。
  • 可见性:可见性指的是一个线程修改了某一个共享变量的值时,其它线程能够立即知道这个修改。
  • 有序性:有序性指的是对于一个线程的执行代码,从前往后依次执行,单线程下可以认为程序是有序的,但是并发时有可能会发生指令重排。

分析下面几行代码的原子性?

int i = 2;int j = i;i++;i = i + 1;
登入後複製
  • 第1句是基本类型赋值,是原子性操作。
  • 第2句先读i的值,再赋值到j,两步操作,不能保证原子性。
  • 第3和第4句其实是等效的,先读取i的值,再+1,最后赋值到i,三步操作了,不能保证原子性。

原子性、可见性、有序性都应该怎么保证呢?

  • 原子性:JMM只能保证基本的原子性,如果要保证一个代码块的原子性,需要使用synchronized
  • 可见性:Java是利用volatile关键字来保证可见性的,除此之外,finalsynchronized也能保证可见性。
  • 有序性:synchronized或者volatile都可以保证多线程之间操作的有序性。

20.那说说什么是指令重排?

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型。

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应 机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序,如图:

歸納整理Java並發知識點

我们比较熟悉的双重校验单例模式就是一个经典的指令重排的例子,Singleton instance=new Singleton();对应的JVM指令分为三步:分配内存空间–>初始化对象—>对象指向分配的内存空间,但是经过了编译器的指令重排序,第二步和第三步就可能会重排序。

歸納整理Java並發知識點

JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

21.指令重排有限制吗?happens-before了解吗?

指令重排也是有一些限制的,有两个规则happens-beforeas-if-serial来约束。

happens-before的定义:

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法

happens-before和我们息息相关的有六大规则:

歸納整理Java並發知識點

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的 ThreadB.start()操作happens-before于线程B中的任意操作。
  • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作 happens-before于线程A从ThreadB.join()操作成功返回。

22.as-if-serial又是什么?单线程的程序一定是顺序的吗?

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。为了具体说明,请看下面计算圆面积的代码示例。

double pi = 3.14;   // Adouble r = 1.0;   // B double area = pi * r * r;   // C
登入後複製

上面3个操作的数据依赖关系:

歸納整理Java並發知識點

A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。

所以最终,程序可能会有两种执行顺序:

歸納整理Java並發知識點

as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、runtime和处理器共同编织了这么一个“楚门的世界”:单线程程序是按程序的“顺序”来执行的。as- if-serial语义使单线程情况下,我们不需要担心重排序的问题,可见性的问题。

23.volatile实现原理了解吗?

volatile有两个作用,保证可见性有序性

volatile怎么保证可见性的呢?

相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,它没有上下文切换的额外开销成本。

volatile可以确保对某个变量的更新对其他线程马上可见,一个变量被声明为volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存 当其它线程读取该共享变量 ,会从主内存重新获取最新值,而不是使用当前线程的本地内存中的值。

例如,我们声明一个 volatile 变量 volatile int x = 0,线程A修改x=1,修改完之后就会把新的值刷新回主内存,线程B读取x的时候,就会清空本地内存变量,然后再从主内存获取最新值。

歸納整理Java並發知識點

volatile怎么保证有序性的呢?

重排序可以分为编译器重排序和处理器重排序,valatile保证有序性,就是通过分别限制这两种类型的重排序。

歸納整理Java並發知識點

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

  1. 在每个volatile写操作的前面插入一个StoreStore屏障
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障
  4. 在每个volatile读操作的后面插入一个LoadStore屏障

歸納整理Java並發知識點

24.synchronized用过吗?怎么使用?

synchronized经常用的,用来保证代码的原子性。

synchronized主要有三种用法:

  • 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
synchronized void method() {
  //业务代码}
登入後複製
  • 修饰静态方法:也就是给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得当前 class 的锁。因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管 new 了多少个对象,只有⼀份)。

    如果⼀个线程 A 调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。

synchronized void staic method() {
 //业务代码}
登入後複製
  • 修饰代码块 :指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进⼊同步代码库前要获得给定对象的锁。 synchronized(类.class) 表示进⼊同步代码前要获得 当前 class 的锁
synchronized(this) {
 //业务代码}
登入後複製

25.synchronized的实现原理?

synchronized是怎么加锁的呢?

我们使用synchronized的时候,发现不用自己去lock和unlock,是因为JVM帮我们把这个事情做了。

  1. synchronized修饰代码块时,JVM采用monitorentermonitorexit两个指令来实现同步,monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指向同步代码块的结束位置。

    反编译一段synchronized修饰代码块代码,javap -c -s -v -l SynchronizedDemo.class,可以看到相应的字节码指令。

歸納整理Java並發知識點

  1. 歸納整理Java並發知識點时,JVM采用ACC_SYNCHRONIZED标记符来实现同步,这个标识指明了该方法是一个同步方法。

    同样可以写段代码反编译看一下。

歸納整理Java並發知識點

synchronized锁住的是什么呢?

monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor实现的。

实例对象结构里有对象头,对象头里面有一块结构叫Mark Word,Mark Word指针指向了monitor

所谓的Monitor其实是一种同步工具,也可以说是一种同步机制。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,可以叫做内部锁,或者Monitor锁。

ObjectMonitor的工作原理:

  • ObjectMonitor有两个队列:_WaitSet、_EntryList,用来保存ObjectWaiter 对象列表。
  • _owner,获取 Monitor 对象的线程进入 _owner 区时, _count + 1。如果线程调用了 wait() 方法,此时会释放 Monitor 对象, _owner 恢复为空, _count - 1。同时该等待线程进入 _WaitSet 中,等待被唤醒。
ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录线程获取锁的次数
    _waiters      = 0,
    _recursions   = 0;  //锁的重入次数
    _object       = NULL;
    _owner        = NULL;  // 指向持有ObjectMonitor对象的线程
    _WaitSet      = NULL;  // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
登入後複製

可以类比一个去医院就诊的例子[18]:

  • 首先,患者在门诊大厅前台或自助挂号机进行挂号

  • 随后,挂号结束后患者找到对应的诊室就诊

    • 诊室每次只能有一个患者就诊;
    • 如果此时诊室空闲,直接进入就诊;
    • 如果此时诊室内有其它患者就诊,那么当前患者进入候诊室,等待叫号;
  • 就诊结束后,走出就诊室,候诊室的下一位候诊患者进入就诊室。

歸納整理Java並發知識點

这个过程就和Monitor机制比较相似:

  • 門診大廳:所有待進入的線程都必須先在入口Entry Set掛號才有資格;
  • 就診室:就診室**_Owner**裡裡只能有一個線程就診,就診完線程就自行離開
  • 候診室:就診室繁忙時,進入等待區(Wait Set),就診室空閒的時候就從**等待區(Wait Set)**叫新的執行緒

Java Montior机制

##所以我們就知道了,同步是鎖住的東西:

    monitorenter,在判斷擁有同步標識ACC_SYNCHRONIZED 搶先進入此方法的線程會優先擁有Monitor 的owner ,此時計數器1。
  • monitorexit,執行完退出後,計數器 -1,歸 0 後被其他進入的執行緒獲得。
26.除了原子性,synchronized可見性,有序性,可重入性怎麼實現?

synchronized怎麼保證可見性?

    線程加鎖前,將清空工作記憶體中共享變數的值,從而使用共享變數時需要從主記憶體重新讀取最新的值。
  • 線程加鎖後,其它執行緒無法取得主記憶體中的共享變數。
  • 執行緒解鎖前,必須把共享變數的最新值刷新到主記憶體中。
synchronized怎麼保證有序性?

synchronized同步的程式碼區塊,具有排他性,一次只能被一個執行緒擁有,所以synchronized保證同一時刻,程式碼是單執行緒執行的。

因為as-if-serial語意的存在,單執行緒的程式能保證最終結果是有順序的,但是不保證不會指令重排。

所以synchronized保證的順序是執行結果的有序性,而不是防止指令重排的有序性。

synchronized怎麼實現可重入的呢?

synchronized 是可重入鎖,也就是說,允許一個執行緒二次請求自己持有物件鎖的臨界資源,這種情況稱為可重入鎖定。

synchronized 鎖定物件的時候有個計數器,他會記錄下執行緒取得鎖的次數,在執行完對應的程式碼區塊之後,計數器就會-1,直到計數器清零,就釋放鎖了。

之所以,是可重入的。是因為 synchronized 鎖定物件有計數器,會隨著執行緒取得鎖定後 1 計數,當執行緒執行完畢後 -1,直到清除釋放鎖定。

27.鎖定升級? synchronized優化了解嗎?

了解鎖升級,得先知道,不同鎖的狀態是什麼樣的。這個狀態指的是什麼呢?

Java物件頭裡,有一塊結構,叫做

Mark Word#標記字段,這塊結構會隨著鎖的狀態變化而改變。

64 位元虛擬機器Mark Word 是64bit,我們來看看它的狀態變化:

Mark Word变化

#Mark Word儲存物件本身的運行數據,如

雜湊碼、GC分代年齡、鎖定狀態標誌、偏向時間戳記(Epoch) 等。

synchronized做了哪些最佳化?

在JDK1.6之前,synchronized的實作直接呼叫ObjectMonitor的enter和exit,這種鎖被稱為

重量級鎖定。從JDK6開始,HotSpot虛擬機開發團隊對Java中的鎖進行最佳化,例如增加了適應性自旋、鎖定消除、鎖定粗化、輕量級鎖定和偏向鎖等最佳化策略,提升了synchronized的效能。

  • 偏向鎖定:在無競爭的情況下,只是在Mark Word裡儲存目前執行緒指針,CAS操作都不做。

  • 輕量級鎖定:在沒有多執行緒競爭時,相對重量級鎖定,減少作業系統互斥量帶來的效能消耗。但是,如果有鎖定競爭,除了互斥本身開銷,還額外有CAS操作的開銷。

  • 自旋鎖定:減少不必要的CPU上下文切換。在輕量級鎖定升級為重量級鎖定時,就使用了自旋加鎖的方式

  • 鎖定粗化:將多個連續的加鎖、解鎖操作連接在一起,擴展成一個範圍更大的鎖。

  • 鎖定消除:虛擬機器即時編譯器在執行時,對一些程式碼上要求同步,但是被偵測到不可能存在共享資料競爭的鎖定進行消除。

鎖定升級的過程是什麼樣的?

鎖定升級方向:無鎖–>偏向鎖—> 輕量級鎖定---->重量級鎖,這個方向基本上是不可逆的。

歸納整理Java並發知識點

我們來看升級的過程:

偏向鎖定:

##偏向鎖定的取得:

######################################## #
  1. 判斷是否為可偏向狀態–MarkWord中鎖標誌是否為'01',是否偏向鎖是否為'1'
  2. 如果是可偏向狀態,則查看線程ID是否為當前線程,如果是,則進入步驟'5',否則進入步驟'3'
  3. 通過CAS操作競爭鎖,如果競爭成功,則將MarkWord中線程ID設為當前線程ID,然後執行'5 ';競爭失敗,則執行'4'
  4. CAS取得偏向鎖定失敗表示有競爭。當達到safepoint時獲得偏向鎖定的執行緒被掛起,偏向鎖定升級為輕量級鎖定,然後被阻塞在安全點的執行緒繼續往下執行同步程式碼區塊
  5. 執行同步程式碼

偏向鎖定的撤銷:

  1. #偏向鎖定不會主動釋放(撤銷),只有在遇到其他執行緒競爭時才會執行撤銷,由於撤銷需要知道目前持有該偏向鎖的線程棧狀態,因此要等到safepoint時執行,此時持有該偏向鎖的線程(T)有'2','3'兩種情況;
  2. 撤銷----T執行緒已經退出同步程式碼區塊,或已經不再存活,則直接撤銷偏向鎖,變成無鎖狀態----該狀態達到閾值20則執行批次重偏向
  3. 升級----T執行緒還在同步程式碼區塊中,則將T執行緒的偏向鎖升級為輕量級鎖定,目前執行緒執行輕量級鎖定狀態下的鎖取得步驟----狀態達到閾值40則執行批次撤銷

#輕量級鎖定:

輕量級鎖定的取得:

  1. #進行加鎖操作時,jvm會判斷是否已經時重量級鎖,如果不是,則會在當前線程堆疊幀中劃出一塊空間,作為該鎖的鎖記錄,並且將鎖定對象MarkWord複製到該鎖定記錄中
  2. 複製成功之後,jvm使用CAS操作將物件頭MarkWord更新為指向鎖定記錄的指針,並將鎖定記錄裡的owner指針指向物件頭的MarkWord。如果成功,則執行'3',否則執行'4'
  3. 更新成功,則當前執行緒持有該物件鎖,且物件MarkWord鎖定標誌設為'00',即表示此物件處於輕量級級鎖定狀態
  4. 更新失敗,jvm先檢查物件MarkWord是否指向目前執行緒堆疊訊框中的鎖定記錄,如果是則執行'5',否則執行'4'
  5. 表示鎖定重入;然後當前線程棧幀中增加一個鎖記錄第一部分(Displaced Mark Word)為null,並指向Mark Word的鎖對象,起到一個重入計數器的作用。
  6. 表示該鎖定物件已經被其他執行緒搶佔,則進行自旋等待(預設10次),等待次數達到閾值仍未取得到鎖,則升級為重量級鎖定

大體上省簡的升級過程:

歸納整理Java並發知識點

完整的升級過程:

synchronized 锁升级过程-来源参考[14]

28.說說synchronized和ReentrantLock的差別?

可以從鎖的實作、功能特點、效能等幾個維度去回答這個問題:

  • 鎖定的實作: synchronized是Java語言的關鍵字,基於JVM實作。而ReentrantLock是基於JDK的API層面實現的(一般是lock()和unlock()方法來配合try/finally 語句區塊來完成。)
  • 效能: 在JDK1.6鎖優化以前,synchronized的效能比ReenTrantLock差很多。但是JDK6開始,增加了適應性自旋、鎖定消除等,兩者表現就差不多了。
  • 功能特點: ReentrantLock 比 synchronized 增加了一些進階功能,例如等待可中斷、可實現公平鎖定、可實現選擇性通知。
    • ReentrantLock提供了一個能夠中斷等待鎖的執行緒的機制,透過lock.lockInterruptibly()來實作這個機制
    • ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。
    • synchronized與wait()和notify()/notifyAll()方法結合實作等待/通知機制,ReentrantLock類別借助Condition介面與newCondition()方法實作。
    • ReentrantLock需要手動宣告加鎖和釋放鎖,一般跟finally搭配釋放鎖。而synchronized不用手動釋放鎖。

下面的表格列出了兩種鎖定之間的差異:

歸納整理Java並發知識點

#29.AQS了解多少?

AbstractQueuedSynchronizer 抽象同步佇列,簡稱 AQS ,它是Java並發包的根基,並發包中的鎖就是基於AQS實現的。

  • AQS是基于一个FIFO的双向队列,其内部定义了一个节点类Node,Node 节点内部的 SHARED 用来标记该线程是获取共享资源时被阻挂起后放入AQS 队列的, EXCLUSIVE 用来标记线程是 取独占资源时被挂起后放入AQS 队列
  • AQS 使用一个 volatile 修饰的 int 类型的成员变量 state 来表示同步状态,修改同步状态成功即为获得锁,volatile 保证了变量在多线程之间的可见性,修改 State 值时通过 CAS 机制来保证修改的原子性
  • 获取state的方式分为两种,独占方式和共享方式,一个线程使用独占方式获取了资源,其它线程就会在获取失败后被阻塞。一个线程使用共享方式获取了资源,另外一个线程还可以通过CAS的方式进行获取。
  • 如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁的分配,AQS 中会将竞争共享资源失败的线程添加到一个变体的 CLH 队列中。

歸納整理Java並發知識點先简单了解一下CLH:Craig、Landin and Hagersten 队列,是 单向链表实现的队列。申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现 前驱节点释放了锁就结束自旋

歸納整理Java並發知識點

AQS 中的队列是 CLH 变体的虚拟双向队列,通过将每条请求共享资源的线程封装成一个节点来实现锁的分配:

AQS变种歸納整理Java並發知識點

AQS 中的 CLH 变体等待队列拥有以下特性:

  • AQS 中队列是个双向链表,也是 FIFO 先进先出的特性
  • 通过 Head、Tail 头尾两个节点来组成队列结构,通过 volatile 修饰保证可见性
  • Head 指向节点为已获得锁的节点,是一个虚拟节点,节点本身不持有具体线程
  • 获取不到同步状态,会将节点进行自旋获取锁,自旋一定次数失败后会将线程阻塞,相对于 CLH 队列性能较好

ps:AQS源码里面有很多细节可问,建议有时间好好看看AQS源码。

30.ReentrantLock实现原理?

ReentrantLock 是可重入的独占锁,只能有一个线程可以获取该锁,其它获取该锁的线程会被阻塞而被放入该锁的阻塞队列里面。

看看ReentrantLock的加锁操作:

    // 创建非公平锁
    ReentrantLock lock = new ReentrantLock();
    // 获取锁操作
    lock.lock();
    try {
        // 执行代码逻辑
    } catch (Exception ex) {
        // ...
    } finally {
        // 解锁操作
        lock.unlock();
    }
登入後複製

new ReentrantLock()构造函数默认创建的是非公平锁 NonfairSync。

公平锁 FairSync

  1. 公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁
  2. 公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大

非公平锁 NonfairSync

  • 非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁
  • 非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁

默认创建的对象lock()的时候:

  • 如果锁当前没有被其它线程占用,并且当前线程之前没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为1 ,然后直接返回。如果当前线程之前己经获取过该锁,则这次只是简单地把 AQS 的状态值加1后返回。
  • 如果该锁己经被其他线程持有,非公平锁会尝试去获取锁,获取失败的话,则调用该方法线程会被放入 AQS 队列阻塞挂起。

ReentrantLock 非公平锁加锁流程简图

31.ReentrantLock怎么实现公平锁的?

new ReentrantLock()构造函数默认创建的是非公平锁 NonfairSync

public ReentrantLock() {
    sync = new NonfairSync();}
登入後複製

同时也可以在创建锁构造函数中传入具体参数创建公平锁 FairSync

ReentrantLock lock = new ReentrantLock(true);--- ReentrantLock// true 代表公平锁,false 代表非公平锁public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();}
登入後複製

FairSync、NonfairSync 代表公平锁和非公平锁,两者都是 ReentrantLock 静态内部类,只不过实现不同锁语义。

非公平锁和公平锁的两处不同:

  1. 非公平鎖在呼叫 lock 後,首先就會呼叫 CAS 進行一次搶鎖,如果這個時候恰巧鎖沒有被佔用,那麼直接就獲取到鎖返回了。
  2. 非公平鎖在CAS 失敗後,和公平鎖一樣都會進入到tryAcquire 方法,在tryAcquire 方法中,如果發現鎖這個時候被釋放了(state == 0),非公平鎖會直接CAS搶鎖,但公平鎖會判斷等待佇列是否有執行緒處於等待狀態,如果有則不去搶鎖,乖乖排到後面。

歸納整理Java並發知識點

相對來說,非公平鎖定會有更好的效能,因為它的吞吐量比較大。當然,非公平鎖定讓獲取鎖的時間變得更加不確定,可能會導致在阻塞佇列中的執行緒長期處於飢餓狀態。

32.CAS呢? CAS了解多少?

CAS叫做CompareAndSwap,⽐較並交換,主要是透過處理器的指令來保證操作的原⼦性的。

CAS 指令包含 3 個參數:共享變數的記憶體位址 A、預期的值 B 和共享變數的新值 C。

只有當記憶體中位址 A 處的值等於 B 時,才能將記憶體中位址 A 處的值更新為新值 C。作為一條 CPU 指令,CAS 指令本身是能夠保證原子性的 。

33.CAS 有什麼問題?如何解決?

CAS的經典三大問題:

歸納整理Java並發知識點

ABA 問題

並發環境下,假設初始條件是A,去修改資料時,發現是A就會執行修改。但看到的雖然是A,中間可能發生了A變B,B又變回A的情況。此時A已經非彼A,資料即使成功修改,也可能有問題。

怎麼解決ABA問題?

  • 加上版本號

每次修改變量,都在這個變數的版本號碼上加1,這樣,剛剛A->B-> ;A,雖然A的值沒變,但是它的版本號已經變了,再判斷版本號就會發現此時的A已經被改過了。參考樂觀鎖的版本號,這種做法可以為資料帶上了一種實效性的檢驗。

Java提供了AtomicStampReference類,它的compareAndSet方法首先檢查當前的物件參考值是否等於預期引用,並且當前印戳(Stamp)標誌是否等於預期標誌,如果全部相等,則以原子方式將引用值和印戳標誌的值更新為給定的更新值。

循環效能開銷

自旋CAS,如果一直循環執行,一直不成功,會為CPU帶來非常大的執行開銷。

怎麼解決循環效能開銷問題?

在Java中,許多使用自旋CAS的地方,會有一個自旋次數的限制,超過一定次數,就停止自旋。

只能保證一個變數的原子操作

CAS 保證的是對一個變數執行操作的原子性,如果對多個變數操作時,CAS 目前無法直接保證操作的原子性的。

怎麼解決只能保證一個變數的原子操作問題?

  • 可以考慮改用鎖來保證操作的原子性
  • 可以考慮合併多個變量,將多個變量封裝成一個對象,透過AtomicReference來保證原子性。

34.Java有哪些保證原子性的方法?如何保證多執行緒下i 結果正確?

歸納整理Java並發知識點

  • 使用循環原子類,例如AtomicInteger,實作i 原子操作
  • 使用juc包下的鎖,如ReentrantLock ,對i 運算加鎖lock.lock()來實現原子性
  • 使用synchronized,對i 操作加鎖

35.原子操作類別了解多少?

當程式更新一個變數時,如果多執行緒同時更新這個變量,可能得到期望之外的值,例如變數i=1,A執行緒更新i 1,B執行緒也更新i 1,經過兩個執行緒操作之後可能i不等於3,而是等於2。因為A和B執行緒在更新變數i的時候拿到的i都是1,這就是執行緒不安全的更新操作,一般我們會使用synchronized來解決這個問題,synchronized會保證多執行緒不會同時更新變數i。

其實除此之外,還有更輕量級的選擇,Java從JDK 1.5開始提供了java.util.concurrent.atomic包,這個包中的原子操作類別提供了一種用法簡單、效能高效、線程安全地更新一個變數的方式。

因為變數的型別有很多種,所以在Atomic套件裡一共提供了13個類,屬於4種類型的原子更新方式,分別是原子更新基本型別、原子更新陣列、原子更新引用和原子更新屬性(字段)。

歸納整理Java並發知識點

Atomic套件裡的類別基本上都是使用Unsafe實作的包裝類別。

使用原子的方式更新基本类型,Atomic包提供了以下3个类:

  • AtomicBoolean:原子更新布尔类型。

  • AtomicInteger:原子更新整型。

  • AtomicLong:原子更新长整型。

通过原子的方式更新数组里的某个元素,Atomic包提供了以下4个类:

  • AtomicIntegerArray:原子更新整型数组里的元素。

  • AtomicLongArray:原子更新长整型数组里的元素。

  • AtomicReferenceArray:原子更新引用类型数组里的元素。

  • AtomicIntegerArray类主要是提供原子的方式更新数组里的整型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类:

  • AtomicReference:原子更新引用类型。

  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

  • AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)。

如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新:

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的 ABA问题。

36.AtomicInteger 的原理?

一句话概括:使用CAS实现

以AtomicInteger的添加方法为例:

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
登入後複製

通过Unsafe类的实例来进行添加操作,来看看具体的CAS操作:

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
登入後複製

compareAndSwapInt 是一个native方法,基于CAS来操作int类型变量。其它的歸納整理Java並發知識點基本都是大同小异。

37.线程死锁了解吗?该如何避免?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

歸納整理Java並發知識點

那么为什么会产生死锁呢? 死锁的产生必须具备以下四个条件:

歸納整理Java並發知識點

  • 互斥条件:指线程对己经获取到的资源进行它性使用,即该资源同时只由一个线程占用。如果此时还有其它线程请求获取获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
  • 请求并持有条件:指一个 线程己经持有了至少一个资源,但又提出了新的资源请求,而新资源己被其它线程占有,所以当前线程会被阻塞,但阻塞 的同时并不释放自己已经获取的资源。
  • 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其它线程抢占,只有在自己使用完毕后才由自己释放该资源。
  • 环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即线程集合 {T0,T1,T2,…… ,Tn} 中 T0 正在等待一 T1 占用的资源,Tl1正在等待 T2用的资源,…… Tn 在等待己被 T0占用的资源。

该如何避免死锁呢?答案是至少破坏死锁发生的一个条件

  • 其中,互斥这个条件我们没有办法破坏,因为用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?

  • 对于“请求并持有”这个条件,可以一次性请求所有的资源。

  • 对于“不可剥夺”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

  • 对于“环路等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后就不存在环路了。

38.那死锁问题怎么排查呢?

可以使用jdk自带的命令行工具排查:

  1. 使用jps查找运行的Java进程:jps -l
  2. 使用jstack查看线程堆栈信息:jstack -l 进程id

基本就可以看到死锁的信息。

还可以利用图形化工具,比如JConsole。出现线程死锁以后,点击JConsole线程面板的检测到死锁按钮,将会看到线程的死锁信息。

歸納整理Java並發知識點

39.CountDownLatch(倒计数器)了解吗?

CountDownLatch,倒计数器,有两个常见的应用场景[18]:

场景1:协调子线程结束动作:等待所有子线程运行结束

CountDownLatch允许一个或多个线程等待其他线程完成操作。

例如,我们很多人喜欢玩的王者荣耀,开黑的时候,得等所有人都上线之后,才能开打。

歸納整理Java並發知識點

CountDownLatch模仿这个场景(参考[18]):

创建大乔、兰陵王、安其拉、哪吒和铠等五个玩家,主线程必须在他们都完成确认后,才可以继续运行。

在这段代码中,new CountDownLatch(5)用户创建初始的latch数量,各玩家通过countDownLatch.countDown()完成状态确认,主线程通过countDownLatch.await()等待。

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);

        Thread 大乔 = new Thread(countDownLatch::countDown);
        Thread 兰陵王 = new Thread(countDownLatch::countDown);
        Thread 安其拉 = new Thread(countDownLatch::countDown);
        Thread 哪吒 = new Thread(countDownLatch::countDown);
        Thread 铠 = new Thread(() -> {
            try {
                // 稍等,上个卫生间,马上到...
                Thread.sleep(1500);
                countDownLatch.countDown();
            } catch (InterruptedException ignored) {}
        });

        大乔.start();
        兰陵王.start();
        安其拉.start();
        哪吒.start();
        铠.start();
        countDownLatch.await();
        System.out.println("所有玩家已经就位!");
    }
登入後複製

场景2. 协调子线程开始动作:统一各线程动作开始的时机

王者游戏中也有类似的场景,游戏开始时,各玩家的初始状态必须一致。不能有的玩家都出完装了,有的才降生。

所以大家得一块出生,在

歸納整理Java並發知識點

在这个场景中,仍然用五个线程代表大乔、兰陵王、安其拉、哪吒和铠等五个玩家。需要注意的是,各玩家虽然都调用了start()线程,但是它们在运行时都在等待countDownLatch的信号,在信号未收到前,它们不会往下执行。

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);

        Thread 大乔 = new Thread(() -> waitToFight(countDownLatch));
        Thread 兰陵王 = new Thread(() -> waitToFight(countDownLatch));
        Thread 安其拉 = new Thread(() -> waitToFight(countDownLatch));
        Thread 哪吒 = new Thread(() -> waitToFight(countDownLatch));
        Thread 铠 = new Thread(() -> waitToFight(countDownLatch));

        大乔.start();
        兰陵王.start();
        安其拉.start();
        哪吒.start();
        铠.start();
        Thread.sleep(1000);
        countDownLatch.countDown();
        System.out.println("敌方还有5秒达到战场,全军出击!");
    }

    private static void waitToFight(CountDownLatch countDownLatch) {
        try {
            countDownLatch.await(); // 在此等待信号再继续
            System.out.println("收到,发起进攻!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
登入後複製

CountDownLatch的核心方法也不多:

  • await():等待latch降为0;
  • boolean await(long timeout, TimeUnit unit):等待latch降为0,但是可以设置超时时间。比如有玩家超时未确认,那就重新匹配,总不能为了某个玩家等到天荒地老。
  • countDown():latch数量减1;
  • getCount():获取当前的latch数量。

40.CyclicBarrier(同步屏障)了解吗?

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一 组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

它和CountDownLatch类似,都可以协调多线程的结束动作,在它们结束后都可以执行特定动作,但是为什么要有CyclicBarrier,自然是它有和CountDownLatch不同的地方。

不知道你听没听过一个新人UP主小约翰可汗,小约翰生平有两大恨——“歸納整理Java並發知識點”我们来还原一下事情的经过:小约翰在亲政后认识了新垣结衣,于是决定第一次选妃,向结衣表白,等待回应。然而新垣结衣回应嫁给了星野源,小约翰伤心欲绝,发誓生平不娶,突然发现了铃木爱理,于是小约翰决定第二次选妃,求爱理搭理,等待回应。

歸納整理Java並發知識點

我们拿代码模拟这一场景,发现CountDownLatch无能为力了,因为CountDownLatch的使用是一次性的,无法重复利用,而这里等待了两次。此时,我们用CyclicBarrier就可以实现,因为它可以重复利用。

歸納整理Java並發知識點

歸納整理Java並發知識點:

歸納整理Java並發知識點

CyclicBarrier最最核心的方法,仍然是await():

  • 如果当前线程不是第一个到达屏障的话,它将会进入等待,直到其他线程都到达,除非发生被中断屏障被拆除屏障被重设等情况;

上面的例子抽象一下,本质上它的流程就是这样就是这样:

歸納整理Java並發知識點

41.CyclicBarrier和CountDownLatch有什么区别?

两者最核心的区别[18]:

  • CountDownLatch是一次性的,而CyclicBarrier則可以多次設定屏障,實現重複利用;
  • CountDownLatch中的各個子線程不可以等待其他線程,只能完成自己的任務;而CyclicBarrier中的各個執行緒可以等待其他執行緒

它們區別用一個表格整理:

CyclicBarrier CountDownLatch
CyclicBarrier是可重複使用的,其中的執行緒會等待所有的執行緒完成任務。屆時,屏障將被拆除,並可以選擇性地做一些特定的動作。 CountDownLatch是一次性的,不同的執行緒在同一個計數器上工作,直到計數器為0.
CyclicBarrier面向的是執行緒數 CountDownLatch面向的是任務數
在使用CyclicBarrier時,你必須在建構中指定參與協作的執行緒數,這些執行緒必須呼叫await()方法 使用CountDownLatch時,則必須要指定任務數,至於這些任務由哪些執行緒完成無關緊要
CyclicBarrier可以在所有的執行緒釋放後重新使用 CountDownLatch在當計數器為0時不能再使用
在CyclicBarrier中,如果某個執行緒遇到了中斷、逾時等問題時,則處於await的執行緒都會出現問題 #在CountDownLatch中,如果某個執行緒出現問題,其他執行緒不受影響
#

42.Semaphore(信号量)了解吗?

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

听起来似乎很抽象,现在汽车多了,开车出门在外的一个老大难问题就是停车 。停车场的车位是有限的,只能允许若干车辆停泊,如果停车场还有空位,那么显示牌显示的就是绿灯和剩余的车位,车辆就可以驶入;如果停车场没位了,那么显示牌显示的就是绿灯和数字0,车辆就得等待。如果满了的停车场有车离开,那么显示牌就又变绿,显示空车位数量,等待的车辆就能进停车场。

歸納整理Java並發知識點

我们把这个例子类比一下,车辆就是线程,进入停车场就是线程在执行,离开停车场就是线程执行完毕,看见红灯就表示线程被阻塞,不能执行,Semaphore的本质就是协调多个线程对共享资源的获取

歸納整理Java並發知識點

我们再来看一个Semaphore的用途:它可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。

假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制,如下:

public class SemaphoreTest {
    private static final int THREAD_COUNT = 30;
    private static ExecutorService threadPool = Executors.new歸納整理Java並發知識點(THREAD_COUNT);
    private static Semaphore s = new Semaphore(10);

    public static void main(String[] args) {
        for (int i = 0; i <p>在代码中,虽然有30个线程在执行,但是只允许10个并发执行。Semaphore的构造方法<code>Semaphore(int permits</code>)接受一个整型的数字,表示可用的许可证数量。<code>Semaphore(10)</code>表示允许10个线程获取许可证,也就是最大并发数是10。Semaphore的用法也很简单,首先线程使用 Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证。</p><h2>43.Exchanger 了解吗?</h2><p>Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。</p><p><img src="https://img.php.cn/upload/article/000/000/067/a99f1f171b5f8c414e2981e6a7d5189a-57.png" alt="歸納整理Java並發知識點"></p><p>这两个线程通过 exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。</p><p>Exchanger可以用于遗传算法,遗传算法里需要选出两个人作为交配对象,这时候会交换两人的数据,并使用交叉规则得出2个交配结果。Exchanger也可以用于校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否录入一致。</p><pre class="brush:php;toolbar:false">public class ExchangerTest {
    private static final Exchanger<string> exgr = new Exchanger<string>();
    private static ExecutorService threadPool = Executors.new歸納整理Java並發知識點(2);

    public static void main(String[] args) {
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    String A = "银行流水A"; // A录入银行流水数据 
                    exgr.exchange(A);
                } catch (InterruptedException e) {
                }
            }
        });
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    String B = "银行流水B"; // B录入银行流水数据 
                    String A = exgr.exchange("B");
                    System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:"
                            + A + ",B录入是:" + B);
                } catch (InterruptedException e) {
                }
            }
        });
        threadPool.shutdown();
    }}</string></string>
登入後複製

假如两个线程有一个没有执行exchange()方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以使用exchange(V x, long timeOut, TimeUnit unit)设置最大等待时长

44.什么是线程池?

线程池: 简单理解,它就是一个歸納整理Java並發知識點。

歸納整理Java並發知識點

  • 它帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的。
  • 提高响应速度。 如果任务到达了,相对于从线程池拿线程,重新去创建一条线程执行,速度肯定慢很多。
  • 重复利用。 线程用完,再放回池子,可以达到重复利用的效果,节省资源。

45.能说说工作中线程池的应用吗?

之前我们有一个和第三方对接的需求,需要向第三方推送数据,引入了多线程来提升数据推送的效率,其中用到了线程池来管理线程。

歸納整理Java並發知識點

歸納整理Java並發知識點如下:

歸納整理Java並發知識點

完整可运行代码地址:https://gitee.com/fighter3/thread-demo.git

线程池的参数如下:

  • corePoolSize:线程核心参数选择了CPU数×2

  • maximumPoolSize:最大线程数选择了和核心线程数相同

  • keepAliveTime:非核心闲置线程存活时间直接置为0

  • unit:非核心线程保持存活的时间选择了 TimeUnit.SECONDS 秒

  • workQueue:线程池等待队列,使用 LinkedBlockingQueue阻塞队列

同时还用了synchronized 来加锁,保证数据不会被重复推送:

  synchronized (PushProcessServiceImpl.class) {}
登入後複製

ps:这个例子只是简单地进行了数据推送,实际上还可以结合其他的业务,像什么数据清洗啊、数据统计啊,都可以套用。

46.能简单说一下线程池的工作流程吗?

用一个通俗的比喻:

有一个营业厅,总共有六个窗口,现在开放了三个窗口,现在有三个窗口坐着三个营业员小姐姐在营业。

老三去办业务,可能会遇到什么情况呢?

  1. 老三发现有空间的在营业的窗口,直接去找小姐姐办理业务。

歸納整理Java並發知識點

  1. 老三发现没有空闲的窗口,就在排队区排队等。

歸納整理Java並發知識點

  1. 老三发现没有空闲的窗口,等待区也满了,蚌埠住了,经理一看,就让休息的小姐姐赶紧回来上班,等待区号靠前的赶紧去新窗口办,老三去排队区排队。小姐姐比较辛苦,假如一段时间发现他们可以不用接着营业,经理就让她们接着休息。

歸納整理Java並發知識點

  1. 老三一看,六个窗口都满了,等待区也没位置了。老三急了,要闹,经理赶紧出来了,经理该怎么办呢?

歸納整理Java並發知識點

  1. 我们银行系统已经瘫痪

  2. 谁叫你来办的你找谁去

  3. 看你比较急,去队里加个塞

  4. 今天没办法,不行你看改一天

上面的这个流程几乎就跟 JDK 线程池的大致流程类似,

  1. 营业中的 3个窗口对应核心线程池数:corePoolSize
  2. 总的营业窗口数6对应:maximumPoolSize
  3. 打开的临时窗口在多少时间内无人办理则关闭对应:unit
  4. 排队区就是等待队列:workQueue
  5. 无法办理的时候银行给出的解决方法对应:RejectedExecutionHandler
  6. threadFactory 该参数在 JDK 中是 线程工厂,用来创建线程对象,一般不会动。

所以我们线程池的工作流程也比较好理解了:

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
  • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
  • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
  • 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  • 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会根据拒绝策略来对应处理。

歸納整理Java並發知識點

  1. 当一个线程完成任务时,它会从队列中取下一个任务来执行。

  2. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

47.线程池主要参数有哪些?

歸納整理Java並發知識點

线程池有七大参数,需要重点关注corePoolSizemaximumPoolSizeworkQueuehandler这四个。

  1. corePoolSize

此值是用来初始化线程池中核心线程数,当线程池中线程池数corePoolSize时,系统默认是添加一个任务才创建一个线程池。当线程数 = corePoolSize时,新任务会追加到workQueue中。

  1. maximumPoolSize

maximumPoolSize表示允许的最大线程数 = (非核心线程数+核心线程数),当BlockingQueue也满了,但线程池中总线程数 maximumPoolSize时候就会再次创建新的线程。

  1. keepAliveTime

非核心线程 =(maximumPoolSize - corePoolSize ) ,非核心线程闲置下来不干活最多存活时间。

  1. unit

线程池中非核心线程保持存活的时间的单位

  • TimeUnit.DAYS; 天
  • TimeUnit.HOURS; 小时
  • TimeUnit.MINUTES; 分钟
  • TimeUnit.SECONDS; 秒
  • TimeUnit.MILLISECONDS; 毫秒
  • TimeUnit.MICROSECONDS; 微秒
  • TimeUnit.NANOSECONDS; 纳秒
  1. workQueue

线程池等待队列,维护着等待执行的Runnable对象。当运行当线程数= corePoolSize时,新的任务会被添加到workQueue中,如果workQueue也满了则尝试用非核心线程执行任务,等待队列应该尽量用有界的。

  1. threadFactory

创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等。

  1. handler

corePoolSizeworkQueuemaximumPoolSize都不可用的时候执行的饱和策略。

48.线程池的拒绝策略有哪些?

类比前面的例子,无法办理业务时的处理方式,帮助记忆:

歸納整理Java並發知識點

  • AbortPolicy :直接抛出异常,默认使用此策略
  • CallerRunsPolicy:用调用者所在的线程来执行任务
  • DiscardOldestPolicy:丢弃阻塞队列里最老的任务,也就是队列里靠前的任务
  • DiscardPolicy :当前任务直接丢弃

想实现自己的拒绝策略,实现RejectedExecutionHandler接口即可。

49.线程池有哪几种工作队列?

常用的阻塞队列主要有以下几种:

歸納整理Java並發知識點

  • ArrayBlockingQueue:ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
  • LinkedBlockingQueue:LinkedBlockingQueue(可设置容量队列)是基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;new歸納整理Java並發知識點线程池使用了这个队列
  • DelayQueue:DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
  • PriorityBlockingQueue:PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列
  • SynchronousQueue:SynchronousQueue(同步队列)是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。

50.线程池提交execute和submit有什么区别?

  1. execute 用于提交不需要返回值的任务
threadsPool.execute(new Runnable() { 
    @Override public void run() { 
        // TODO Auto-generated method stub } 
    });
登入後複製
  1. submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个 future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值
Future<object> future = executor.submit(harReturnValuetask); try { Object s = future.get(); } catch (InterruptedException e) { 
    // 处理中断异常 } catch (ExecutionException e) { 
    // 处理无法执行任务异常 } finally { 
    // 关闭线程池 executor.shutdown();}</object>
登入後複製

51.线程池怎么关闭知道吗?

可以通过调用线程池的shutdownshutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。

shutdown() 将线程池状态置为shutdown,并不会立即停止

  1. 停止接收外部submit的任务
  2. 内部正在跑的任务和队列里等待的任务,会执行完
  3. 等到第二步完成后,才真正停止

shutdownNow() 将线程池状态置为stop。一般会立即停止,事实上不一定

  1. 和shutdown()一样,先停止接收外部提交的任务
  2. 忽略队列里等待的任务
  3. 尝试将正在跑的任务interrupt中断
  4. 返回未执行的任务列表

shutdown 和shutdownnow简单来说区别如下:

  • shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大。
  • shutdown()只是关闭了提交通道,用submit()是无效的;而内部的任务该怎么跑还是怎么跑,跑完再彻底停止线程池。

52.线程池的线程数应该怎么配置?

线程在Java中属于稀缺资源,线程池不是越大越好也不是越小越好。任务分为计算密集型、IO密集型、混合型。

  1. 计算密集型:大部分都在用CPU跟内存,加密,逻辑操作业务处理等。
  2. IO密集型:数据库链接,网络通讯传输等。

常见歸納整理Java並發知識點配置方案-来源美团技术博客

一般的经验,不同类型线程池的参数配置:

  1. 计算密集型一般推荐线程池不要过大,一般是CPU数 + 1,+1是因为可能存在页缺失(就是可能存在有些数据在硬盘中需要多来一个线程将数据读入内存)。如果线程池数太大,可能会频繁的 进行线程上下文切换跟任务调度。获得当前CPU核心数代码如下:
Runtime.getRuntime().availableProcessors();
登入後複製
  1. IO密集型:线程数适当大一点,机器的Cpu核心数*2。
  2. 混合型:可以考虑根绝情况将它拆分成CPU密集型和IO密集型任务,如果执行时间相差不大,拆分可以提升吞吐量,反之没有必要。

当然,实际应用中没有固定的公式,需要结合测试和监控来进行调整。

53.有哪几种常见的线程池?

面试常问,主要有四种,都是通过工具类Excutors创建出来的,需要注意,阿里巴巴《Java开发手册》里禁止使用这种方式来创建线程池。

歸納整理Java並發知識點

  • new歸納整理Java並發知識點 (固定数目线程的线程池)

  • newCachedThreadPool (可缓存线程的线程池)

  • newSingleThreadExecutor (单线程的线程池)

  • newScheduledThreadPool (定时及周期执行的线程池)

54.能说一下四种常见线程池的原理吗?

前三种线程池的构造直接调用ThreadPoolExecutor的构造方法。

newSingleThreadExecutor

  public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<runnable>(),
                                    threadFactory));
    }</runnable>
登入後複製

线程池特点

  • 核心线程数为1
  • 最大线程数也为1
  • 阻塞队列是无界队列LinkedBlockingQueue,可能会导致OOM
  • keepAliveTime为0

歸納整理Java並發知識點

工作流程:

  • 提交任务
  • 线程池是否有一条线程在,如果没有,新建线程执行任务
  • 如果有,将任务加到阻塞队列
  • 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个线程执行任务。

适用场景

适用于串行执行任务的场景,一个任务一个任务地执行。

new歸納整理Java並發知識點

  public static ExecutorService new歸納整理Java並發知識點(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<runnable>(),
                                      threadFactory);
    }</runnable>
登入後複製

线程池特点:

  • 核心线程数和最大线程数大小一样
  • 没有所谓的非空闲时间,即keepAliveTime为0
  • 阻塞队列为无界队列LinkedBlockingQueue,可能会导致OOM

歸納整理Java並發知識點

工作流程:

  • 提交任务
  • 如果线程数少于核心线程,创建核心线程执行任务
  • 如果线程数等于核心线程,把任务添加到LinkedBlockingQueue阻塞队列
  • 如果线程执行完任务,去阻塞队列取任务,继续执行。

使用场景

歸納整理Java並發知識點 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。

newCachedThreadPool

   public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<runnable>(),
                                      threadFactory);
    }</runnable>
登入後複製

线程池特点:

  • 核心线程数为0
  • 最大线程数为Integer.MAX_VALUE,即无限大,可能会因为无限创建线程,导致OOM
  • 阻塞队列是SynchronousQueue
  • 非核心线程空闲存活时间为60秒

当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

歸納整理Java並發知識點

工作流程:

  • 提交任务
  • 因为没有核心线程,所以任务直接加到SynchronousQueue队列。
  • 判断是否有空闲线程,如果有,就去取出任务执行。
  • 如果没有空闲线程,就新建一个线程执行。
  • 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。

适用场景

用于并发执行大量短期的小任务。

newScheduledThreadPool

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
登入後複製

线程池特点

  • 最大线程数为Integer.MAX_VALUE,也有OOM的风险
  • 阻塞队列是DelayedWorkQueue
  • keepAliveTime为0
  • scheduleAtFixedRate() :按某种速率周期执行
  • scheduleWithFixedDelay():在某个延迟后执行

歸納整理Java並發知識點

工作机制

  • 线程从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。到期任务是指ScheduledFutureTask的time大于等于当前时间。
  • 线程执行这个ScheduledFutureTask。
  • 线程修改ScheduledFutureTask的time变量为下次将要被执行的时间。
  • 线程把这个修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。

歸納整理Java並發知識點

使用场景

周期性执行任务的场景,需要限制线程数量的场景

使用无界队列的线程池会导致什么问题吗?

例如new歸納整理Java並發知識點使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长,会导致队列的任务越积越多,导致机器内存使用不停飙升,最终导致OOM。

55.线程池异常怎么处理知道吗?

在使用线程池处理任务的时候,任务代码可能抛出RuntimeException,抛出异常后,线程池可能捕获它,也可能创建一个新的线程来代替异常的线程,我们可能无法感知任务出现了异常,因此我们需要考虑线程池异常情况。

常见的异常处理方式:

歸納整理Java並發知識點

56.能说一下线程池有几种状态吗?

线程池有这几个状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED。

   //线程池状态
   private static final int RUNNING    = -1 <p>线程池各个状态切换图:</p><p><img src="https://img.php.cn/upload/article/000/000/067/47690a2c7799a7bd0d10ed5490da3a7b-77.png" alt="歸納整理Java並發知識點"></p><p><strong>RUNNING</strong></p>
登入後複製
  • 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
  • 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
  • 调用线程池的shutdownNow()方法,可以切换到STOP状态;

SHUTDOWN

  • 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
  • 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;

STOP

  • 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
  • 线程池中执行的任务为空,进入TIDYING状态;

TIDYING

  • 该状态表明所有的任务已经运行终止,记录的任务数量为0。
  • terminated()执行完毕,进入TERMINATED状态

TERMINATED

  • 该状态表示线程池彻底终止

57.线程池如何实现参数的动态修改?

线程池提供了几个 setter方法来设置线程池的参数。

JDK 歸納整理Java並發知識點设置接口来源参考[7]

这里主要有两个思路:

动态修改歸納整理Java並發知識點

  • 在我們微服務的架構下,可以利用配置中心如Nacos、Apollo等等,也可以自己開發配置中心。業務服務讀取線程池配置,取得對應的線程池實例來修改線程池的參數。

  • 如果限制了配置中心的使用,也可以自己去擴展ThreadPoolExecutor,重寫方法,監聽線程池參數變化,來動態修改線程池參數。

執行緒池調優了解嗎?

執行緒池配置沒有固定的公式,通常事前會對執行緒池進行一定評估,常見的評估方案如下:

线程池评估方案 来源参考[7]

##上線前也要進行充分的測試,上線之後要建立完善的執行緒池監控機制。

事中結合監控警告機制,分析執行緒池的問題,或可最佳化點,結合執行緒池動態參數配置機制來調整設定。

事後要注意仔細觀察,隨時調整。

歸納整理Java並發知識點

具體的調優案例可以查看參考[7]美團技術部落格。

58.你能設計實作一個執行緒池嗎?

這題在阿里的面試中出現頻率比較高

線程池實現原理可以查看 要是以前有人這麼講線程池,我早就該明白了! ,當然,我們自己實現, 只需要抓住線程池的核心流程-參考[6]:

歸納整理Java並發知識點

#我們自己的實現就是完成這個核心流程:

    執行緒池中有N個工作執行緒
  • 把任務提交給執行緒池執行
  • #如果執行緒池已滿,把任務放入佇列
  • #最後當有空閒時,取得佇列中任務來執行
實作程式碼[6]:

歸納整理Java並發知識點

這樣,一個實作了執行緒池主要流程的類別就完成了。

59.單機執行緒池執行斷電了該怎麼處理?

我們可以對正在處理和阻塞佇列的任務做事務管理或對阻塞佇列中的任務持久化處理,並且當斷電或系統崩潰,操作無法繼續下去的時候,可以透過回溯日誌的方式來撤銷

正在處理的已經執行成功的操作。然後重新執行整個阻塞佇列。

也就是說,對阻塞佇列持久化;正在處理任務事務控制;斷電之後正在處理任務的回滾,透過日誌恢復該次操作;伺服器重啟後阻塞佇列中的資料再載入。

並發容器與框架

關於一些並發容器,可以去看看面渣逆襲:Java集合連環三十問,裡面有

CopyOnWriteList ConcurrentHashMap這兩種線程安全容器類別的問答。 。

60.Fork/Join框架了解嗎?

Fork/Join框架是Java7提供的一個用於平行執行任務的框架,是一個把大任務分割成若干個小任務,最終匯總每個小任務結果後得到大任務結果的框架。

要掌握Fork/Join框架,首先需要理解兩個點,

分而治之工作竊取演算法

分而治之

Fork/Join框架的定義,其實體現了分治思想:將一個規模為N的問題分解為K個規模較小的子問題,這些子問題相互獨立且與原問題性質相同。求出子問題的解,就可得到原問題的解。

歸納整理Java並發知識點

工作竊取演算法

大任務拆成了若干個小任務,把這些小任務放到不同的佇列裡,各自建立單獨執行緒來執行佇列裡的任務。

那麼問題來了,有的線程幹活塊,有的線程幹活慢。幹完活的線程不能讓它空下來,得讓它去幫沒幹完活的線程工作。它去其它線程的佇列裡竊取一個任務來執行,這就是所謂的

工作竊取

工作竊取發生的時候,它們會存取同一個佇列,為了減少竊取任務執行緒和被竊取任務執行緒之間的競爭,通常任務會使用雙端佇列,被竊取任務執行緒永遠從雙端佇列的頭部拿,而竊取任務的執行緒永遠從雙端佇列的尾部拿任務執行。

歸納整理Java並發知識點

看一個Fork/Join框架應用的例子,計算1~n之間的和:1 2 3 … n

  • 设置一个分割阈值,任务大于阈值就拆分任务
  • 任务有结果,所以需要继承RecursiveTask
public class CountTask extends RecursiveTask<integer> {
    private static final int THRESHOLD = 16; // 阈值
    private int start;
    private int end;

    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        // 如果任务足够小就计算任务
        boolean canCompute = (end - start)  result = forkJoinPool.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
        }
    }
    }</integer>
登入後複製

ForkJoinTask与一般Task的主要区别在于它需要实现compute方法,在这个方法里,首先需要判断任务是否足够小,如果足够小就直接执行任务。如果比较大,就必须分割成两个子任务,每个子任务在调用fork方法时,又会进compute方法,看看当前子任务是否需要继续分割成子任务,如果不需要继续分割,则执行当前子任务并返回结果。使用join方法会等待子任务执行完并得到其结果。

推荐学习:《java教程

以上是歸納整理Java並發知識點的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:csdn.net
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板