2019年java中高級java面試題(七)多線程

1、 現在有T1、T2、T3三個線程,你怎樣保證T2在T1執行完後執行,T3在T2執行完後執行?

        Thread t1 = new Thread(() -> System.out.println("t1執行"));
        Thread t2 = new Thread(() -> System.out.println("t2執行"));
        Thread t3 = new Thread(() -> System.out.println("t3執行"));
        try {
            // t1先啓動
            t1.start();
            t1.join();
            // t2
            t2.start();
            t2.join();
            // t3
            t3.start();
            t3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


上述主要是利用join方法  等待該線程終止。主線程等待t1執行完     再開啓t2線程,等待t2線程執行完再執行線程t3

join方法源碼分析

 public final synchronized void join(long millis)throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
 
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
 
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }


2、 什麼是線程


線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位。

3、 線程和進程有什麼區別?


線程是進程的子集,一個進程可以有很多線程,每條線程並行執行不同的任務。不同的進程使用不同的內存空間,而所有的線程共享一片相同的內存空間

4、 Java中Runnable和Callable有什麼不同?


Runnable和Callable都代表那些要在不同的線程中執行的任務。Runnable從JDK1.0開始就有了,Callable是在JDK1.5增加的。它們的主要區別是

(1)Callable規定的方法是call(),Runnable規定的方法是run()。其中Runnable可以提交給Thread來包裝下,直接啓動一個線程來執行,而Callable則一般都是提交給ExecuteService來執行。

(2)Callable的任務執行後可返回值,而Runnable的任務是不能返回值得

(3)call方法可以拋出異常,run方法不可以

(4)運行Callable任務可以拿到一個Future對象,c表示異步計算的結果。

5、Java中CountDownLatch 和CyclicBarrier 有什麼不同?

CountDownLatch類位於java.util.concurrent包下,利用它可以實現類似計數器的功能,一個線程(或者多個), 等待另外N個線程完成某個事情之後才能執行

CyclicBarrier通過它可以實現讓一組線程等待至某個狀態之後再全部同時執行,CyclicBarrier可以被重用
 

CountDownLatch

CyclicBarrier

減計數方式

加計數方式

計算爲0時釋放所有等待的線程

計數達到指定值時釋放所有等待線程

計數爲0時,無法重置

計數達到指定值時,計數置爲0重新開始

調用countDown()方法計數減一,調用await()方法只進行阻塞,對計數沒任何影響

調用await()方法計數加1,若加1後的值不等於構造方法的值,則線程阻塞

不可重複利用

可重複利用

6、線程運行的多種狀態

 

  •          創建  new Thread類或者其子類對象。
  •          運行 start().  具備者CPU的執行資格和CPU的執行權。
  •          凍結 釋放了CPU的執行資格和CPU的執行權。比如執行到sleep(time)  wait() 導致線程凍結。
  •          臨時阻塞狀態: 具備者CPU的執行資格,不具備CPU的執行權。
  •         消亡:線程結束。run方法結束 

7、Java中的volatile 變量是什麼?


volatile是一個特殊的修飾符,只有成員變量才能使用它。 當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。

volatile特性一:內存可見性,即線程A對volatile變量的修改,其他線程獲取的volatile變量都是最新的。

volatile特性二:可以禁止指令重排序

volatile關鍵字保證了操作的可見性,但是不能保證對變量的操作是原子性。

8、 volatile的原理和實現機制


加入volatile關鍵字時,生成的彙編代碼會多出一個lock前綴指令

  lock前綴指令實際上相當於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:

  1)它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;

  2)它會強制將對緩存的修改操作立即寫入主存;

  3)如果是寫操作,它會導致其他CPU中對應的緩存行無效。

9、 使用volatile關鍵字的場景


1、狀態標記量

volatile boolean flag= false;
 
// 線程1
context = loadContext();
flag= true;
 
// 線程2
while(!flag) {
    sleep();
}
doSomethingWithConfig(context);


2、雙重檢查

