天天看點

40道Java并發程式設計高頻面試題,收藏等于精通

作者:程式員的秃頭之路

為什麼使用Executor架構?

使用Executor架構有以下幾個優點:

  • 提高程式性能:使用線程池來管理線程,可以減少線程的建立和銷毀造成的消耗。
  • 避免資源耗盡:線程池可以限制并發線程數,進而避免資源耗盡的問題。
  • 提供異步程式設計:Executor架構提供了異步程式設計的支援,允許開發者在程式中同時執行多個任務,進而提高程式的響應能力和吞吐量。
  • 提供任務隊列:Executor架構提供了任務隊列的支援,可以将多個任務按照一定的規則放入任務隊列中,然後由線程池按照一定的政策來取出任務并執行。
  • 提供統一的接口:Executor架構提供了一系列接口,可以讓開發者友善地管理線程池和任務隊列,進而減少代碼的複雜性。

相比之下,每次執行任務建立線程 new Thread()有以下幾個缺點:

  • 消耗性能:建立一個線程是比較耗時、耗資源的。
  • 缺乏管理:調用 new Thread()建立的線程缺乏管理,被稱為野線程,而且可以無限制的建立,線程之間的互相競争會導緻過多占用系統資源而導緻系統癱瘓,還有線程之間的頻繁交替也會消耗很多系統資源。
  • 不利于擴充:使用new Thread() 啟動的線程不利于擴充,比如定時執行、定期執行、定時定期執行、線程中斷等都不便實作。

是以,使用Executor架構是一種更好的選擇,它可以讓并發程式設計變得更加簡單和高效。

什麼是線程池?為什麼要使用它?

線程池是一種管理多個線程的技術,它可以在程式啟動時建立一定數量的線程,并将它們放在一個容器中,當有任務到來時,就從容器中取出一個空閑的線程來執行任務,當任務結束後,再将這個線程放回容器中,等待下一個任務。這樣可以避免頻繁地建立和銷毀線程,提高程式的性能和穩定性。

使用線程池的主要優點有:

  • 降低資源消耗。通過重複利用已建立的線程,減少了線程建立和銷毀造成的開銷。
  • 提高響應速度。當任務到達時,可以不需要等待線程建立就能立即執行。
  • 提高線程的可管理性。線程池可以統一配置設定、調優和監控線程的狀态,避免了線程數量過多導緻的資源競争和記憶體溢出問題。

使用線程池的主要缺點有:

  • 線程池不适用于生命周期較長或較大的任務,因為這樣會占用線程池中的資源,影響其他任務的執行。
  • 線程池不能對任務設定優先級,也不能辨別線程的各個狀态,如啟動、終止等。
  • 線程池對于每個應用程式域隻能有一個,如果想把線程放到不同的應用程式域中,就需要建立新的線程池。
  • 線程池所有的線程都處于多線程單元中,如果想把線程放到單線程單元中,就需要自己實作。

從JDK1.5開始,Java提供了Executor架構來支援不同類型的線程池,如固定大小、可緩存、定時、單例等。、可以根據不同的場景選擇合适的線程池來執行任務。

Java中Executor、ExecutorService和Executors的差別?

在Java中,Executor、ExecutorService和Executors都是用于管理和控制線程的重要元件。

  • Executor 是一個接口,它定義了一個方法:execute(Runnable),該方法用于執行線程任務。它是Java中線程執行架構的基礎,能夠建立和管理線程池。
  • ExecutorService 是 Executor 的子接口,它提供了更多用于任務執行和線程池管理的方法。其中包括:
    • submit(Runnable) 和 submit(Callable):這兩個方法都可以用于執行任務,其中 Callable 任務執行後有傳回值。這兩個方法都會傳回一個 Future 對象,可以用于擷取任務執行的結果或者取消任務。
    • invokeAll(Collection<Callable>) 和 invokeAny(Collection<Callable>):這兩個方法可以批量執行一組 Callable 任務,并分别傳回一個包含所有 Future 的清單或者任一任務的結果。
    • shutdown() 和 shutdownNow():這兩個方法用于關閉線程池,其中 shutdownNow() 方法會嘗試取消正在執行的任務。
  • Executors 是一個工具類,它提供了一系列的靜态工廠方法用于建立不同類型的線程池,這些方法包括:
    • newSingleThreadExecutor():建立一個隻有單個線程的線程池。
    • newFixedThreadPool(int nThreads):建立一個固定大小的線程池。
    • newCachedThreadPool():建立一個可根據需要建立新線程或複用空閑線程的線程池。
    • newScheduledThreadPool(int corePoolSize):建立一個支援定時及周期性任務執行的線程池。

總結來說,Executor 是定義線程任務執行基本方法的接口,ExecutorService 是 Executor 的擴充接口,提供了更多的任務執行和線程池管理功能,而 Executors 是一個工具類,提供了一系列建立不同類型線程池的方法。

什麼是原子操作?在Java API中有哪些原子類(atomic classes)?

原子操作是指不可被中斷的一個或一系列操作,它們可以保證在多線程環境下資料的一緻性和安全性。原子操作可以通過處理器的緩存加鎖或總線加鎖,或者通過Java的鎖和循環CAS(Compare And Set)的方式來實作。

在Java Concurrency API中,有一些原子類(atomic classes)可以提供原子操作的支援,它們位于java.util.concurrent.atomic包下。這些原子類有以下幾種類型:

  • 原子更新基本類型:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
  • 原子更新數組:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  • 原子更新屬性:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
  • 解決ABA問題的原子類:AtomicMarkableReference,AtomicStampedReference

這些原子類都提供了一些常用的方法,如get(),set(),getAndSet(),incrementAndGet(),getAndIncrement()等。使用這些原子類可以簡化代碼,提高性能,并保證線程安全。

線程與程序的差別?

  • 程序是作業系統配置設定資源的最小單元,線程是作業系統排程的最小單元。
  • 一個程式至少有一個程序,一個程序至少有一個線程。
  • 線程共享本程序的位址空間和資源,而程序之間是獨立的位址空間和資源。
  • 線程的建立和切換比程序的建立和切換更快,開銷更小。
  • 線程之間的通信和同步比程序之間的通信和同步更容易。
  • 多線程可以提高程式的并發性,但也增加了複雜性和風險。
  • 線程受全局解釋器鎖(GIL)的限制,不能充分利用多核CPU,而多程序可以。

java中有幾種方法可以實作一個線程?

java中有四種方法可以實作一個線程,分别是:

  • 繼承 Thread 類,并重寫 run () 方法。例如:
class PrimeThread extends Thread {
  long minPrime;
  PrimeThread (long minPrime) {
    this.minPrime = minPrime;
  }
  public void run () {
    // compute primes larger than minPrime
    . . .
  }
}
// 建立并啟動線程
PrimeThread p = new PrimeThread (143);
p.start ();           
  • 實作 Runnable 接口,并實作 run () 方法。例如:
class PrimeRun implements Runnable {
  long minPrime;
  PrimeRun (long minPrime) {
    this.minPrime = minPrime;
  }
  public void run () {
    // compute primes larger than minPrime
    . . .
  }
}
// 建立并啟動線程
PrimeRun p = new PrimeRun (143);
new Thread (p).start ();           
  • 使用匿名内部類,即在建立 Thread 對象時直接傳入一個 Runnable 執行個體。例如:
