面試官不厭其煩地問:如何開啓一個線程,開啓大量線程會有什麼問題,如何優化?

這裏整理了最近BAT最新面試題,2020船新版本!!需要的朋友可以點擊:這個,點這個!!,備註:簡書。希望那些有需要朋友能在今年第一波招聘潮找到一個自己滿意順心的工作!

作者:享學學員:孫學海

這道題想考察什麼?

  1. 是否瞭解線程開啓的方式?
  2. 開啓大量線程會引起什麼問題?爲什麼?怎麼優化?

考察的知識點

  1. 線程的開啓方式
  2. 開啓大量線程的問題
  3. 線程池

考生應該如何回答

1、首先,關於如何開啓一個線程,大多數人可能都會說3種,Thread、Runnable、Callback嘛!但事實卻不是這樣的。 看JDK裏怎麼說的。

/**
 * ...
 * There are two ways to create a new thread of execution. One is to
 * declare a class to be a subclass of <code>Thread</code>. 
 * The other way to create a thread is to declare a class that
 * implements the <code>Runnable</code> interface.
 * ....
 */
public class Thread implements Runnable{
      
}

Thread源碼的類描述中有這樣一段,翻譯一下,只有兩種方法去創建一個執行線程,一種是聲明一個Thread的子類,另一種是創建一個類去實現Runnable接口。驚不驚訝,並沒有提到Callback。

  • 繼承Thread類
public class ThreadUnitTest {

    @Test
    public void testThread() {
        //創建MyThread實例
        MyThread myThread = new MyThread();
        //調用線程start的方法,進入可執行狀態
        myThread.start();
    }

    //繼承Thread類,重寫內部run方法
    static class MyThread extends Thread {

        @Override
        public void run() {
            System.out.println("test MyThread run");
        }
    }
}
  • 實現Runnable接口
public class ThreadUnitTest {

    @Test
    public void testRunnable() {
        //創建MyRunnable實例,這其實只是一個任務,並不是線程
        MyRunnable myRunnable = new MyRunnable();
        //交給線程去執行
        new Thread(myRunnable).start();
    }

    //實現Runnable接口,並實現內部run方法
    static class MyRunnable implements Runnable {

        @Override
        public void run() {
            System.out.println("test MyRunnable run");
        }
    }
}
  • 還是看看Callable是怎麼回事吧。
public class ThreadUnitTest {

    @Test
    public void testCallable() {
        //創建MyCallable實例,需要與FutureTask結合使用
        MyCallable myCallable = new MyCallable();
        //創建FutureTask,與Runnable一樣,也只能算是個任務
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        //交給線程去執行
        new Thread(futureTask).start();

        try {
            //get方法獲取任務返回值,該方法是阻塞的
            String result = futureTask.get();
            System.out.println(result);
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //實現Callable接口,並實現call方法,不同之處是該方法有返回值
    static class MyCallable implements Callable<String> {

        @Override
        public String call() throws Exception {
            Thread.sleep(10000);
            return "test MyCallable run";
        }
    }
}

Callable的方式必須與FutureTask結合使用,我們看看FutureTask的繼承關係。

//FutureTask實現了RunnableFuture接口
public class FutureTask<V> implements RunnableFuture<V> {

}

//RunnableFuture接口繼承Runnable和Future接口
public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

真相大白了,其實實現Callback接口創建線程的方式,歸根到底就是Runnable方式,只不過它是在Runnable的基礎上又增加了一些能力,例如取消任務執行等。

2、開啓線程的幾種方式算是答上了,那開啓大量線程,或者說頻繁開啓線程到底會引起什麼問題呢? 衆所周知,在Java中,調用Thread的start方法後,該線程即置爲就緒狀態,等待CPU的調度。這個流程裏有兩個關注點需要去理解。

  • start內部怎樣開啓線程的?看看start方法是怎麼實現的。
// Thread類的start方法
public synchronized void start() {
        // 一系列狀態檢查
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
   
        group.add(this);
           
        boolean started = false;
        try {
             //調用start0方法,真正啓動java線程的地方
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                 group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
            }
        }
    }
   
//start0方法是一個native方法
private native void start0();

JVM中,native方法與java方法存在一個映射關係,Java中的start0對應c層的JVM_StartThread方法,我們繼續看一下。

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;
  bool throw_illegal_thread_state = false;
  {
   
    MutexLocker mu(Threads_lock);
    // 判斷Java線程是否已經啓動,如果已經啓動過,則會拋異常。
    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {
      //如果沒有啓動過,走到這裏else分支,去創建線程
      //分配c++線程結構並創建native線程
      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
   
      size_t sz = size > 0 ? (size_t) size : 0;
      //注意這裏new JavaThread
      native_thread = new JavaThread(&thread_entry, sz);
      if (native_thread->osthread() != NULL) {
        native_thread->prepare(jthread);
      }
    }
  }
  ......
  Thread::start(native_thread);

走到這裏發現,Java層已經過渡到native層,但遠遠還沒結束。

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
                          Thread()
   {
     initialize();
     _jni_attach_state = _not_attaching_via_jni;
     set_entry_point(entry_point);
     os::ThreadType thr_type = os::java_thread;
     thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
                                                        os::java_thread;
     //根據平臺,調用create_thread,創建真正的內核線程                       
     os::create_thread(this, thr_type, stack_sz);
   }
   