所謂雙重檢查加鎖機制,指的是:並不是每次進入getInstance方法都需要同步,而是先不同步,進入方法過後,先檢查實例是否存在,如果不存在才進入下面的同步塊,這是第一重檢查。進入同步塊過後,再次檢查實例是否存在,如果不存在,就在同步的情況下創建一個實例,這是第二重檢查。這樣一來,就只需要同步一次了,從而減少了多次在同步情況下進行判斷所浪費的時間。 

public class Singleton {
    private volatile static Singleton instance = null;
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {// 1
                if (instance == null) {// 2
                    instance = new Singleton();// 3
                }
            }
        }
        return instance;
    }
}


3、獨立觀察(independent observation)

安全使用 volatile 的另一種簡單模式是:定期 “發佈” 觀察結果供程序內部使用。例如,假設有一種環境傳感器能夠感覺環境溫度。一個後臺線程可能會每隔幾秒讀取一次該傳感器,並更新包含當前文檔的 volatile 變量。然後,其他線程可以讀取這個變量,從而隨時能夠看到最新的溫度值。

使用該模式的另一種應用程序就是收集程序的統計信息。下面程序 展示了身份驗證機制如何記憶最近一次登錄的用戶的名字。將反覆使用lastUser引用來發布值,以供程序的其他部分使用。

public class UserManager {
    public volatile String lastUser;
 
    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }
}


4、“volatile bean” 模式

volatile bean 模式適用於將 JavaBeans 作爲“榮譽結構”使用的框架。在 volatile bean 模式中,JavaBean 被用作一組具有 getter 和/或 setter 方法 的獨立屬性的容器。volatile bean 模式的基本原理是:很多框架爲易變數據的持有者(例如 HttpSession)提供了容器,但是放入這些容器中的對象必須是線程安全的。

在 volatile bean 模式中,JavaBean 的所有數據成員都是 volatile 類型的,並且 getter 和 setter 方法必須非常普通 —— 除了獲取或設置相應的屬性外,不能包含任何邏輯。此外,對於對象引用的數據成員,引用的對象必須是有效不可變的。(這將禁止具有數組值的屬性,因爲當數組引用被聲明爲 volatile 時,只有引用而不是數組本身具有 volatile 語義)。對於任何 volatile 變量,不變式或約束都不能包含 JavaBean 屬性。下面代碼示例展示了遵守 volatile bean 模式的 JavaBean:

@ThreadSafe
public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;
 
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
 
    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }
 
    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }
 
    public void setAge(int age) { 
        this.age = age;
    }
}


5 、開銷較低的讀-寫鎖策略

目前爲止,您應該瞭解了 volatile 的功能還不足以實現計數器。因爲++x 實際上是三種操作(讀、添加、存儲)的簡單組合,如果多個線程湊巧試圖同時對 volatile 計數器執行增量操作,那麼它的更新值有可能會丟失。

然而,如果讀操作遠遠超過寫操作,您可以結合使用內部鎖和 volatile 變量來減少公共代碼路徑的開銷。清單 6 中顯示的線程安全的計數器使用synchronized確保增量操作是原子的,並使用volatile 保證當前結果的可見性。如果更新不頻繁的話,該方法可實現更好的性能,因爲讀路徑的開銷僅僅涉及 volatile 讀操作,這通常要優於一個無競爭的鎖獲取的開銷。

@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;
 
    public int getValue() { return value; }
 
    public synchronized int increment() {
        return value++;
    }
}


有關volatile 查看更多

10、如何終止一個線程

Java提供了很豐富的API但沒有爲停止線程提供API。JDK 1.0本來有一些像stop(), suspend() 和 resume()的控制方法但是由於潛在的死鎖威脅因此在後續的JDK版本中他們被棄用了,之後Java API的設計者就沒有提供一個兼容且線程安全的方法來停止一個線程。當run() 或者 call() 方法執行完的時候線程會自動結束,如果要手動結束一個線程,你可以用volatile 布爾變量來退出run()方法的循環或者是取消任務來中斷線程。示例代碼

 

public class StopThreadDemo implements Runnable {
 
    private volatile boolean stopFlag = false;
 