new Thread(new Runnable() {
  public void run() {
    // do something
    . . .
  }
}).start();           
  • 實作 Callable 接口,并實作 call () 方法。這種方法可以讓線程傳回一個結果,需要配合 FutureTask 使用。例如:
class PrimeCall implements Callable<Integer> {
  long minPrime;
  PrimeCall (long minPrime) {
    this.minPrime = minPrime;
  }
  public Integer call() {
    // compute and return the number of primes larger than minPrime
    . . .
  }
}
// 建立并啟動線程
PrimeCall p = new PrimeCall (143);
FutureTask<Integer> task = new FutureTask<>(p);
new Thread(task).start();
// 擷取線程的傳回結果
Integer result = task.get();           

繼承 Thread 類和實作 Runnable 接口的差別是,如果一個類已經繼承了另一個類,就不能再繼承 Thread 類,但是可以實作 Runnable 接口。實作 Callable 接口的差別是,它允許線程有傳回值和抛出異常。

如何在兩個線程間共享資料?

在兩個線程間共享資料,有以下幾種常用的方式:

  • 将資料抽象成一個類,并将對這個資料的操作封裝在類的方法中。這種方式隻需要在方法上加synchronized關鍵字即可做到資料的同步。例如,賣票系統就可以用這種方式實作。
  • 将Runnable對象作為一個類的内部類,将共享資料作為這個類的成員變量。這種方式可以讓不同的Runnable對象調用同一個類中的操作共享資料的方法。例如,銀行轉賬系統就可以用這種方式實作。
  • 使用線程間通信的機制,如wait/notify或者BlockingQueue。這種方式可以讓一個線程在等待另一個線程的操作結果,進而實作資料的共享。例如,生産者消費者問題就可以用這種方式實作。

一般來說,共享變量要求變量本身是線程安全的,然後線上程内使用的時候,如果有對共享變量的複合操作,那麼也得保證複合操作的原子性。

什麼是阻塞隊列?阻塞隊列的實作原理是什麼?如何使用阻塞隊列來實作生産者-消費者模型?

1、阻塞隊列(BlockingQueue)是一個支援線程安全的隊列,它可以阻塞生産者線程或消費者線程,直到隊列滿足條件。 2、 阻塞隊列的實作原理是基于鎖和條件變量。當隊列為空時,消費者線程會被阻塞在take()方法上,直到隊列中有元素可用時才會被喚醒。當隊列滿時,生産者線程會被阻塞在put()方法上,直到隊列中有空間可用時才會被喚醒。 3、 阻塞隊列可以用于實作生産者-消費者模型。生産者線程将資料放入隊列,消費者線程從隊列中取出資料。生産者線程和消費者線程可以獨立運作,而不必擔心隊列是否為空或滿。

以下是如何使用阻塞隊列來實作生産者-消費者模型的示例:

// 生産者線程
public class Producer implements Runnable {
    private BlockingQueue<String> queue;

    public Producer(BlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                queue.put("data-" + i);
                System.out.println("producer put data-" + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 消費者線程
public class Consumer implements Runnable {
    private BlockingQueue<String> queue;

    public Consumer(BlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            while (true) {
                String data = queue.take();
                System.out.println("consumer get data-" + data);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}           

以上代碼建立了兩個線程,一個生産者線程和一個消費者線程。生産者線程将資料放入隊列,消費者線程從隊列中取出資料。生産者線程和消費者線程可以獨立運作,而不必擔心隊列是否為空或滿。

運作上述代碼,可以看到生産者線程每隔1秒鐘将一個資料放入隊列,消費者線程每隔1秒鐘從隊列中取出一個資料。

什麼是Callable和Future?

Callable接口和Runnable接口類似,都是用于封裝要在另一個線程上運作的任務,但是Callable接口有以下幾個不同點:

  • Callable接口是一個泛型接口,可以指定傳回值的類型,而Runnable接口的run方法沒有傳回值(void)。
  • Callable接口的call方法可以抛出異常,而Runnable接口的run方法不可以。
  • Callable接口的任務執行後會傳回一個Future對象,表示異步計算的結果,而Runnable接口沒有。

Future接口表示一個異步計算的結果,它提供了一些方法來檢查計算是否完成,等待計算完成,并擷取計算的結果。Future接口有以下幾個主要方法:

  • boolean cancel(boolean mayInterruptIfRunning):取消任務的執行,參數表示是否允許中斷正在執行但未完成的任務。
  • boolean isCancelled():判斷任務是否被取消成功。
  • boolean isDone():判斷任務是否已經完成。
  • V get() throws InterruptedException, ExecutionException:擷取任務的結果,如果任務還沒完成則會阻塞等待。
  • V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException:擷取任務的結果,如果在指定時間内任務還沒完成則會抛出逾時異常。

總之,可以認為Callable和Future是一對組合,Callable用于産生結果,Future用于擷取結果。

示例代碼:

// 建立一個Callable接口的實作類,重寫call方法,傳回一個字元串
class MyCallable implements Callable<String> {
    public String call() throws Exception {
        // 模拟一個耗時操作
        Thread.sleep(3000);
        return "Hello Callable";
    }
}

// 在主方法中,建立一個線程池對象
ExecutorService executor = Executors.newFixedThreadPool(2);

// 建立一個MyCallable對象
MyCallable task = new MyCallable();

// 使用線程池的submit方法送出任務,并接收傳回的Future對象
Future<String> future = executor.submit(task);

// 做一些其他的事情
System.out.println("主線程正在執行...");

// 調用Future的get方法擷取結果,如果任務還沒完成,會阻塞等待
String result = future.get();

// 輸出結果
System.out.println("任務的結果是:" + result);

// 關閉線程池
executor.shutdown();           

解釋一下FutureTask

FutureTask是一個類,它實作了RunnableFuture接口,而RunnableFuture接口繼承了Runnable和Future兩個接口。是以,FutureTask既可以作為一個任務送出給線程池執行,也可以作為一個異步計算的結果傳回給調用者。

FutureTask的構造方法可以接受一個Callable或者一個Runnable和一個結果對象作為參數。如果傳入的是Callable,那麼FutureTask的run方法會調用Callable的call方法,并将傳回值設定為FutureTask的結果;如果傳入的是Runnable和結果對象,那麼FutureTask的run方法會調用Runnable的run方法,并将結果對象設定為FutureTask的結果。

FutureTask可以用來實作一些高效的并發場景,比如:

  • 預加載資料:如果有一個耗時的資料加載操作,可以在程式啟動時建立一個FutureTask來執行這個操作,然後在需要使用資料時調用FutureTask的get方法擷取結果,這樣可以利用空閑時間提前加載資料,提高程式的響應速度。
  • 緩存計算結果:如果有一個複雜的計算操作,而且可能會被多次調用,可以使用一個ConcurrentHashMap來緩存計算結果,鍵為輸入參數,值為FutureTask對象。當有新的輸入參數時,先檢查緩存中是否有對應的FutureTask,如果沒有就建立一個新的FutureTask并放入緩存,然後執行這個FutureTask并傳回結果;如果有就直接傳回這個FutureTask的結果。這樣可以避免重複計算,提高程式的效率。

示例代碼:

// 建立一個Callable接口的實作類,重寫call方法,傳回一個字元串
class MyCallable implements Callable<String> {
    public String call() throws Exception {
        // 模拟一個耗時操作
        Thread.sleep(3000);
        return "Hello Callable";
    }
}

// 在主方法中,建立一個FutureTask對象,傳入MyCallable對象作為參數
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());

// 建立一個線程對象,傳入FutureTask對象作為參數
Thread thread = new Thread(futureTask);

// 啟動線程
thread.start();

// 做一些其他的事情
System.out.println("主線程正在執行...");

// 調用FutureTask的get方法擷取結果,如果任務還沒完成,會阻塞等待
String result = futureTask.get();

// 輸出結果
System.out.println("任務的結果是:" + result);           

如何建立守護線程?

守護線程是一種為其他線程提供服務的線程,當所有非守護線程結束時,守護線程也會自動結束。例如,垃圾回收器就是一個守護線程。

要建立一個守護線程,可以使用Thread類的setDaemon(true)方法将一個普通線程設定為守護線程。這個方法必須在調用start()方法之前調用,否則會抛出IllegalThreadStateException異常。一旦線程開始運作,就不能改變它的守護狀态。

示例代碼如下:

// 建立一個普通線程
Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
        // do something
    }
});

// 将其設定為守護線程
t.setDaemon(true);

// 啟動線程
t.start();           

什麼是Java Timer 類?如何建立一個有特定時間間隔的任務?

java.util.Timer是一個工具類,可以用于安排一個線程在未來的某個特定時間或者按照一定的周期執行某個任務。Timer類可以用來安排一次性任務或者重複性任務。 java.util.TimerTask是一個實作了Runnable接口的抽象類,我們需要繼承這個類并重寫run () 方法來建立我們自己的定時任務,并使用Timer的schedule () 或者 scheduleAtFixedRate () 方法來安排它的執行。 目前有開源的Quartz架構可以用來建立更複雜和靈活的定時任務。

要建立一個有特定時間間隔的任務,我們可以使用以下步驟:

  • 建立一個Timer對象,可以選擇是否将其設定為守護線程。
  • 建立一個TimerTask對象,繼承TimerTask類并重寫run () 方法,定義要執行的任務邏輯。
  • 調用Timer對象的schedule () 或者 scheduleAtFixedRate () 方法,傳入TimerTask對象,指定首次執行時間和時間間隔。

例如,以下代碼建立了一個每隔5秒列印目前時間的任務,并在3秒後開始執行:

import java.util.Timer;
import java.util.TimerTask;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TestTimer {
    public static void main (String [] args) {
        // 建立一個Timer對象,設定為守護線程
        Timer timer = new Timer (true);
        // 建立一個TimerTask對象,繼承TimerTask類并重寫run () 方法
        TimerTask task = new TimerTask () {
            @Override
            public void run () {
                // 定義要執行的任務邏輯,這裡是列印目前時間
                SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss");
                System.out.println ("目前時間:" + sdf.format (new Date ()));
            }
        };
        // 調用Timer對象的schedule () 方法,傳入TimerTask對象,指定首次執行時間和時間間隔
        timer.schedule (task, 3000, 5000);
    }
}           

Java中interrupted 和 isInterrupted方法的差別?

Java中interrupt(), interrupted(), 和 isInterrupted()方法都是線程中斷的工具。它們在處理線程中斷時有着不同的用途。

1、interrupt(): 這是一個執行個體方法,用于打斷線程。當我們調用某個線程的 interrupt() 方法時,會設定該線程的中斷标志為 true。此外,如果線程在 sleep(), wait(), join()等會引起線程阻塞的操作中,那麼将會抛出InterruptedException,中斷狀态會被清除(即置為false)。

2、interrupted(): 這是一個靜态方法,主要用于檢測目前線程是否被打斷。這個方法會傳回目前線程的中斷标志位,并且會清除中斷标志位(即将其設為 false)。這是一個靜态方法,是以通常被用在目前線程中,以檢查目前線程是否被中斷。

3、isInterrupted(): 這是一個執行個體方法,用于檢測線程是否被打斷。與 interrupted() 不同的是,這個方法不會改變線程的中斷标志位。如果、隻想檢查中斷狀态而不改變它,那麼就可以使用這個方法。

這三個方法通常用于處理線程中斷。可能會在處理那些需要長時間運作的線程時,或者當需要提供一個可以取消線程操作的方式時使用這些方法。

下面的例子建立了一個線程,并使用了 interrupt(), interrupted(), 和 isInterrupted() 方法。

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            // 嘗試運作5秒,但可能會被中斷
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread running for " + (i + 1) + " seconds");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("Thread was interrupted during sleep.");
                    // 在捕獲InterruptedException後,需要手動中斷,因為catch InterruptedException會清除中斷标志位
                    Thread.currentThread().interrupt();
                }

                // 檢查是否被中斷
                if (Thread.interrupted()) {
                    System.out.println("Thread was interrupted. Exiting gracefully");
                    return;
                }
            }
        });

        thread.start();

        // 讓主線程睡眠2秒,保證新線程能運作
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 中斷線程
        System.out.println("About to interrupt thread.");
        thread.interrupt();

        // 檢查線程是否被中斷
        System.out.println("Is thread interrupted? : " + thread.isInterrupted());
    }
}           

在這個例子中,我們首先建立了一個線程,并開始運作。然後我們在主線程中 sleep() 2秒鐘,以確定新線程有機會開始運作。然後我們調用 interrupt() 來打斷新線程。最後,我們使用 isInterrupted() 來檢查新線程是否被打斷。

在新線程中,我們使用 sleep() 來模拟一項需要持續一段時間的操作。然後我們捕獲 InterruptedException ,并列印一條消息。然後我們使用 interrupted() 來檢查線程是否被打斷。如果是,我們退出線程。

Java線程池中submit() 和 execute()方法有什麼差別?

兩個方法都可以向線程池送出任務,但是它們之間還是有一些差別的,具體如下:

  • 方法參數的不同:execute()方法的參數是一個實作了Runnable接口的任務,而submit()方法的參數可以是Runnable接口的實作類,也可以是Callable接口的實作類。Callable接口是可以傳回執行結果的,而Runnable接口不能。
  • 方法傳回值的不同:execute()方法沒有傳回值,而submit()方法有傳回值,傳回一個Future對象。Future對象可以用來擷取任務的執行結果。
  • 異常處理的不同:execute()方法在執行任務出現異常時,會直接抛出異常,而submit()方法則會捕獲異常并封裝到Future對象中。我們可以通過調用Future對象的get()方法,來擷取執行過程中的異常。
  • 對線程池的影響不同:execute()方法向線程池送出一個任務,如果線程池中的線程已滿,它會直接抛出RejectedExecutionException異常。submit()方法向線程池送出任務時,如果線程池已滿,會将任務放入阻塞隊列中,等待有空閑的線程時再執行。

綜上,兩種方法适用的場景不同,我們需要根據自己的需求來選擇使用哪個方法。如果需要擷取執行結果或者需要捕獲執行過程中的異常,就使用submit()方法,否則可以使用execute()方法。同時,需要注意線程池的阻塞隊列的大小,以防止任務因為隊列已滿而丢失。

