(一)Java多線程基礎篇

1、多線程中的一些名詞概念

1.1 進程與線程的區別

1)進程:

  • 進程就是程序在併發環境中的執行過程,也就是一個正在運行的程序。所謂正在運行的程序,就少不了三個東西,一個是cpu,一個是程序本身(代碼什麼的),還有一個運行環境。比如說:一個人正在做飯(進程),首先要有一個人(CPU),還要有一個菜譜(代碼),最後要有一個廚房(運行環境)。
  • 進程是資源(CPU、內存等)分配的基本單位,它是程序執行時的一個實例。程序運行時系統就會創建一個進程,併爲它分配資源,然後把該進程放入進程就緒隊列,進程調度器選中它的時候就會爲它分配CPU時間,程序開始真正運行。

2)線程:

  • 所謂線程,就是程序的一個個子模塊。比如說:一個人正在做飯(進程),他要洗菜(線程),他也要切菜(線程),他還要和麪(也是線程),把做飯這個事情,分成一個個小的模塊,再有這個人(CPU)去一一處理。
  • 線程是程序執行時的最小單位,它是進程的一個執行流,是CPU調度和分派的基本單位,一個進程可以由很多個線程組成,線程間共享進程的所有資源,每個線程有自己的堆棧和局部變量。線程由CPU獨立調度執行,在多CPU環境下就允許多個線程同時運行。同樣多線程也可以實現併發操作,每個請求分配一個線程來處理。

3)進程與線程的區別

  • 進程是資源分配的最小單位,線程是程序執行的最小單位。一個程序要運行,給你cpu讓你運行,但是要具體怎麼運行,就看程序是如何分配的了。
  • 線程之間的通信更方便,同一進程下的線程共享全局變量、靜態變量等數據,而進程之間的通信需要以通信的方式(IPC)進行。不過如何處理好同步與互斥是編寫多線程程序的難點。這也就是後面爲什麼要用鎖的原因了。
  • 進程有自己的獨立地址空間,每啓動一個進程,系統就會爲它分配地址空間,建立數據表來維護代碼段、堆棧段和數據段,這種操作非常昂貴。而線程是共享進程中的數據的,使用相同的地址空間,因此CPU切換一個線程的花費遠比進程要小很多,同時創建一個線程的開銷也比進程要小很多。
  • 但是多進程程序更健壯,多線程程序只要有一個線程死掉,整個進程也死掉了,而一個進程死掉並不會對另外一個進程造成影響,因爲進程有自己獨立的地址空間。

1.2 同步與異步的區別

多線程併發時,多個線程同時請求同一個資源,必然導致此資源的數據不安全,A線程修改了B線程的處理的數據,而B線程又修改了A線程處理的數理。顯然這是由於全局源造成的,有時爲了解決此問題,優先考慮使用局部變量,退而求其次使用同步代碼塊,出於這樣的安全考慮就必須犧牲系統處理性能,加在多線程併發時資源掙奪最激烈的地方,這就實現了線程的同步機制

  • 同步:A線程要請求某個資源,但是此資源正在被B線程使用中,因爲同步機制存在,A線程請求不到,怎麼辦,A線程只能等待下去
  • 異步:A線程要請求某個資源,但是此資源正在被B線程使用中,因爲沒有同步機制存在,A線程仍然請求的到,A線程無需等待

顯然,同步最最安全,最保險的。而異步不安全,容易導致死鎖,這樣一個線程死掉就會導致整個進程崩潰,但沒有同步機制的存在,性能會有所提升

1.3 併發與並行的區別

  • 併發:就是讓一個處理器處理多個任務,但這些任務不一定要同時進行。比如說:一個人喫三個饅頭,他可以一個一個的喫,並不一定要一口喫完。
  • 並行:同時發生的兩個併發事件,也就是多個處理器同時處理多個任務。比如說:三個人同時喫三個饅頭。

1.4 什麼是鎖、什麼是死鎖

  • 鎖:可以理解爲普通意義上的一把鎖,不過他是用來鎖資源的。比如給一段資源(方法、代碼塊)加上一把鎖,則這段資源同一時間,只能有一個線程對他訪問,只有等這個線程訪問完了,其他的線程才能訪問。這不就是同步機制嗎???沒錯,鎖存在的意義,就是讓他產生同步機制的。
  • 死鎖:是指同一個進程集合中的每個進程都在等待僅有該集合中的另一個進程才能引發的事件而無限期地僵持下去的局面,也就是指多個進程在運行過程中因爭奪資源而造成的一種僵局,當進程處於這種僵持狀態時,若無外力作用,它們都將無法再向前推進。比如說:在一條單車道的路上,雙向各來一輛車,這兩個車都在等對方給自己讓路,就會形成一種僵局,也就是死鎖。