    @Override
    public void run() {
 
        while (!stopFlag) {
            synchronized (StopThreadDemo.class) {
                System.out.println("我好忙啊,我在做事情!");
                try {
                    StopThreadDemo.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    this.stopThread();
                }
            }
        }
    }
 
    private void stopThread() {
        stopFlag = true;
        System.out.println("老子把線程給停了!");
    }
 
    public static void main(String[] args) {
        StopThreadDemo d = new StopThreadDemo();
        Thread t1 = new Thread(d, "旺財");
        Thread t2 = new Thread(d, "小強");
        try {
            t1.start();
            t2.start();
            Thread.sleep(5000);
            t1.interrupt();// 對t1線程對象進行中斷狀態的清除,強制讓其恢復到運行狀態。
            t2.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


11、 生產者消費者模型
 

 class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 
 
   final Object[] items = new Object[100];
   int putptr, takeptr, count;
 
   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length) 
         notFull.await();
       items[putptr] = x; 
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }
 
   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0) 
         notEmpty.await();
       Object x = items[takeptr]; 
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   } 
 }


12、 什麼是ThreadLocal變量?


ThreadLocal是線程本地的變量,只要是本線程內都可以使用,線程結束了,那麼相應的線程本地變量也就跟隨着線程消失了。

在Thread類裏面有一個ThreadLocalMap,用於存儲每一個線程的變量的引用,這個Map中的鍵爲ThreadLocal對象,而值對應的是ThreadLocal通過set放進去的變量引用。

13、 什麼是FutureTask?


在Java併發程序中FutureTask表示一個可以取消的異步運算。它有啓動和取消運算、查詢運算是否完成和取回運算結果等方法,此類提供了對 Future 的基本實現。只有當運算完成的時候結果才能取回,如果運算尚未完成get方法將會阻塞。可使用 FutureTask 包裝 Callable 或 Runnable 對象。因爲 FutureTask 實現了 Runnable,所以可將 FutureTask 提交給 Executor 執行

14、 Java中interrupt 、interrupted 和 isInterruptedd方法的區別?


interrupt 中斷線程。

如果當前線程沒有中斷它自己(這在任何情況下都是允許的),則該線程的 checkAccess 方法就會被調用,這可能拋出 SecurityException

如果線程在調用 Object 類的 wait()、wait(long) 或 wait(long, int) 方法,或者該類的 join()join(long)join(long, int)sleep(long) sleep(long, int) 方法過程中受阻,則其中斷狀態將被清除,它還將收到一個 InterruptedException

如果該線程在可中斷的通道上的 I/O 操作中受阻,則該通道將被關閉,該線程的中斷狀態將被設置並且該線程將收到一個 ClosedByInterruptException

如果該線程在一個 Selector 中受阻,則該線程的中斷狀態將被設置,它將立即從選擇操作返回,並可能帶有一個非零值,就好像調用了選擇器的 wakeup 方法一樣。

如果以前的條件都沒有保存,則該線程的中斷狀態將被設置。

中斷一個不處於活動狀態的線程不需要任何作用。

 

interrupted() 和 isInterrupted()的主要區別是前者會將中斷狀態清除而後者不會。

(1)interrupted 是作用於當前線程,isInterrupted 是作用於調用該方法的線程對象所對應的線程。

(2)這兩個方法最終都會調用同一個方法isInterrupted(boolean flag),只不過參數一個是true,一個是false。是前者會將中斷狀態清除而後者不會。

15、Java中的同步集合與併發集合有什麼區別?


不管是同步集合還是併發集合他們都支持線程安全,他們之間主要的區別體現在性能和可擴展性,還有他們如何實現的線程安全。同步HashMap, Hashtable, HashSet, Vector, ArrayList 相比他們併發的實現(比如:ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteHashSet)會慢得多。造成如此慢的主要原因是鎖, 同步集合會把整個Map或List鎖起來,而併發集合不會。併發集合實現線程安全是通過使用先進的和成熟的技術像鎖剝離。比如ConcurrentHashMap 會把整個Map 劃分成幾個片段,只對相關的幾個片段上鎖,同時允許多線程訪問其他未上鎖的片段。

16、 常用的線程池


ThreadPoolExecutor

