一文深刻理解java中的線程

深刻理解java中的線程

咱們知道,一個線程能夠用來執行一個任務,而且該任務的執行是異步的,並不會阻塞後面的代碼。在一個java進程中,包含main方法的類也是在一個線程中執行的。在實際應用中,若是須要處理一個比較耗時的操做,爲了避免影響程序總體的響應,一般會將這個耗時的操做封裝到一個線程中,異步的執行。可是,線程是怎樣實現任務的異步執行的呢?本文將深刻了解Thread類,以指望得出線程執行的祕密。java

根據《深刻理解JAVA虛擬機》中關於線程的章節,咱們得知,在java中一個Thread對應着操做系統中的一個線程。而操做系統的線程是稀缺資源,不能無限制的建立線程,這也就是爲何要使用線程池的緣由之一。bash

咱們也知道,在java中要實現一個線程,有兩種方式:多線程

  • 繼承Thread類
  • 實現Runnable接口

可是不論是哪一種方式,最後線程的執行仍是要經過調用Thread的start()方法異步

讓咱們看一下Thread類的重要屬性和方法:jvm

// target就是一個傳遞給Thread等待Thread執行的Runnable對象
/* What will be run. */
private Runnable target;

/* The group of this thread */
private ThreadGroup group;

// 類方法,該方法會在Thread類初始化時,在類的構造器<clinit>中被調用,且只會調用一次,該方法主要的做用是註冊一些本地的方法,以便後期可使用
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
    registerNatives();
}
// 註冊瞭如下本地方法:
public static native Thread currentThread();
public static nativevoid yield();
public static native void sleep(long millis) throws InterruptedException;
private native void start0();
private native boolean isInterrupted(boolean ClearInterrupted);
public final native boolean isAlive();
public static native boolean holdsLock(Object obj);
private native static StackTraceElement[][] dumpThreads(Thread[] threads);
private native static Thread[] getThreads();
private native void setPriority0(int newPriority);
private native void stop0(Object o);
private native void suspend0();
private native void resume0();
private native void interrupt0();
private native void setNativeName(String name);
複製代碼

讓咱們看看下面這段代碼將輸出什麼內容:ide

public static class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("MyThread---1");
    }
}
public static class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("MyRunnable---1");
    }
}
public static void main(String[] args) {
    Thread t1 = new MyThread();
    Thread t2 = new Thread(new MyRunnable());
    t1.start();
    t2.start();
    System.out.println("MyThread---2");
    System.out.println("MyRunnable---2");
}
複製代碼

該代碼的輸出內容是不肯定的,可能輸出爲:函數

MyThread---2
MyRunnable---2
MyRunnable---1
MyThread---1
複製代碼

也可能輸出爲:ui

MyThread---1
MyRunnable---1
MyThread---2
MyRunnable---2
複製代碼

可是若是把上述的代碼t1.start(),t2.start()改成:this

t1.run();
t2.run();
複製代碼

那麼輸出將變成肯定的:spa

MyThread---1
MyRunnable---1
MyThread---2
MyRunnable---2
複製代碼

爲何使用start(),輸出的內容是不肯定的,而使用run()輸出倒是肯定的呢?這就須要從Thread的啓動過程開始瞭解了。 Thread類中start()方法的源代碼以下:

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads * and the group's unstarted count can be decremented. */
    group.add(this);
    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}
private native void start0();
複製代碼

能夠看到,start()方法內部其實調用了一個native的方法start0()。而在Thread類初始化時,執行了一個registerNatives()的方法來註冊本地方法,其中start0方法實際關聯的是JVM_StartThread方法:

{"start0", "()V",(void *)&JVM_StartThread}
複製代碼

在 jvm.cpp 中,有以下代碼段:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)){
    ...
    native_thread = new JavaThread(&thread_entry, sz);
    ...
}
複製代碼

這裏JVMENTRY是一個宏,用來定義JVMStartThread函數,能夠看到函數內建立了真正的平臺相關的本地線程,其線程函數是thread_entry,以下:

static void thread_entry(JavaThread* thread, TRAPS) {
    HandleMark hm(THREAD);
    Handle obj(THREAD, thread->threadObj());
    JavaValue result(T_VOID);
    JavaCalls::call_virtual(&result,obj,KlassHandle(THREAD,SystemDictionary::Thread_klass()),
    vmSymbolHandles::run_method_name(),    //調用了run_method_name
    vmSymbolHandles::void_method_signature(),THREAD);
}
複製代碼

能夠看到調用了vmSymbolHandles::runmethodname方法,而runmethodname是在vmSymbols.hpp 用宏定義的:

class vmSymbolHandles: AllStatic {
    ...
    // 這裏決定了調用的方法名稱是 「run」
    template(run_method_name,"run")
    ...
}
複製代碼

從以上的代碼中能夠看出,Thread執行start()方法,首先會建立一個新的操做系統的線程,而後當該操做線程獲得CPU時間片時,會執行一個回調方法:run(),這也就證實了經過start()能夠建立一個新線程並異步執行線程體,而經過run()只是執行了一個Thread對象的普通方法而已,並不會並行執行,而是串行執行的。