一些示例代碼,來展示submit() 和 execute()的差別:

// 建立一個線程池
ExecutorService pool = Executors.newFixedThreadPool(2);

// 建立一個Runnable任務
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Runnable任務執行中...");
        try {
            Thread.sleep(1000); // 模拟耗時操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Runnable任務執行結束");
    }
};

// 建立一個Callable任務
Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        System.out.println("Callable任務執行中...");
        try {
            Thread.sleep(1000); // 模拟耗時操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Callable任務執行結束");
        return 1 + 1; // 傳回計算結果
    }
};

// 使用execute方法送出Runnable任務,沒有傳回值
pool.execute(runnable);

// 使用submit方法送出Runnable任務,有傳回值,但是為null
Future<?> future1 = pool.submit(runnable);
System.out.println("future1的結果:" + future1.get()); // 輸出null

// 使用submit方法送出Callable任務,有傳回值,可以擷取計算結果
Future<Integer> future2 = pool.submit(callable);
System.out.println("future2的結果:" + future2.get()); // 輸出2

// 關閉線程池
pool.shutdown();           

什麼是代碼重排序?

代碼重排序是指在執行程式時,為了提高性能,編譯器和處理器會對指令進行重新排序的一種優化手段。但是重排序并不是随意的,它需要遵循以下兩個原則:

  • 在單線程環境下不能改變程式運作的結果,這叫做as-if-serial語義;
  • 存在資料依賴關系的不允許重排序,即如果一個操作需要用到另一個操作的結果,那麼這兩個操作的順序不能颠倒。

需要注意的是:重排序不會影響單線程環境的執行結果,但是會破壞多線程的執行語義,導緻出現記憶體可見性問題。

代碼重排序可以分為以下三種情況:

  • 編譯器重排序:編譯器在不改變單線程程式語義的前提下,可以調整代碼語句的順序,以提高執行效率。例如,把對同一個變量的多次操作放到一起,減少寄存器的讀寫次數。
  • 指令重排序:處理器在執行時會利用指令級并行技術來重疊執行多條指令,如果不存在資料依賴性,處理器可以改變指令的執行順序,以提高吞吐量。例如,在等待記憶體讀取資料的時候,處理器可以先執行其他已準備好的指令。
  • 記憶體系統重排序:由于處理器有讀、寫緩存區,寫緩存區沒有及時重新整理到記憶體,造成其他處理器讀到的值不是最新的,使得處理器執行的讀寫操作與記憶體上反映出的順序不一緻。例如,在多線程環境下,一個線程修改了一個共享變量的值,但是另一個線程看不到這個修改,因為修改後的值還在寫緩存區中沒有同步到主存。

為了防止重排序帶來的問題,我們需要使用一些手段來禁止或限制重排序:

  • 阻止編譯器重排序:使用volatile關鍵字來修飾共享變量,告訴編譯器不要對這個變量進行重排序或優化。
  • 阻止指令重排序和記憶體系統重排序:使用記憶體屏障或Lock字首指令來強制重新整理緩存區,并保證指令按照順序執行。

為什麼我們調用start()方法時會執行run()方法,為什麼我們不能直接調用run()方法?

當、調用start()方法時,、将建立并啟動一個新的線程,并且執行在run()方法裡的代碼。這時,線程處于就緒狀态,一旦得到CPU時間片,就開始執行run()方法,這裡的run()方法稱為線程體,它包含了要執行的這個線程的内容。run()方法運作結束,此線程随即終止。

但是如果、直接調用run()方法,它不會建立并啟動新的線程,也不會執行在就緒狀态等待CPU排程,而是在主線程中直接執行run()方法裡的代碼。這樣就相當于把run()方法當作普通方法去執行,并沒有實作多線程的效果。

總之,調用start()方法是為了實作多線程的優點,讓多個線程并發地執行。而調用run()方法隻是在主線程中順序地執行,并沒有達到寫線程的目的。

普通線程(非守護線程)和守護線程的差別

在Java中,普通線程(非守護線程)和守護線程的主要差別在于他們在程式結束時的行為。

普通線程: 普通線程是程式的主工作線程。當程式中隻剩下守護線程,而沒有任何活動的非守護線程時,JVM就會結束這個程式。也就是說,如果任何使用者線程還在運作,JVM就不會停止。

守護線程: 守護線程主要用作支援性工作,提供服務給其他(使用者)線程。例如,垃圾收集線程就是一種典型的守護線程。它的主要作用是在背景清理不再使用的記憶體,以供程式中的其他線程使用。當程式中所有的非守護線程都結束時,JVM會認為不再需要守護線程提供服務,于是JVM也就結束了,同時所有的守護線程也會随之被終止。

要注意的是,在建立線程的時候可以使用 Thread 類的 setDaemon(true) 方法将其設定為守護線程,但這必須在 start() 方法被調用之前完成。另外,預設情況下,線程繼承其父線程的 "守護狀态",也就是說,如果父線程是守護線程,那麼子線程也是守護線程。

在Java中CyclicBarrier和CountdownLatch有什麼差別?

CyclicBarrier和CountdownLatch都是并發包中提供的同步輔助類,它們可以用來協調多個線程之間的執行。但是它們有以下幾點差別:

  • CyclicBarrier可以重複使用,而CountdownLatch不能重複使用。CyclicBarrier在釋放等待線程後會自動重置計數器,是以可以循環使用。CountdownLatch的計數器隻能使用一次,除非建立新的執行個體。
  • CyclicBarrier的基本操作是await,當所有線程都調用了await方法後,會觸發一個可選的屏障動作(barrier action),然後釋放所有等待線程。CountdownLatch的基本操作是countDown和await,任何線程都可以調用countDown方法使計數器減一,而隻有主線程才能調用await方法等待計數器歸零。
  • CyclicBarrier側重于多個線程之間的互相等待,要求所有線程同時到達屏障點。CountdownLatch側重于一個或多個線程等待其他一組線程完成操作,不要求所有線程同時到達。

舉例來說,CyclicBarrier适合用于多個線程之間需要協作完成某個任務的場景,比如多人遊戲中所有玩家都準備好後才開始遊戲。CountdownLatch适合用于一個或多個線程需要等待其他一組線程完成某些操作後才繼續執行的場景,比如主線程需要等待子線程完成查詢訂單和派送單後才進行對賬。

下面是一些CyclicBarrier和CountdownLatch的代碼示例:

CyclicBarrier的代碼示例:

//建立一個CyclicBarrier,指定屏障點為5個線程,并設定一個屏障動作
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
    System.out.println("所有線程都到達屏障點,開始執行屏障動作");
});

//建立5個線程,模拟5個玩家準備遊戲
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        try {
            System.out.println(Thread.currentThread().getName() + "正在準備遊戲");
            Thread.sleep((long) (Math.random() * 10000)); //模拟準備時間
            System.out.println(Thread.currentThread().getName() + "準備好了,等待其他玩家");
            cyclicBarrier.await(); //等待其他線程到達屏障點
            System.out.println(Thread.currentThread().getName() + "開始遊戲");
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }).start();
}           

CountdownLatch的代碼示例:

//建立一個CountDownLatch,指定計數器為2
CountDownLatch countDownLatch = new CountDownLatch(2);