構造方法:

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,
TimeUnit unit,BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,RejectedExecutionHandler handler)


參數:

corePoolSize - 池中所保存的線程數,包括空閒線程。

maximumPoolSize - 池中允許的最大線程數。

keepAliveTime - 當線程數大於核心時,此爲終止前多餘的空閒線程等待新任務的最長時間。

unit - keepAliveTime 參數的時間單位。

workQueue - 執行前用於保持任務的隊列。此隊列僅保持由 execute 方法提交的 Runnable 任務。

threadFactory - 執行程序創建新線程時使用的工廠。

handler - 由於超出線程範圍和隊列容量而使執行被阻塞時所使用的處理程序。

超出線程範圍和隊列容量而使執行被阻塞時所使用的處理程序
策略 含義
ThreadPoolExecutor.AbortPolicy   用於被拒絕任務的處理程序,它將拋出 RejectedExecutionException
ThreadPoolExecutor.CallerRunsPolicy  用於被拒絕任務的處理程序,它直接在 execute 方法的調用線程中運行被拒絕的任務;如果執行程序已關閉,則會丟棄該任務。
ThreadPoolExecutor.DiscardOldestPolicy  用於被拒絕任務的處理程序,它放棄最舊的未處理請求,然後重試 execute;如果執行程序已關閉,則會丟棄該任務。
ThreadPoolExecutor.DiscardPolicy  用於被拒絕任務的處理程序,默認情況下它將丟棄被拒絕的任務。
  • AbortPolicy 丟棄任務,拋運行時異常
  • CallerRunsPolicy 執行任務
  • DiscardPolicy 忽視,什麼都不會發生
  • DiscardOldestPolicy 從隊列中踢出最先進入隊列(最後一個執行)的任務

 

線程池工具類 Executors 可以構造常用的線程池

線程池  
newFixedThreadPool(int nThreads)

public static ExecutorService newFixedThreadPool(int nThreads){
    return new ThreadPoolExecutor(nThreads, nThreads,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>());



}
創建一個可重用固定線程數的線程池,以共享的無界隊列方式來運行這些線程
newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

創建一個可根據需要創建新線程的線程池,但是在以前構造的線程可用時將重用它們

newScheduledThreadPool(int corePoolSize) 創建一個線程池,它可安排在給定延遲後運行命令或者定期地執行。
newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

創建一個使用單個 worker 線程的 Executor,以無界隊列方式來運行該線程

 

 

17、 sleep和wait的區別?


sleep和wait都會使線程進入凍結狀態,並且都會釋放cpu的執行權和cpu的執行資格,不同的是sleep不會釋放鎖,而wait會連鎖一起釋放。
sleep()方法可以在任何地方使用;wait()方法則只能在同步方法或同步塊中使用
sleep方法是Thread類中定義的方法,wait是Object中定義的方法

18、  fail-fast和fail-safe機制


fail-fast即快速失敗機制,是java集合(Collection)中的一種錯誤檢測機制。當在迭代集合的過程中該集合在結構上發生改變的時候,就有可能會發生fail-fast,即拋出ConcurrentModificationException異常

fail-safe 採用安全失敗機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷

19 、JVM性能調優監控工具jps、jstack、jmap、jhat、jstat、hprof使用


A、 jps(Java Virtual Machine Process Status Tool)      

    jps主要用來輸出JVM中運行的進程狀態信息。語法格式如下:

jps [options] [hostid]


    如果不指定hostid就默認爲當前主機或服務器。

    命令行參數選項說明如下:

-q 不輸出類名、Jar名和傳入main方法的參數
 
-m 輸出傳入main方法的參數
 
-l 輸出main類或Jar的全限名
 
-v 輸出傳入JVM的參數


  比如下面:

B、 jstack

    jstack主要用來查看某個Java進程內的線程堆棧信息。語法格式如下:

jstack [option] pid
jstack [option] executable core
jstack [option] [server-id@]remote-hostname-or-ip


    命令行參數選項說明如下:

-l long listings,會打印出額外的鎖信息,在發生死鎖時可以用jstack -l pid來觀察鎖持有情況-m mixed mode,不僅會輸出Java堆棧信息,還會輸出C/C++堆棧信息(比如Native方法)


    jstack可以定位到線程堆棧,根據堆棧信息我們可以定位到具體代碼,所以它在JVM性能調優中使用得非常多。下面我們來一個實例找出某個Java進程中最耗費CPU的Java線程並定位堆棧信息,用到的命令有ps、top、printf、jstack、grep。

查找最消耗cpu的java線程

命令:可以使用ps -Lfp pid或者ps -mp pid -o THREAD, tid, time或者top -Hp pid

 

    TIME列就是各個Java線程耗費的CPU時間,CPU時間最長的是線程ID爲21742的線程,用

printf "%x\n" 21742


    得到21742的十六進制值爲54ee,下面會用到。    

    OK,下一步終於輪到jstack上場了,它用來輸出進程21711的堆棧信息,然後根據線程ID的十六進制值grep,如下:

root@ubuntu:/# jstack 21711 | grep 54ee
"PollIntervalRetrySchedulerThread" prio=10 tid=0x00007f950043e000 nid=0x54ee in Object.wait() [0x00007f94c6eda000


C、 jmap(Memory Map)和jhat(Java Heap Analysis Tool)

    jmap用來查看堆內存使用狀況,一般結合jhat使用。

    jmap語法格式如下:

jmap [option] pid
jmap [option] executable core
jmap [option] [server-id@]remote-hostname-or-ip


 

D、jstat(JVM統計監測工具)

    語法格式如下:

jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ]


    vmid是Java虛擬機ID,在Linux/Unix系統上一般就是進程ID。interval是採樣時間間隔。count是採樣數目。比如下面輸出的是GC信息,採樣時間間隔爲250ms,採樣數爲4: 

20160220110058880 (558Ã231)

可以看出:

 

堆內存 = 年輕代 + 年老代 
年輕代 = Eden區 + 兩個Survivor區(From和To)


現在來解釋各列含義:

S0C、S1C、S0U、S1U:Survivor 0/1區容量(Capacity)和使用量(Used)
EC、EU:Eden區容量和使用量
OC、OU:年老代容量和使用量
MC、MU:元數據空間容量和使用量
CCSC、CCSU
YGC、YGT:年輕代GC次數和GC耗時
FGC、FGCT:Full GC次數和Full GC耗時
GCT:GC總耗時


 

E、hprof(Heap/CPU Profiling Tool)

    hprof能夠展現CPU使用率,統計堆內存使用情況。

    語法格式如下:

java -agentlib:hprof[=options] ToBeProfiledClass
java -Xrunprof[:options] ToBeProfiledClass
javac -J-agentlib:hprof[=options] ToBeProfiledClass


20、  Java中的同步集合與併發集合有什麼區別


不管是同步集合還是併發集合他們都支持線程安全,他們之間主要的區別體現在性能和可擴展性,還有他們如何實現的線程安全。同步HashMap, Hashtable, HashSet, Vector, ArrayList 相比他們併發的實現(比如:ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteHashSet)會慢得多。造成如此慢的主要原因是鎖, 同步集合會把整個Map或List鎖起來,而併發集合不會。併發集合實現線程安全是通過使用先進的和成熟的技術像鎖剝離。比如ConcurrentHashMap 會把整個Map 劃分成幾個片段,只對相關的幾個片段上鎖,同時允許多線程訪問其他未上鎖的片段。

同樣的,CopyOnWriteArrayList 允許多個線程以非同步的方式讀,當有線程寫的時候它會將整個List複製一個副本給它。

如果在讀多寫少這種對併發集合有利的條件下使用併發集合,這會比使用同步集合更具有可伸縮性。

同步集合與併發集合都爲多線程和併發提供了合適的線程安全的集合,不過併發集合的可擴展性更高。在Java1.5之前程序員們只有同步集合來用且在多線程併發的時候會導致爭用,阻礙了系統的擴展性。Java5介紹了併發集合像ConcurrentHashMap,不僅提供線程安全還用鎖分離和內部分區等現代技術提高了可擴展性。