   bool os::create_thread(Thread* thread, ThreadType thr_type,
                          size_t req_stack_size) {
       ......
       pthread_t tid;
       //利用pthread_create()來創建線程
       int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
       ......
       return true;
}

pthread_create方法,第三個參數表示啓動這個線程後要執行的方法的入口,第四個參數表示要給這個方法傳入的參數。

static void *thread_native_entry(Thread *thread) {
  ......
  //thread_native_entry方法的最下面的run方法,這個thread就是上面傳遞下來的參數,也就是JavaThread
  thread->run();
  ......
  return 0;
}

終於開始執行run方法了!!!

//thread.cpp類
void JavaThread::run() {
  ......
  //調用內部thread_main_inner  
  thread_main_inner();
}
   
void JavaThread::thread_main_inner() {
  if (!this->has_pending_exception() &&
   !java_lang_Thread::is_stillborn(this->threadObj())) {
    {
      ResourceMark rm(this);
      this->set_native_thread_name(this->get_thread_name());
    }
    HandleMark hm(this);
    //注意:內部通過JavaCalls模塊,調用了Java線程要執行的run方法
    this->entry_point()(this, this);
  }
  DTRACE_THREAD_PROBE(stop, this);
  this->exit(false);
  delete this;
}

一條U字型代碼調用鏈總算結束了,高呼一聲,原來start之後做了這麼多事情呀,稍微總結一下。

  1. Java中調用Thread的star方法,通過JNI方式,調用到native層。

  2. native層,JVM通過pthread_create方法創建一個系統內核線程,並指定內核線程的初始運行地址,即一個方法指針。

  3. 在內核線程的初始運行方法中,利用JavaCalls模塊,回調到java線程的run方法,開始java級別的線程執行。

線程開啓後CPU調度會發生什麼?

計算機的世界裏,CPU會分爲若干時間片,通過各種算法分配時間片來執行任務,有耳熟能詳時間片輪轉調度算法、短進程優先算法、優先級算法等。當一個任務的時間片用完,就會切換到另一個任務。在切換之前會保存上一個任務的狀態,當下次再切換到該任務,就會加載這個狀態, 這就是所謂的線程的上下文切換。很明顯,上下文的切換是有開銷的,包括很多方面,操作系統保存和恢復上下文的開銷、線程調度器調度線程的開銷和高速緩存重新加載的開銷等。

經過上面兩個理論基礎的回顧,開啓大量線程引起的問題,總結起來,就兩個字——開銷

  • 消耗時間。線程的創建和銷燬都需要時間,當數量太大的時候,會影響效率。
  • 消耗內存。創建更多的線程會消耗更多的內存,這是毋庸置疑的。線程頻繁創建與銷燬,還有可能引起內存抖動,頻繁觸發GC,最直接的表現就是卡頓。長而久之,內存資源佔用過多或者內存碎片過多,系統甚至會出現OOM。
  • 消耗CPU。在操作系統中,CPU都是遵循時間片輪轉機制進行處理任務,線程數過多,必然會引起CPU頻繁的進行線程上下文切換。這個代價是昂貴的,某些場景下甚至超過任務本身的消耗。

3、針對上面提及到的問題,我們自然需要進行優化。 線程的本質是爲了執行任務,在計算機的世界裏,任務分大致分爲兩類,CPU密集型任務和IO密集型任務。

  • CPU密集型任務,比如公式計算、資源解碼等。這類任務要進行大量的計算,全都依賴CPU的運算能力,持久消耗CPU資源。所以針對這類任務,其實不應該開啓大量線程。因爲線程越多,花在線程切換的時間就越多,CPU執行效率就越低,一般CPU密集型任務同時進行的數量等於CPU的核心數,最多再加個1。
  • IO密集型任務,比如網絡讀寫、文件讀寫等。這類任務不需要消耗太多的CPU資源,絕大部分時間是在IO操作上。所以針對這類任務,可以開啓大量線程去提高CPU的執行效率,一般IO密集型任務同時進行的數量等於CPU的核心數的兩倍。

另外,在無法避免,必須要開啓大量線程的情況下,我們也可以使用線程池代替直接創建線程的做法進行優化。線程池的基本作用就是複用已有的線程,從而減少線程的創建,降低開銷。在Java中,線程池的使用還是非常方便的,JDK中提供了現成的ThreadPoolExecutor類,我們只需要按照自己的需求進行相應的參數配置即可,這裏提供一個示例。

/**
 * 線程池使用
 */
public class ThreadPoolService {