2、線程中的五種基本狀態

在這裏插入圖片描述

2.1 新建狀態(New)

當線程對象對創建後,即進入了新建狀態,如:Thread t = new MyThread();

2.2 就緒狀態(Runnable)

當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處於就緒狀態的線程,只是說明此線程已經做好了準備,隨時等待CPU調度執行,並不是說執行了t.start()此線程立即就會執行;

2.3 運行狀態(Running)

當CPU開始調度處於就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就 緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;

2.4 阻塞狀態(Blocked)

處於運行狀態中的線程由於某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分爲三種:

  • 1)等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;

  • 2)同步阻塞: 線程在獲取synchronized同步鎖失敗(因爲鎖被其它線程所佔用),它會進入同步阻塞狀態;

  • 3)其他阻塞:通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。

2.5 死亡狀態(Dead)

線程執行完了或者因異常退出了run()方法,該線程結束生命週期。

3、線程的創建於啓動

3.1 繼承Thread類

1)重寫該類的run()方法。

class MyThread extends Thread {

    private int i = 0;

    @Override
    public void run() {
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}
public class ThreadTest {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread myThread1 = new MyThread();     // 創建一個新的線程  myThread1  此線程進入新建狀態
                Thread myThread2 = new MyThread();     // 創建一個新的線程 myThread2 此線程進入新建狀態
                myThread1.start();                     // 調用start()方法使得線程進入就緒狀態
                myThread2.start();                     // 調用start()方法使得線程進入就緒狀態
            }
        }
    }
}
  • 繼承Thread類,通過重寫run()方法定義了一個新的線程類MyThread,其中run()方法的方法體代表了線程需要完成的任務,稱之爲線程執行體。當創建此線程類對象時一個新的線程得以創建,並進入到線程新建狀態。通過調用線程對象引用的start()方法,使得該線程進入到就緒狀態,此時此線程並不一定會馬上得以執行,這取決於CPU調度時機。

3.2 實現Runnable接口

-1)實現Runnable接口,並重寫該接口的run()方法,該run()方法同樣是線程執行體,創建Runnable實現類的實例,並以此實例作爲Thread類的target來創建Thread對象,該Thread對象纔是真正的線程對象。

class MyRunnable implements Runnable {
    private int i = 0;

    @Override
    public void run() {
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}
public class ThreadTest {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Runnable myRunnable = new MyRunnable(); // 創建一個Runnable實現類的對象
                Thread thread1 = new Thread(myRunnable); // 將myRunnable作爲Thread target創建新的線程
                Thread thread2 = new Thread(myRunnable);
                thread1.start(); // 調用start()方法使得線程進入就緒狀態
                thread2.start();
            }
        }
    }
}

2)或者說,直接通過lamda表達式也是可以的實現的,因爲Runnable接口只有一個方法:

public class ThreadTest {

	public static void main(String[] args) {
		Runnable mt = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread().getName() + "    " + i);
			}
		};
		
		new Thread(mt).start();
		new Thread(mt).start();
	}

}

3)Thread和Runnable之間到底是什麼關係?

class MyRunnable implements Runnable {
    private int i = 0;

    @Override
    public void run() {
        System.out.println("in MyRunnable run");
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

class MyThread extends Thread {

    private int i = 0;

    public MyThread(Runnable runnable){
        super(runnable);
    }

    @Override
    public void run() {
        System.out.println("in MyThread run");
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

public class ThreadTest {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Runnable myRunnable = new MyRunnable();
                //這裏沒有用Thread類,二是繼承了Thread的MyThread類
                Thread thread = new MyThread(myRunnable);
                thread.start();
            }
        }
    }
}
  • 首先,可以肯定的是,這種方式是沒有問題的。

  • 至於此時的線程執行體到底是MyRunnable接口中的run()方法還是MyThread類中的run()方法呢?通過輸出我們知道線程執行體是MyThread類中的run()方法。

  • 爲什麼呢?

    //Runnable源碼是這樣寫的
     public interface Runnable {
    
         public abstract void run();
    
     }
    
    	//Thread源碼中是這樣寫的
    public class Thread implements Runnable {
    	