21、 Java中synchronized 和 ReentrantLock 有什麼不同


 這兩種方式最大區別就是對於Synchronized來說,它是java語言的關鍵字,是原生語法層面的互斥,需要jvm實現。而ReentrantLock它是JDK 1.5之後提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語句塊來完成。

 Synchronized進過編譯,會在同步塊的前後分別形成monitorenter和monitorexit這個兩個字節碼指令。在執行monitorenter指令時,首先要嘗試獲取對象鎖。如果這個對象沒被鎖定,或者當前線程已經擁有了那個對象鎖,把鎖的計算器加1,相應的,在執行monitorexit指令時會將鎖計算器就減1,當計算器爲0時,鎖就被釋放了。如果獲取對象鎖失敗,那當前線程就要阻塞,直到對象鎖被另一個線程釋放爲止。

22、 Java中的fork join框架是什麼?


Fork/Join框架是Java 7提供的一個用於並行執行任務的框架,是一個把大任務分割成若干個小任務,最終彙總每個小任務結果後得到大任務結果的框架。Fork/Join框架要完成兩件事情:

  1.任務分割:首先Fork/Join框架需要把大的任務分割成足夠小的子任務,如果子任務比較大的話還要對子任務進行繼續分割

  2.執行任務併合並結果:分割的子任務分別放到雙端隊列裏,然後幾個啓動線程分別從雙端隊列裏獲取任務執行。子任務執行完的結果都放在另外一個隊列裏,啓動一個線程從隊列裏取數據,然後合併這些數據。

  在Java的Fork/Join框架中,使用兩個類完成上述操作

  1.ForkJoinTask:我們要使用Fork/Join框架,首先需要創建一個ForkJoin任務。該類提供了在任務中執行fork和join的機制。通常情況下我們不需要直接集成ForkJoinTask類,只需要繼承它的子類,Fork/Join框架提供了兩個子類:

    a.RecursiveAction:用於沒有返回結果的任務

    b.RecursiveTask:用於有返回結果的任務

  2.ForkJoinPool:ForkJoinTask需要通過ForkJoinPool來執行

  任務分割出的子任務會添加到當前工作線程所維護的雙端隊列中,進入隊列的頭部。當一個工作線程的隊列裏暫時沒有任務時,它會隨機從其他工作線程的隊列的尾部獲取一個任務(工作竊取算法)。

public class CountTask extends RecursiveTask<Integer>{
 
    private static final int THREAD_HOLD = 2;
 
    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) <= THREAD_HOLD;
        if(canCompute){
            for(int i=start;i<=end;i++){
                sum += i;
            }
        }else{
            int middle = (start + end) / 2;
            CountTask left = new CountTask(start,middle);
            CountTask right = new CountTask(middle+1,end);
            //執行子任務
            left.fork();
            right.fork();
            //獲取子任務結果
            int lResult = left.join();
            int rResult = right.join();
            sum = lResult + rResult;
        }
        return sum;
    }
 
    public static void main(String[] args){
        ForkJoinPool pool = new ForkJoinPool();
        CountTask task = new CountTask(1,4);
        Future<Integer> result = pool.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}


23、 Java中的鎖機制  偏向鎖 & 輕量級鎖 & 重量級鎖各自優缺點及場景

優點

缺點

適用場景

偏向鎖

加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。

如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。

適用於只有一個線程訪問同步塊場景。

輕量級鎖

競爭的線程不會阻塞,提高了程序的響應速度。

如果始終得不到鎖競爭的線程使用自旋會消耗CPU。

追求響應時間。

同步塊執行速度非常快。

重量級鎖

線程競爭不使用自旋,不會消耗CPU。

線程阻塞,響應時間緩慢。

追求吞吐量。

同步塊執行速度較長。

24 、synchronized修飾代碼塊、普通方法、靜態方法有什麼區別。


修飾代碼塊獲取的是指定的對象監視器。

修飾普通代碼塊並未顯示使用monitorenter指令,而是通過標誌位ACC_SYNCHRONIZED標誌位來判斷是否由synchronized修飾,獲取的是該實例對象的監視器。

修飾靜態方法同普通方法,不同的是獲取的是這個類的監視器 

25、 synchronized 和 lock 有什麼區別?