    /**
     * 線程池變量
     */
    private ThreadPoolExecutor mThreadPoolExecutor;

    private static volatile ThreadPoolService sInstance = null;

    /**
     * 線程池中的核心線程數,默認情況下,核心線程一直存活在線程池中,即便他們在線程池中處於閒置狀態。
     * 除非我們將ThreadPoolExecutor的allowCoreThreadTimeOut屬性設爲true的時候,這時候處於閒置的核心         * 線程在等待新任務到來時會有超時策略,這個超時時間由keepAliveTime來指定。一旦超過所設置的超時時間,閒     * 置的核心線程就會被終止。
     * CPU密集型任務  N+1   IO密集型任務   2*N
     */
    private final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() + 1;
    /**
     * 線程池中所容納的最大線程數,如果活動的線程達到這個數值以後,後續的新任務將會被阻塞。包含核心線程數+非*      * 核心線程數。
     */
    private final int MAXIMUM_POOL_SIZE = Math.max(CORE_POOL_SIZE, 10);
    /**
     * 非核心線程閒置時的超時時長,對於非核心線程,閒置時間超過這個時間,非核心線程就會被回收。
     * 只有對ThreadPoolExecutor的allowCoreThreadTimeOut屬性設爲true的時候,這個超時時間纔會對核心線       * 程產生效果。
     */
    private final long KEEP_ALIVE_TIME = 2;
    /**
     * 用於指定keepAliveTime參數的時間單位。
     */
    private final TimeUnit UNIT = TimeUnit.SECONDS;
    /**
     * 線程池中保存等待執行的任務的阻塞隊列
     * ArrayBlockingQueue  基於數組實現的有界的阻塞隊列
     * LinkedBlockingQueue  基於鏈表實現的阻塞隊列
     * SynchronousQueue   內部沒有任何容量的阻塞隊列。在它內部沒有任何的緩存空間
     * PriorityBlockingQueue   具有優先級的無限阻塞隊列。
     */
    private final BlockingQueue<Runnable> WORK_QUEUE = new LinkedBlockingDeque<>();
    /**
     * 線程工廠,爲線程池提供新線程的創建。ThreadFactory是一個接口,裏面只有一個newThread方法。 默認爲DefaultThreadFactory類。
     */
    private final ThreadFactory THREAD_FACTORY = Executors.defaultThreadFactory();
    /**
     * 拒絕策略,當任務隊列已滿並且線程池中的活動線程已經達到所限定的最大值或者是無法成功執行任務,這時候       * ThreadPoolExecutor會調用RejectedExecutionHandler中的rejectedExecution方法。
     * CallerRunsPolicy  只用調用者所在線程來運行任務。
     * AbortPolicy  直接拋出RejectedExecutionException異常。
     * DiscardPolicy  丟棄掉該任務,不進行處理。
     * DiscardOldestPolicy   丟棄隊列裏最近的一個任務,並執行當前任務。
     */
    private final RejectedExecutionHandler REJECTED_HANDLER = new ThreadPoolExecutor.AbortPolicy();

    private ThreadPoolService() {
    }

    /**
     * 單例
     * @return
     */
    public static ThreadPoolService getInstance() {
        if (sInstance == null) {
            synchronized (ThreadPoolService.class) {
                if (sInstance == null) {
                    sInstance = new ThreadPoolService();
                    sInstance.initThreadPool();
                }
            }
        }
        return sInstance;
    }

    /**
     * 初始化線程池
     */
    private void initThreadPool() {
        try {
            mThreadPoolExecutor = new ThreadPoolExecutor(
                    CORE_POOL_SIZE,
                    MAXIMUM_POOL_SIZE,
                    KEEP_ALIVE_TIME,
                    UNIT,
                    WORK_QUEUE,
                    THREAD_FACTORY,
                    REJECTED_HANDLER);
        } catch (Exception e) {
            LogUtil.printStackTrace(e);
        }
    }

    /**
     * 向線程池提交任務,無返回值
     *
     * @param runnable
     */
    public void post(Runnable runnable) {
        mThreadPoolExecutor.execute(runnable);
    }

    /**
     * 向線程池提交任務,有返回值
     *
     * @param callable
     */
    public <T> Future<T> post(Callable<T> callable) {
        RunnableFuture<T> task = new FutureTask<T>(callable);
        mThreadPoolExecutor.execute(task);
        return task;
    }
}

如果你需要這份完整版的面試筆記,只需你多多支持我這篇文章。

多多支持,即可免費獲取資料——三連之後(承諾:100%免費)

快速入手通道:(點這裏)下載!誠意滿滿!!!

Java面試精選題、架構實戰文檔傳送門:https://docs.qq.com/doc/DWGNIdkZtWEFLaFhE

整理不易,覺得有幫助的朋友可以幫忙點贊分享支持一下~
你的支持,我的動力;祝各位前程似錦,offer不斷!!!

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