    	private Runnable target;
    	
    	private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
    		...
    	}
    	
        @Override
        public void run() {
            if (target != null) {
                target.run();
            }
        }
        
    }
        
    

    如果我們使用Thread thread = new Thread (myRunnable);則由源碼可知:當啓動這個線程,經過初始化,在運行run()方法的時候,會先判斷 target 是否爲空,這裏是不爲空的,因爲Thread是繼承Runnable的,這裏也對其初始化過。

    如果使用Thread thread = new MyThread(myRunnable);,因爲沒有target 這個變量,重寫的run()也沒有對他判null,又由多態可知,這裏沒有機會執行Thread中的run()方法,所以輸出的是:MyThread類中的run()方法。

3.3 實現Callable接口

1)具體是創建Callable接口的實現類,並實現clall()方法。並使用FutureTask類來包裝Callable實現類的對象,且以此FutureTask對象作爲Thread對象的target來創建線程。

public class ThreadTest {

    public static void main(String[] args) {

        Callable<Integer> myCallable = new MyCallable();    // 創建MyCallable對象
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask來包裝MyCallable對象

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread thread = new Thread(ft);   //FutureTask對象作爲Thread對象的target創建新的線程
                thread.start();                      //線程進入到就緒狀態
            }
        }

        System.out.println("主線程for循環執行完畢..");

        try {
            int sum = ft.get();            //取得新創建的新線程中的call()方法返回的結果
            System.out.println("sum = " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}


class MyCallable implements Callable<Integer> {
    private int i = 0;

    // 與run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() {
        int sum = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }

}
  • 在實現Callable接口中,此時不再是run()方法了,而是call()方法,此call()方法作爲線程執行體,同時還具有返回值!在創建新的線程時,是通過FutureTask來包裝MyCallable對象,同時作爲了Thread對象的target。那麼看下FutureTask類的定義:

    public class FutureTask<V> implements RunnableFuture<V> {
    
         //....
    
     }
     
    public interface RunnableFuture<V> extends Runnable, Future<V> {
    
         void run();
    
     }
    

我們發現FutureTask類實際上是同時實現了Runnable和Future接口,由此才使得其具有Future和Runnable雙重特性。通過Runnable特性,可以作爲Thread對象的target,而Future特性,使得其可以取得新創建線程中的call()方法的返回值。

執行下此程序,我們發現sum = 4950永遠都是最後輸出的。而“主線程for循環執行完畢…”則很可能是在子線程循環中間輸出。由CPU的線程調度機制,我們知道,“主線程for循環執行完畢…”的輸出時機是沒有任何問題的,那麼爲什麼sum =4950會永遠最後輸出呢?

原因在於通過ft.get()方法獲取子線程call()方法的返回值時,當子線程此方法還未執行完畢,ft.get()方法會一直阻塞,直到call()方法執行完畢才能取到返回值。

4、多線程重要 API 以及其背後的意義

4.1 yield()——線程讓步

1)當調用線程的yield()方法時,線程從運行狀態轉換爲就緒狀態,但接下來CPU調度就緒狀態中的哪個線程還是與線程的優先級緊密相關,CPU從就緒狀態線程隊列中只會選擇與該線程優先級相同或優先級更高的線程去執行。

2)這個方法會釋放CPU資源,但是不會釋放鎖資源,只有他訪問的帶鎖資源執行完之後,鎖資源纔會釋放。(如果剛看不理解沒關係,記住這是一個超級重要的知識點就行)

4.2 join()

1)讓一個線程等待另一個線程完成才繼續執行。如A線程線程執行體中調用B線程的join()方法,則A線程被阻塞,直到B線程執行完爲止,A才能得以繼續執行。

public class ThreadTest {

	public static void main(String[] args) {
		Runnable r = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread().getName() + " " + i);
			}
		};
		Thread thread1 = new Thread(r);
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
			if (i == 30) {
				thread1.start();
				try {
					//在thread1線程啓動後才能才能使用這個方法
					thread1.join(); // main線程需要等待thread線程執行完後才能繼續執行
				} catch (Exception e) {
					e.printStackTrace();
				}

			}
		}
	}
}

4.3 sleep()

1)讓當前的正在執行的線程暫停指定的時間,並進入阻塞狀態。在其睡眠的時間段內,該線程由於不是處於就緒狀態,因此不會得到執行的機會。即使此時系統中沒有任何其他可執行的線程,出於sleep()中的線程也不會執行。因此sleep()方法常用來暫停線程執行。