1.首先synchronized是java內置關鍵字,在jvm層面,Lock是個java類;

2.synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖;

3.synchronized會自動釋放鎖(a 線程執行完同步代碼會釋放鎖 ;b 線程執行過程中發生異常會釋放鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成線程死鎖;

4.Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;

5.synchronized的鎖可重入、不可中斷、非公平,而Lock鎖可重入、可判斷、可公平(兩者皆可)

6.Lock鎖適合大量同步的代碼的同步問題,synchronized鎖適合代碼少量的同步問題。

26、 什麼是CAS?


使用鎖時,線程獲取鎖是一種悲觀鎖策略,即假設每一次執行臨界區代碼都會產生衝突,所以當前線程獲取到鎖的時候同時也會阻塞其他線程獲取該鎖。而CAS操作(又稱爲無鎖操作)是一種樂觀鎖策略,它假設所有線程訪問共享資源的時候不會出現衝突,既然不會出現衝突自然而然就不會阻塞其他線程的操作。因此,線程就不會出現阻塞停頓的狀態。那麼,如果出現衝突了怎麼辦?無鎖操作是使用CAS(compare and swap)又叫做比較交換來鑑別線程是否出現衝突,出現衝突就重試當前操作直到沒有衝突爲止。

CAS比較交換的過程可以通俗的理解爲CAS(V,O,N),包含三個值分別爲:V 內存地址存放的實際值;O 預期的值(舊值);N 更新的新值

27、 Synchronized 和 CAS


這是兩者主要的區別是Synchronized在存在線程競爭的情況下會出現線程阻塞和喚醒鎖帶來的性能問題,因爲這是一種互斥同步(阻塞同步)。

而CAS並不是武斷的間線程掛起,當CAS操作失敗後會進行一定的嘗試,而非進行耗時的掛起喚醒的操作,因此也叫做非阻塞同步。

CAS的問題

ABA問題
因爲CAS會檢查舊值有沒有變化,這裏存在這樣一個有意思的問題。比如一箇舊值A變爲了成B,然後再變成A,剛好在做CAS時檢查發現舊值並沒有變化依然爲A,但是實際上的確發生了變化。解決方案可以沿襲數據庫中常用的樂觀鎖方式,添加一個版本號可以解決。原來的變化路徑A->B->A就變成了1A->2B->3C。爲了解決ABA問題,偉大的java爲我們提供了AtomicMarkableReference和AtomicStampedReference類,爲我們解決了問題

自旋時間過長

28、 java中原子操作


(1)原子更新基本類型

atomic包提高原子更新基本類型的工具類,主要有這些:

AtomicBoolean:以原子更新的方式更新boolean;
AtomicInteger:以原子更新的方式更新Integer;
AtomicLong:以原子更新的方式更新Long
(2)原子更新數組類型

atomic包下提供能原子更新數組中元素的類有:

 

AtomicIntegerArray:原子更新整型數組中的元素;
AtomicLongArray:原子更新長整型數組中的元素;
AtomicReferenceArray:原子更新引用類型數組中的元素
(3)原子更新引用類型

如果需要原子更新引用類型變量的話,爲了保證線程安全,atomic也提供了相關的類:

 

AtomicReference:原子更新引用類型;
AtomicReferenceFieldUpdater:原子更新引用類型裏的字段;
AtomicMarkableReference:原子更新帶有標記位的引用類型;
(4)原子更新字段類型

如果需要更新對象的某個字段,並在多線程的情況下,能夠保證線程安全,atomic同樣也提供了相應的原子操作類:

AtomicIntegeFieldUpdater:原子更新整型字段類;
AtomicLongFieldUpdater:原子更新長整型字段類;
AtomicStampedReference:原子更新引用類型,這種更新方式會帶有版本號。而爲什麼在更新的時候會帶有版本號,是爲了解決CAS的ABA問題;
要想使用原子更新字段需要兩步操作:

原子更新字段類都是抽象類,只能通過靜態方法newUpdater來創建一個更新器,並且需要設置想要更新的類和屬性;
更新類的屬性必須使用public volatile進行修飾;
 
 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章