//建立一個主線程,模拟對賬操作
new Thread(() -> {
    try {
        System.out.println("主線程等待子線程完成查詢操作");
        countDownLatch.await(); //等待計數器歸零
        System.out.println("主線程開始對賬操作");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

//建立兩個子線程,分别模拟查詢訂單和派送單的操作
new Thread(() -> {
    try {
        System.out.println("子線程1查詢訂單");
        Thread.sleep(5000); //模拟查詢時間
        System.out.println("子線程1查詢訂單完成");
        countDownLatch.countDown(); //計數器減一
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

new Thread(() -> {
    try {
        System.out.println("子線程2查詢派送單");
        Thread.sleep(3000); //模拟查詢時間
        System.out.println("子線程2查詢派送單完成");
        countDownLatch.countDown(); //計數器減一
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();           

什麼是不可變對象,它對寫并發應用有什麼幫助?

不可變對象(Immutable Objects)是指對象一旦被建立,它的狀态(對象的資料,也即對象屬性值)就不能改變,反之即為可變對象(Mutable Objects)。

不可變對象的類即為不可變類(Immutable Class)。Java平台類庫中包含許多不可變類,如String、基本類型的包裝類、BigInteger和BigDecimal等。

不可變對象有很多優點:

  • 不可變對象天生是線程安全的。它們的常量(域)是在構造函數中建立的。既然它們的狀态無法修改,這些常量永遠不會變。這樣就避免了多線程并發通路時的同步問題。
  • 不可變對象可以增強語義,提高代碼可讀性。閱讀代碼時,一看就能把常量和隻讀變量和其他可變變量差別開。
  • 不可變對象可以作為Map的鍵和Set的元素,因為它們通常不會在建立後改變。
  • 不可變對象可以使得編寫、使用和推理代碼更容易。類不變式是在建立後就建立并保持不變的。
  • 不可變對象可以使得并行化程式更容易,因為它們之間沒有沖突。
  • 不可變對象可以保持程式的内部狀态一緻,即使有異常發生。
  • 不可變對象的引用可以被緩存,因為它們不會改變。(例如,在哈希表中提供快速操作)。

不可變對象的缺點是:

  • 每次需要修改對象的值時,都必須建立一個新的對象。這會增加對象建立的開銷,以及垃圾回收的負擔。
  • 如果對象包含了對其他可變對象的引用,那麼這些引用可能會被修改,進而破壞了不可變性。

要建立一個不可變類,需要滿足以下條件:

  • 類本身被聲明為final,不能被繼承。
  • 所有的域都被聲明為final和private,不能被修改和通路。
  • 對于任何可變元件的通路,都要通過防禦性複制來實作。
  • 在構造函數中不要洩露this引用,避免被其他線程提前通路。

什麼是線程組,為什麼在Java中不推薦使用?

線程組是一個線程的集合,它可以對一批線程進行分類管理。Java中使用ThreadGroup類來表示線程組,它提供了一些方法來設定線程組的屬性,擷取線程組的資訊,處理線程組内的未捕獲異常等。線程組是以樹形結構存在的,每個線程都屬于一個線程組,每個線程組又有一個父線程組,直到根線程組system。預設情況下,所有的線程都屬于主線程組main。

線程組和線程池是兩個不同的概念,他們的作用完全不同。線程組是為了友善線程的管理,而線程池是為了管理線程的生命周期,複用線程,減少建立銷毀線程的開銷。在Java中不推薦使用線程組的原因有以下幾點:

  • 線程組的API設計不完善,有些方法已經被廢棄,有些方法沒有實作預期的功能。
  • 線程組不能阻止非法通路,一個線程可以通路和修改其他線程組中的任何線程。
  • 線程組沒有提供額外的功能,可以用其他方式替代。例如,可以用Thread類的setUncaughtExceptionHandler方法來處理未捕獲異常,可以用Executor架構來管理和複用線程。

綜上所述,線程組是一個過時且低效的概念,在Java中不建議使用。

什麼是Daemon線程?它有什麼意義?

Daemon線程,也叫守護線程,是一種在程式運作時在背景提供一種通用服務的線程,它并不屬于程式中不可或缺的部分。是以,當所有的非Daemon線程結束時,程式也就終止了,同時會殺死程序中的所有Daemon線程。

反過來說,隻要有任何非Daemon線程還在運作,程式就不會終止。必須線上程啟動之前調用setDaemon(true)方法,才能把它設定為Daemon線程。注意:Daemon線程在不執行finally子句的情況下就會終止其run()方法。

Daemon線程的作用是為其他線程提供便利服務,比如垃圾回收線程、Finalizer線程、定時器線程等。它們通常具有較低的優先級,并且不需要使用者的互動。它們可以在背景執行一些周期性的任務,或者等待一些特定的事件發生。

Daemon線程的優點是可以減輕主線程的負擔,提高程式的性能和穩定性。它們的缺點是不能保證執行完整内容,可能會導緻一些資源無法釋放或清理。是以,在使用Daemon線程時要注意以下幾點:

  • 不要依賴Daemon線程來完成重要的或需要持久化的工作。
  • 不要在Daemon線程中操作可能會被中斷的資源,比如檔案、資料庫等。
  • 在退出程式之前,盡量讓Daemon線程完成目前的任務或者儲存狀态。

如何使用thread dump?、将如何分析Thread dump?

Thread dump 是一種用于分析 Java 應用程式的工具,可以在指令行中輸入 jstack 指令,然後指定應用程式的程序 ID。 例如,jstack 1234。 這将生成一個包含所有線程狀态資訊的文本檔案,可以使用文本編輯器或其他工具進行分析。

Thread dump 可以幫助我們診斷 Java 應用程式的性能問題,如記憶體洩漏,死鎖,線程競争等。 Thread dump 的資訊包括線程的名稱,類型,優先級,狀态,堆棧跟蹤等。 我們可以根據線程的狀态和堆棧跟蹤來判斷線程在做什麼,是否有資源争用或等待,是否有死循環或遞歸調用等。

分析 Thread dump 的方法有很多,一般需要結合應用程式的業務邏輯和代碼來進行。 一些常用的步驟和技巧如下:

  • 首先檢視線程的總數和狀态分布,看是否有異常或不正常的情況,如線程數過多,阻塞或等待的線程過多等。
  • 然後檢視各個線程的堆棧跟蹤,找出熱點線程或關鍵線程,看它們在執行什麼方法,是否有耗時或阻塞的操作,是否有鎖競争或死鎖的情況等。
  • 接着比較多次抓取的 Thread dump ,看線程的狀态和堆棧是否有變化,是否有進展或停滞的現象,是否有重複或相似的模式等。
  • 最後根據分析結果,定位問題的原因和解決方案,如優化代碼邏輯,調整參數配置,增加資源等。

java如何實作多線程之間的通訊和協作?

java提供了多種方式實作多線程之間的通訊和協作,包括:

  • wait ()、notify () 和 notifyAll () 方法:這些方法是 Object 類中的方法,可以用于實作線程之間的通訊和協作。wait () 方法會使目前線程進入等待狀态,直到其他線程調用 notify () 或 notifyAll () 方法喚醒它;notify () 方法會随機喚醒一個等待的線程,而 notifyAll () 方法會喚醒所有等待的線程。
  • Lock 和 Condition 接口:Lock 接口提供了比 synchronized 更加靈活的線程同步機制,它可以實作更細粒度的鎖定和更高效的并發控制。Condition 接口則提供了類似 wait () 和 notify () 的方法,可以用于實作線程之間的通訊和協作。
  • CountDownLatch 類:CountDownLatch 類可以用于實作線程之間的協作,它允許一個或多個線程等待其他線程完成某些操作後再繼續執行。當計數器減為 0 時,所有等待的線程都會被喚醒。
  • CyclicBarrier 類:CyclicBarrier 類也可以用于實作線程之間的協作,它允許一組線程互相等待,直到所有線程都到達某個屏障點後再繼續執行。當所有線程都到達屏障點後,屏障會自動解除,所有線程可以繼續執行。
  • Semaphore 類:Semaphore 類可以用于控制同時通路某個資源的線程數量,它可以限制同時通路某個資源的線程數量,進而避免資源競争和死鎖問題。Semaphore 類的 acquire () 方法可以用于擷取資源,release () 方法可以用于釋放資源。

除了以上方法外,java還支援以下方式實作多線程之間的通訊和協作:

  • 中斷和共享變量:中斷是一種簡單但低效的方式,它可以使一個線程向另一個線程發送一個信号,表示要求它停止正在做的事情。共享變量是一種常見但容易出錯的方式,它可以使多個線程通過讀寫一個共享資料來實作通信,但需要保證資料的可見性和原子性。
  • 管道流:管道流是一種基于位元組流或字元流的通信方式,它可以連接配接兩個運作在同一個JVM中的線程,使一個線程向另一個線程發送資料。管道流有兩種類型:PipedInputStream 和 PipedOutputStream(位元組流),PipedReader 和 PipedWriter(字元流)。
  • 消息隊列:消息隊列是一種基于消息傳遞的通信方式,它可以連接配接運作在同一個或不同JVM中的多個線程或程序,使它們通過發送和接收消息來實作通信。消息隊列有多種實作方式,如 JMS、ActiveMQ、RabbitMQ 等。

一個線程運作時發生異常會怎樣?

如果一個線程運作時發生了未捕獲的異常,它将會終止執行,并釋放它占用的所有資源。為了處理這種情況,Java提供了一個内嵌接口Thread.UncaughtExceptionHandler,它可以線上程因為未捕獲的異常而終止時被調用。當這種情況發生時,JVM會先調用Thread.getUncaughtExceptionHandler()方法,擷取線程的UncaughtExceptionHandler對象,然後将線程對象和異常對象作為參數傳遞給該對象的uncaughtException()方法,由它來處理異常。

Thread類中的yield方法有什麼作用?

yield是一個靜态方法,可以在任何地方被調用,而不僅僅局限于Thread類或者它的子類。yield可以讓目前線程主動放棄CPU的使用權,暫停自己的執行,并轉入就緒狀态,等待再次被排程。yield隻是影響了具有相同優先級的線程,對于高優先級或者低優先級的線程并沒有影響。

重要的是明白,yield方法隻是一個"建議"給線程排程器,表示目前線程願意放棄目前的CPU使用權。但是線程排程器可以完全忽略這個"建議"。具體的行為取決于JVM實作以及作業系統。另外,yield不會導緻線程阻塞,即線程在調用yield之後,還是處于可運作狀态,是以在某些情況下,可能會發生這樣的情況:線程執行了yield方法後,線程排程器又将其排程出來重新執行。

在多線程程式設計中,yield方法常常被用來調試,它可以幫助我們了解和示範線程排程器的工作原理。然而,yield并不常被用在實際的生産環境,因為它的行為在不同的JVM和作業系統下可能會有很大的差異,這就使得它在跨平台的Java應用中變得難以預測和控制。更常見的做法是使用更進階的線程控制結構,如synchronized關鍵字和wait/notify機制,以及java.util.concurrent包中的工具類。

Java中Semaphore是什麼?

Java中的Semaphore是一種同步類,它可以用來控制對某些資源的并發通路。從概念上講,信号量維護了一組許可證。每個線程在通路資源之前,需要調用acquire()方法來擷取一個許可證,如果沒有可用的許可證,線程會被阻塞,直到有其他線程釋放許可證。每個線程在使用完資源之後,需要調用release()方法來歸還一個許可證,進而可能喚醒等待的線程。但是,信号量并不使用實際的許可證對象,它隻是對可用許可證的數量進行計數,并根據計數值來執行相應的操作。

信号量常常用于限制可以同時通路某個(實體或邏輯)資源的線程數量。例如,下面是一個使用信号量來控制使用者登入數量的類:

class LoginQueueUsingSemaphore {
  private Semaphore semaphore; // 定義一個信号量對象

  public LoginQueueUsingSemaphore(int slotLimit) { // 構造方法,指定最大的登入數量
    semaphore = new Semaphore(slotLimit); // 初始化信号量
  }

  boolean tryLogin() { // 嘗試登入的方法
    return semaphore.tryAcquire(); // 調用信号量的tryAcquire()方法,如果有可用的許可證,傳回true并擷取該許可證,否則傳回false
  }

  void logout() { // 登出的方法
    semaphore.release(); // 調用信号量的release()方法,歸還一個許可證
  }

  int availableSlots() { // 擷取目前可用的登入數量
    return semaphore.availablePermits(); // 調用信号量的availablePermits()方法,傳回目前可用的許可證數量
  }
}           

除了tryAcquire()方法外,信号量還提供了其他幾種擷取許可證的方法:

  • acquire():擷取一個許可證,如果沒有可用的許可證,會一直阻塞直到有其他線程釋放許可證。
  • acquire(int permits):擷取指定數量的許可證,如果沒有足夠的可用許可證,會一直阻塞直到有其他線程釋放足夠數量的許可證。
  • acquireUninterruptibly():擷取一個許可證,如果沒有可用的許可證,會一直阻塞直到有其他線程釋放許可證,并且不響應中斷。
  • acquireUninterruptibly(int permits):擷取指定數量的許可證,如果沒有足夠的可用許可證,會一直阻塞直到有其他線程釋放足夠數量的許可證,并且不響應中斷。
  • tryAcquire(long timeout, TimeUnit unit):嘗試在指定時間内擷取一個許可證,如果在指定時間内有其他線程釋放了一個許可證,則傳回true并擷取該許可證;否則傳回false。
  • tryAcquire(int permits, long timeout, TimeUnit unit):嘗試在指定時間内擷取指定數量的許可證,如果在指定時間内有其他線程釋放了足夠數量的許可證,則傳回true并擷取這些許可證;否則傳回false。

當初始化信号量時,可以選擇傳入一個公平性參數。當設定為true時,信号量會保證按照線程請求許可證的順序來配置設定許可證,進而避免線程饑餓。當設定為false時,信号量不會保證任何順序,可能會導緻某些線程優先擷取許可證,而某些線程長時間等待。

信号量還可以用來實作互斥鎖的功能。如果初始化信号量時指定隻有一個許可證,那麼隻有一個線程可以擷取該許可證,進而實作對資源的獨占通路。這種情況下的信号量也稱為二進制信号量,因為它隻有兩種狀态:有一個許可證或沒有許可證。與鎖不同的是,二進制信号量可以被任何線程釋放,而不一定是擷取它的線程(因為信号量沒有所有權的概念)。這在一些特殊的場景下可能有用,例如死鎖恢複。

什麼是阻塞式方法?

阻塞式方法是指程式在調用某個方法時,必須等待該方法執行完畢并傳回結果,期間不能執行其他任務。例如,ServerSocket的accept()方法會一直等待用戶端的連接配接請求,直到建立連接配接或者逾時為止。阻塞式方法的特點是在得到結果之前,目前線程會被挂起,無法響應其他事件。與之相對的是異步和非阻塞式方法,它們可以在任務完成之前就傳回,不會影響線程的執行。

如何讓正在運作的線程暫停一段時間?

我們可以使用Thread類的sleep()方法讓線程暫停一段時間。這個方法有兩個重載版本,一個接受一個參數,表示以毫秒為機關的暫停時間;另一個接受兩個參數,表示以毫秒和納秒為機關的暫停時間。需要注意的是,這兩個參數都不能為負數,否則會抛出IllegalArgumentException。調用sleep()方法并不會讓線程終止,而是讓線程進入阻塞狀态,等待指定的時間後,線程的狀态将會被改變為就緒狀态,并且根據線程排程器的安排,它将得到執行。但是,實際的暫停時間并不一定精确等于指定的時間,因為它受到作業系統的線程排程器和系統定時器的影響。另外,任何其他線程都可以中斷正在睡眠的線程,這時會抛出InterruptedException。調用sleep()方法時,線程不會釋放任何已經持有的螢幕或鎖。

如何確定main()方法所在的線程是Java 程式最後結束的線程?

我們可以使用Thread類的join()方法來確定所有程式建立的線程在main()方法退出前結束。join()方法的作用是讓調用該方法的線程(例如主線程)等待被調用的線程(例如子線程)執行完畢後再繼續執行。這樣,我們可以在main()方法中對所有建立的子線程調用join()方法,使得主線程在所有子線程都結束後才退出。例如:

public class JoinDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t1 is running");
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t2 is running");
            }
        });
        t1.start();
        t2.start();
        try {
            // 主線程等待t1和t2執行完畢
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main thread is finished");
    }
}           

輸出結果:

t1 is running
t2 is running
main thread is finished           

如果沒有調用join()方法,主線程可能會在子線程執行完畢之前就退出,導緻程式不完整或不正确。

談談什麼是零拷貝?

零拷貝(Zero-copy)是一種計算機程式設計技術,它的主要目标是減少資料複制的數量,進而提高系統性能和效率。當資料需要從一個系統部件傳遞到另一個部件(例如從硬碟到記憶體,或從記憶體到網絡)時,如果沒有使用零拷貝技術,資料可能需要被多次複制。這種資料複制會消耗CPU資源和記憶體帶寬,并可能影響到程式的性能。零拷貝技術通過避免資料複制來解決這個問題。

以下是零拷貝的三種主要實作方式:

1、直接記憶體通路(DMA): DMA是一種讓硬體裝置直接通路記憶體的技術,不需要通過CPU。硬體裝置可以直接讀寫記憶體,進而避免了資料複制。這是一種常見的在硬體層面實作零拷貝的技術。

2、記憶體映射(Memory-mapped IO): 通過将檔案或其他資源映射到程序的位址空間,可以避免讀或寫操作的資料複制。這是一種在作業系統層面實作零拷貝的技術。

3、發送檔案(sendfile): sendfile是Linux和其他一些作業系統提供的一個系統調用,它可以在核心中直接将資料從一個檔案描述符傳送到另一個檔案描述符,避免了資料複制。這是一種在系統調用層面實作零拷貝的技術。

這些技術在許多應用中都得到了廣泛應用,如檔案系統、網絡協定棧和資料庫等。使用零拷貝可以顯著提高系統的性能和效率。

Future 實作阻塞等待擷取結果的原理是什麼?

java.util.concurrent.Future接口在Java中提供了異步計算的結果的方式。Future任務在送出到ExecutorService時将啟動新線程運作任務,任務完成後,、可以使用Future的get()方法來擷取結果。如果計算尚未完成,則get()方法将阻塞直到結果可用。

那麼,Future是如何實作這種阻塞等待擷取結果的原理呢?我們可以從java.util.concurrent.FutureTask的實作中來了解這個問題。FutureTask是Future的一個基礎實作類,它實作了Runnable和Future接口,是以它既可以作為Runnable被線程執行,也可以作為Future擷取計算結果。

1、FutureTask的實作:

在FutureTask中,有一個内部類Sync,這個類繼承了AbstractQueuedSynchronizer,這是一個用來建構鎖和同步元件的架構,Sync使用AbstractQueuedSynchronizer的acquireSharedInterruptibly(int)方法在任務沒有完成時,讓線程等待,任務完成後再喚醒線程。

2、源碼解析:

主要關注FutureTask中的get()方法和set()方法。

get()方法主要是通過Sync的acquireSharedInterruptibly(int)方法實作阻塞,具體源碼如下:

public V get() throws InterruptedException, ExecutionException {
    int s = sync.innerGetState();
    if (s <= COMPLETING)
        s = sync.acquireSharedInterruptibly(0);
    return sync.innerGet();
}           

set()方法主要是用來設定計算結果并喚醒等待的線程,源碼如下:

protected void set(V v) {
    sync.innerSet(v);
}           

在Sync内部類中:

protected int tryAcquireShared(int ignore) {
    return innerIsDone() ? 1 : -1;
}

protected boolean tryReleaseShared(int ignore) {
    setState(RUNNING);
    return true;
}           

在tryAcquireShared(int)方法中,如果任務已經完成(innerIsDone()傳回true),那麼方法傳回1,否則傳回-1。傳回1表示目前共享鎖已經釋放,傳回-1則表示共享鎖尚未釋放,線程需要等待。

在任務計算完成後,set()方法會被調用,此時會調用tryReleaseShared(int)方法來喚醒因調用get()方法而在等待的線程。

這樣,就實作了Future的阻塞等待擷取結果的功能。

B+樹聊一下?B+樹是不是有序?B+樹和 B-樹的主要差別?B+樹索引,一次查找過程?

B+樹是一個n叉樹,每個節點可以有多于2個子節點。它是一種自平衡的搜尋樹,用于存儲排序資料并進行高效插入、删除和搜尋操作。這使得B+樹成為各種資料庫和檔案系統的首選資料結構。

是的,B+樹是有序的。具體來說,所有的值都存儲在葉子節點,并按照排序順序連結。這使得B+樹非常适合對大範圍的資料進行順序通路。

B+樹與B-樹的主要差別在于:

1、在B+樹中,所有記錄節點都是葉子節點,而在B-樹中,記錄可以存儲在任何節點中。 2、在B+樹中,所有葉子節點都通過指針連接配接,這使得範圍查詢更高效。然而,在B-樹中,葉子節點并不互相連結。 3、在B+樹中,每個非葉子節點的子節點數等于關鍵字數,而在B-樹中,子節點數等于關鍵字數+1。

B+樹索引的一次查找過程如下:

1、首先,從根節點開始。 2、使用二分搜尋或線性搜尋找到關鍵字所在的區間。 3、進入相應的子節點,然後在這個子節點中重複步驟2。 4、繼續這個過程,直到到達葉子節點。在葉子節點中,查找最終的關鍵字。 5、如果找到了關鍵字,那麼傳回它的關聯值。否則,關鍵字不在樹中。

這種查找過程是非常高效的,因為B+樹的高度通常比較低(尤其是相比二叉搜尋樹),是以步驟數通常非常少。

Runnable接口和Callable接口的差別

Runnable和Callable接口在Java中都被用來實作線程,但是它們有以下主要的差別:

1、傳回值:Runnable接口的run()方法沒有傳回值,而Callable接口的call()方法則是有傳回值的。這就意味着如果、在執行線程時需要擷取一些計算結果,、應該使用Callable接口。

2、異常處理:Runnable.run()方法不能抛出任何受檢異常,隻能抛出非受檢異常。然而,Callable.call()方法可以抛出任何類型的異常。

3、使用場景:Runnable接口通常與Thread類一起使用,而Callable通常與ExecutorService一起使用,可以傳回一個Future對象以處理異步計算的結果。

4、功能性:由于Callable接口可以傳回結果,并且可以抛出異常,是以Callable接口比Runnable接口更為強大和靈活。

總的來說,Callable接口提供了比Runnable接口更強大和靈活的線程執行機制。然而,如果、不需要傳回值,并且不需要處理受檢異常,那麼使用Runnable接口就足夠了。

Linux環境下如何查找哪個線程使用CPU最長?

在Linux環境下,查找哪個線程使用CPU最長的步驟如下:

1、使用ps或者jps指令來擷取Java程序的pid。例如:

ps -ef | grep java           

或者

jps           

2、使用top -H -p pid指令來顯示該程序的所有線程及其CPU使用率。這裡的pid是在上一步驟中獲得的Java程序的pid。例如,如果Java程序的pid是1234,、可以運作:

top -H -p 1234           

在這個指令的輸出中,、可以看到哪個線程的CPU使用率最高。這個指令顯示的線程ID是作業系統級别的線程ID,也就是LWP(Light-Weight Process,輕量級程序)。

3、如果、需要查找這個線程在Java内的線程ID,、需要将LWP的十進制值轉換成十六進制,因為Java的線程dump中的線程ID是十六進制表示的。你可以使用printf指令來進行這個轉換。例如,如果LWP是12345678,你可以運作:

printf "%x\n" 12345678           

這個指令會輸出該數字的十六進制表示。

4、然後,你可以擷取Java的線程dump來查找該線程在Java中的堆棧跟蹤。你可以使用jstack指令來擷取線程dump。例如,如果Java程序的pid是1234,你可以運作:

jstack 1234           

在這個指令的輸出中,你可以使用上一步驟中獲得的十六進制線程ID來查找對應的線程。

這個過程會幫助你找到使用CPU最長的線程,并且擷取該線程在Java中的堆棧跟蹤,這樣你可以進一步分析該線程的行為,例如查找是否有死循環或者其他導緻CPU使用高的原因。

什麼是阻塞隊列?阻塞隊列常用的應用場景?

阻塞隊列是一種線程安全的隊列,它在資料結構中的操作是阻塞的,即當隊列為空的時候,如果有線程試圖從隊列中取出元素,那麼這個線程将會被阻塞,直到隊列中有新的元素可取。同樣的,當隊列已滿的時候,如果有線程試圖向隊列中添加元素,那麼這個線程将會被阻塞,直到隊列中有空餘的位置。通過這種方式,阻塞隊列可以在多線程環境中安全有效地處理并發資料。

阻塞隊列的應用場景廣泛,常用的包括生産者消費者模型、線程池等。生産者消費者模型,這是一個經典的并發程式設計模型,生産者負責生成資料放入隊列,而消費者則負責從隊列中取出資料進行處理。阻塞隊列在這裡起到了緩沖的作用,能夠在生産者和消費者之間提供一個平衡,防止生産者生成資料的速度過快或者消費者處理資料的速度過慢導緻資料丢失或者過載。

另一個常見的應用場景是線程池,線程池使用阻塞隊列來存儲待執行的任務。線程池中的線程從阻塞隊列中取出任務來執行,當隊列為空時,線程會阻塞等待,直到隊列中有新的任務。同樣地,如果隊列已滿,送出新任務的線程會阻塞,直到隊列中有空位。

還有其他一些應用場景,比如在網絡程式設計中,阻塞隊列常用于存放待發送或待接收的消息等等。總的來說,阻塞隊列在處理并發問題時是一個非常有用的工具。

有三個線程T1,T2,T3,如何保證順序執行?

線程的 join() 方法确實能夠用于保證線程的順序執行。實際上,join() 方法的作用就是讓調用這個方法的線程等待,直到該方法所屬的線程完成執行,才能繼續執行調用者線程。

下面是一個示例的Java代碼,該代碼将會確定 T1, T2, 和 T3 線程按順序執行。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread 1 is running.");
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread 2 is running.");
            }
        });

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread 3 is running.");
            }
        });

        t1.start();
        t1.join();

        t2.start();
        t2.join();

        t3.start();
        t3.join();
    }
}           