2)當調用了新建的線程的start()方法後,線程進入到就緒狀態,可能會在接下來的某個時間獲取CPU時間片得以執行,如果希望這個新線程必然性的立即執行,直接調用原來線程的sleep(1)即可。

3)這是一個靜態方法,在哪個線程的執行體裏面調用它,就讓那個線程暫停指定的時間。

4)在調用sleep()方法的過程中,線程不會釋放對象鎖。 但是會讓出自己所佔用的CPU資源,自身進入阻塞狀態。

public class ThreadTest {

	public static void main(String[] args) {
		Runnable r = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread().getName() + " " + i);
			}
		};
		Thread thread1 = new Thread(r);
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
			if (i == 30) {
				thread1.start();
				try {
					Thread.sleep(1); // 使得thread必然能夠馬上得以執行
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

4.4 後臺線程

1)後臺線程主要是爲其他線程(相對可以稱之爲前臺線程)提供服務,或“守護線程”。如JVM中的垃圾回收線程。

2)生命週期:後臺線程的生命週期與前臺線程生命週期有一定關聯。主要體現在:當所有的前臺線程都進入死亡狀態時,後臺線程會自動死亡(其實這個也很好理解,因爲後臺線程存在的目的在於爲前臺線程服務的,既然所有的前臺線程都死亡了,那它自己還留着有什麼用…偉大啊 ! !)。

3)設置後臺線程:調用Thread對象的setDaemon(true)方法可以將指定的線程設置爲後臺線程。

public class ThreadTest {

	public static void main(String[] args) {
		Runnable r = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread().getName() + " " + i);
			}
		};
		Thread thread1 = new Thread(r);
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
			if (i == 30) {
				thread1.start();
				thread1.setDaemon(true);
			}
		}
	}
}

4.5 setPriority() / getPriority()——線程優先級

1)每個線程在執行時都具有一定的優先級,優先級高的線程具有較多的執行機會。每個線程默認的優先級都與創建它的線程的優先級相同。main線程默認具有普通優先級。

  • 改變線程的優先級/setPriority():
  • 獲取線程優先級:getPriority()。

設置線程優先級:setPriority(int priorityLevel)。參數priorityLevel範圍在1-10之間,常用的有如下三個靜態常量值:

  • MAX_PRIORITY:10

  • MIN_PRIORITY:1

  • NORM_PRIORITY:5

public class ThreadTest {

	public static void main(String[] args) {
		Runnable r = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread().getName() + " " + i);
			}
		};
		Thread thread1 = new Thread(r);
		thread1.setPriority(Thread.MAX_PRIORITY);

		Thread thread2 = new Thread(r);
		thread2.setPriority(Thread.NORM_PRIORITY);

		Thread thread3 = new Thread(r);
		thread3.setPriority(Thread.MIN_PRIORITY);

		for (int i = 0; i < 100; i++) {
			if (i == 30) {
				thread1.start();
				thread2.start();
				thread3.start();
			}
		}
	}
}

4.6 interrupt()——中斷線程

1)向線程發送中斷請求。線程的中斷狀態將被設置爲true。如果目前該線程被一個sleep調用阻塞,那麼,InterruptedException異常將會拋出。

2)它基於一個線程不應該由其他線程來強制中斷或停止,而是應該由線程自己自行停止的思想(我命由我不由天)。是一個比較溫柔的做法,它更類似一個標誌位。其實作用不是中斷線程,而是通知線程應該中斷了,具體到底中斷還是繼續運行,應該由被通知的線程自己處理。

3)並不能真正的中斷線程,這點要謹記。需要被調用的線程自己進行配合纔行。也就是說,一個線程如果有被中斷的需求,那麼就需要這樣做:

  • 在正常運行任務時,經常檢查本線程的中斷標誌位,如果被設置了中斷標誌就自行停止線程。
  • 在調用阻塞方法時正確處理InterruptedException異常。(例如:catch異常後就結束線程。)

4)static boolean interrupted():測試當前線程(正在執行這一命令的線程)是否被中斷。這是一個靜態方法,會產生一個副作用——將當前線程的中斷狀態重置爲false

5)boolean isInterrupted()測試線程是否被終止。這一個調用不會改變線程的中斷狀態。

4.7 其他方法

1)static Thread currentThread()——當前線程:返回對當前正在執行的線程對象的引用。

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