如下附上一些Thread相關的常見性的問題:

Thread的sleep、join、yield

  • 1.sleep
  1. sleep()使當前線程進入停滯狀態(阻塞當前線程),讓出CPU的使用,以留必定時間給其餘線程執行
  2. sleep休眠時不會釋放對象的鎖
  • 2.join 在一個線程A中執行了線程B的join方法,則A會掛起,等待B執行完畢後再執行後續任務
public static void main(String[] args){
    Thread t1 = new Thread();
    t1.start();
    t1.join();
    // 如下代碼會在t1執行完畢後打印
    System.out.println("t1 finished");
}
複製代碼
  • 3.yield
  1. yield並不意味着退出和暫停,只是,告訴線程調度若是有人須要,能夠先拿去,我過會再執行,沒人須要,我繼續執行
  2. 調用yield的時候鎖並無被釋放

object的wait、notify、notifyAll

  • 1.wait
  1. wait()方法是Object類裏的方法;當一個線程執行到wait()方法時
  2. 該線程就進入到一個和該對象相關的等待池中,同時失去了對象的鎖,被喚醒時再次得到鎖
  3. wait()使用notify()或者notifyAll()或者指定睡眠時間來喚醒當前等待池中的線程
  4. wait()必須放在synchronized塊中,不然會報錯"java.lang.IllegalMonitorStateException"
  • 2.notify

wait()和notify()必須操做同一個"對象監視器"

Runnable1 implements Runnable{
    public void run(){
        synchronized(lock){
            // 等待其餘線程來喚醒
            lock.wait();
            System.out.println("Runnable1 has been notified by other thread");
        }
    }
}
Runnable2 implements Runnable{
    public void run(){
        synchronized(lock){
            System.out.println("Runnable2 will notify other thread who wait for lock");
            // 喚醒其餘線程            lock.notify();
        }
    }
}
public static void main(String[] args){
    Object lock = new Object();
    Thread t1 = new Thread(new Runnable1(lock));
    Thread t2 = new Thread(new Runnable2(lock));
    t1.start();
    t2.start();
}
複製代碼

Thread和Runnable的區別

  • Runnable能夠經過Thread的start(實際調用了target的run方法)啓動,而Thread中target的屬性就是一個Runnable
  • Runnable能夠實現屬性資源共享,Thread不能夠實現資源共享
MyRunnable r = new MyRunnable();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
// t1/t2線程操做的都是同一個實例r,因此r中的數據能夠實現多線程共享
t1.start();
t2.start();
複製代碼

線程之間如何進行通訊

  • join : 一個線程等待另外一個線程執行完畢後再執行
  • wait/notify : 一個線程等待另外一個線程喚醒本身所擁有的對象監視器後再執行
  • CountdownLatch : 一個線程等待(countDownLatch.await())其餘任意個數的線程執行完畢後(countDownLatch.countDown())再執行
  • CyclicBarrier : 全部線程先各自準備,當全部線程都準備完畢(所有都調用了cyclicBarrier.await())後統一開始執行後續操做
  • Semaphore : 能夠控制同時訪問的線程個數,經過acquire()獲取一個許可,若是沒有就等待,而release()釋放一個許可
  • Callable : 子線程將執行結果返回給父線程
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
Object result = futureTask.get();
複製代碼
  1. CountDownLatch和CyclicBarrier都可以實現線程之間的等待,只不過它們側重點不一樣:
  2. CountDownLatch通常用於某個線程A等待若干個其餘線程執行完任務以後,它才執行;
  3. CyclicBarrier通常用於一組線程互相等待至某個狀態,而後這一組線程再同時執行;
  4. 另外,CountDownLatch是不可以重用的,而CyclicBarrier是能夠重用的。
  5. Semaphore其實和鎖有點相似,它通常用於控制對某組資源的訪問權限。

線程池的原理

線程池有兩個參數:核心線程數coreNum和最大線程數maxNum

假設初始化一個線程池,核心線程數是5,最大線程數是10,線程池初始化的時候,裏面是沒有線程的

當來了一個任務時,就初始化了一個線程,若是再來一個任務,再初始化了一個線程,連續初始化了5個線程以後,若是第6個任務過來了

這時會把第6個任務放到阻塞隊列中

如今線程池中有了5個線程,若是其中一個線程空閒了,就會從阻塞隊列中獲取第6個任務,進行執行

若是線程池的5個線程都在running狀態,那麼任務就先保存在阻塞隊列中

若是隊列滿了,而且咱們設置了最大線程數是10,但線程池中只有5個線程,這時會新建一個線程去執行不能保存到阻塞隊列的任務,此時線程池中有了6個線程

若是線程池中的線程數達到10個了,而且阻塞隊列也滿了,則能夠經過自定義的reject函數去處理這些任務

最後運行一段時間以後,阻塞隊列中的任務也執行完了,線程池中超過核心線程數的線程會在空閒一段時間內自動回收

相關文章
相關標籤/搜索