在這個示例中,我們先啟動了線程T1,并且在主線程中調用了 t1.join(),讓主線程等待T1線程執行完成。同樣的方法也被應用到了T2和T3線程上,進而確定了線程的順序執行。

然而需要注意的是,這種方法雖然能保證線程的執行順序,但是同時也限制了線程的并發執行,是以可能無法充分利用多核心的優勢。是以,除非你有特殊的需求,否則通常我們更傾向于讓線程并行執行。

如何停止一個正在運作的線程?

1)使用退出标志,使線程正常退出,也就是當run方法完成後線程終止。這種方法是最友好,最安全的方法。例如,你可以線上程類裡設定一個volatile類型的boolean變量,通過改變這個變量的值來控制線程的運作和停止。

public class MyRunnable implements Runnable {

    private volatile boolean exit = false;

    public void run() {
        while(!exit) {
            // your code
        }
    }

    public void stop() {
        exit = true;
    }
}           

2)使用stop方法強行終止,但是不推薦這個方法,因為stop和suspend及resume一樣都是過期廢棄的方法。這些方法被棄用是因為它們是不安全的,可能導緻對象處于不一緻的狀态。

3)使用interrupt方法中斷線程。當線程處于阻塞狀态時(如等待,睡眠,或者輸入/輸出操作),這種方法很有效。這會抛出InterruptedException,然後你可以在catch塊中結束線程。但是如果線程目前并未處于阻塞狀态,那麼該中斷信号就會被忽略,是以需要你自己在代碼中周期性的檢查線程是否被中斷,例如使用Thread.interrupted()。

public class MyRunnable implements Runnable {

    public void run() {
        while(!Thread.interrupted()) {
            // your code
        }
    }
}

// elsewhere in code
MyRunnable myRunnable = new MyRunnable();
Thread myThread = new Thread(myRunnable);
myThread.start();

// when you want to stop the thread
myThread.interrupt();           

4)使用Thread的destroy()方法。但是這個方法在Java中并未實作,并且可能會被Oracle在未來的JDK版本中删除。是以不建議使用。

總的來說,最好的方式是設計線程能夠響應中斷,或者通過一個退出标志自行結束。

40道Java并發程式設計高頻面試題,收藏等于精通

繼續閱讀