Java併發

1. 線程狀態轉換

新建(New)
創建後尚未啓動
可運行(Runnable)
可能正在運行,也可能正在等待CPU時間片。
包含了操作系統線程狀態中的Running和Ready。
阻塞(Blocked)
等待獲取一個排它鎖,如果其線程釋放了鎖就會結束次狀態。
無限期等待(Waiting)
等待其它線程顯式地喚醒,否則不會被分配 CPU 時間片。
在這裏插入圖片描述
限期等待(Timed Waiting)
無需等待其他線程顯示的被喚醒,在一定時間之後會被系統自動喚醒。
調用Thread.sleep()方法使線程進入限期等待狀態時,常常用 是一個線程睡眠 進行描述。
調用Object.wait()方法使線程進入限期等待或者無限期等待時,嚐嚐用掛起一個線程進行描述。
睡眠和掛起是用來描述行爲,而阻塞和等待用來描述狀態。
阻塞和等待的區別在於,阻塞是被動地,它是在等待獲取一個排他鎖。而等待時主動地,通過調用Thread.sleep()和Object.wait()等方法進去。
在這裏插入圖片描述
死亡(Terminated)
可以使線程結束之後自己結束,或者產生了異常而結束。

2. 使用線程

有三種使用線程方法:

  • 實現Runnable接口;
  • 實現Callable接口;
  • 繼承Thread類。

實現Runnable和Callable接口的類只能當做一個可以在線程中運行的任務,不是真正意義上的線程,因此最後還是需要通過Thread來調用。可以說任務是通過線程驅動從而執行的。

實現Runnable接口

需要實現run()方法。
通過Thread調用start()方法來啓動線程。

public class MyRunnable implements Runnable {
    public void run() {
		// ... 
	}
}
public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}
實現Callable接口

與Runnable相比,Callable可以有返回值,返回值通過FutureTask進行封裝。

public class MyCallable implements Callable<Integer> {
    public Integer call() {
		return 123; 
	}
}

public static void main(String[] args) throws ExecutionException, 
											InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}
繼承Thread類

同樣也是需要實現run()方法,因爲Thread類也實現了Runnable接口。
當調用start()方法啓動一個線程時,虛擬機會將該線程放入就緒隊列中等待被調度,當一個線程被調度時會執行該線程的run()方法。

public class MyThread extends Thread {
    public void run() {
		// ... 
	}
}
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}
實現接口 VS 繼承Thread

實現接口會更好一些,因爲:

  • Java不支持多繼承,因此繼承了Thread類就無法繼承其他類,但是可以實現多個接口;
  • 類可能只要求可執行就行,繼承整個Thread類開銷過大。
3.基礎線程機制
Executor

Executor管理多個異步任務的執行,而無需程序員顯示的管理線程的生命週期。這裏的異步是指多個任務的執行互不干擾,不需要進行同步操作。
主要有三種Executor:

  • CachedThreadPool:一個任務創建一個下次呢很難過;
  • FixedThreadPool:所有任務只能使用固定大小的線程;
  • SingleThreadExecutor:相當於大小爲1的FixedThreadPool。
 public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        executorService.execute(new MyRunnable());
    }
    executorService.shutdown();
}
Daemon

守護線程是程序運行時在後臺提供服務的線程,不屬於程序中不可或缺的部分。
當所有非守護線程結束時,程序也就終止,同時會殺掉所有守護線程。
main()屬於非守護線程。
使用setDaemon()方法將一個線程設置爲守護線程。

 public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
}
sleep()

Thread.sleet(millisec) 方法會休眠當前正在執行的線程,millisec單位爲毫秒。
sleet()可能會拋出InterruptedException,因爲一場不能跨線程傳播回main()中,因此必須在本地進行處理。線程中拋出的其他異常也同樣需要在本地進行處理。

public void run() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
yield()

對靜態方法Thread.yield()的調用聲明瞭當前線程已經完成了生命週期中最重要的部分,可以切換給其他線程來執行。該方法只是對線程調度器的一個建議,而且也只是建議具有相同優先級的其他線程可以運行。

public void run() {
    Thread.yield();
}
4.中斷

一個線程執行完畢之後會自動結束,如果在運行過程中發生異常也會提前結束。

InterruptedException

通過調用一個線程的interrupt()來中斷該線程,如果該線程處於阻塞、限期等待或者無限期等待狀態,那麼就會拋出InterruptedException,從而提前結束該線程。但是不能中斷I/O阻塞和synchronized鎖阻塞。
對於以下代碼,在main()中啓動一個線程之後再中斷它,由於線程中調用了Thread.sleep()方法,因此會拋出一個InterruptedException,從而提前結束線程,不再執行之後的語句。

public class InterruptExample {
    private static class MyThread1 extends Thread {
        @Override
        public void run() {
            try {
            	Thread.sleep(2000);
    			System.out.println("Thread run");
			} catch (InterruptedException e) {
    			e.printStackTrace();
			}
		} 
	}
}

public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new MyThread1();
    thread1.start();
    thread1.interrupt();
    System.out.println("Main run");
}

Main run
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at InterruptExample.lambda$main$0(InterruptExample.java:5)
    at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)
interrupted()

如果一個線程的run()方法執行了一個無限循環,並且沒有執行sleep()等會拋出InterruptedException的操作,那麼調用線程的interrupt()方法就無法使線程提前結束。
但是調用interrupt()方法會設置線程中斷標記,此時調用interrupted()方法會返回true。因此可以在循環體中使用interrupted()方法來判斷線程是否處於中斷狀態,從而提前結束線程。

public class InterruptExample {
    private static class MyThread2 extends Thread {
        @Override
		public void run() {
    		while (!interrupted()) {
			// .. 
			}
		}
    	System.out.println("Thread end");
    	}
    }
}

public static void main(String[] args) throws InterruptedException {
    Thread thread2 = new MyThread2();
    thread2.start();
    thread2.interrupt();
}

Thread end
Executor 的中斷操作

調用Executor的shutdown()方法會等待線程都執行完畢之後再關閉,但是如果調用的是shutdownNow()方法,則相當於調用每個線程的interrupt()方法。
以下使用Lambda創建線程,相當於創建了一個匿名內部線程。

public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> {
        try {
            Thread.sleep(2000);
            System.out.println("Thread run");
        } catch (InterruptedException e) {
            e.printStackTrace();
		} });
    executorService.shutdownNow();
    System.out.println("Main run");
}

Main run
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9)
    at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

如果只想中斷 Executor 中的一個線程,可以通過使用 submit() 方法來提交一個線程,它會返回一個 Future<?> 對 象,通過調用該對象的 cancel(true) 方法就可以中斷線程。

5.互斥同步

Java提供了兩種鎖機制來控制多個線程對共享資源的互斥訪問,第一個是JVM實現的synchronized,另一個是JDK實現的ReentrantLock。

synchronized
  1. 同步一個代碼塊
public void func() {
    synchronized (this) {
		// ...
	 }
}

它只作用於同一個對象,如果調用兩個對象上的同步代碼塊,就不會進行同步。
對於以下代碼,使用ExecutorService執行了兩個線程,由於調用的是同一個對象的同步代碼塊,因此這兩個線程會進行同步,當一個線程進入同步語塊時,另一個線程就必須等待。

public class SynchronizedExample {
	public void func1() {
	    synchronized (this) {
	        for (int i = 0; i < 10; i++) {
	            System.out.print(i + " ");
	        }
	    }
	}
}

public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}

01234567890123456789

對於以下代碼,兩個線程調用了不同對象的同步代碼塊,因此這兩個線程不需要同步。從輸出結果可以看出兩個線程交叉執行。

public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
}

00112233445566778899
  1. 同步一個方法
public synchronized void func () {
    // ...
}

他和同步代碼塊一樣,作用於同一個對象。
3. 同步一個類

public void func() {
    synchronized (SynchronizedExample.class) {
		// ... 
	}
}

作用於整個類,也就是說兩個線程調用同一個類的不同對象上的這種同步語句,也會進行同步。

 public class SynchronizedExample {
    public void func2() {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
			}	 
		}
	} 
}

 public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func2());
    executorService.execute(() -> e2.func2());
}

01234567890123456789
  1. 同步一個靜態方法
 public synchronized static void fun() {
    // ...
}

作用於整個類。

ReentrantLock

ReentrantLock是java.util.concurrent(J.U.C)包中的鎖。

 public class LockExample {
    private Lock lock = new ReentrantLock();
    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
		} finally {
			lock.unlock(); // 確保釋放鎖,從而避免發生死鎖。
		} 
	}
}

public static void main(String[] args) {
    LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
}

01234567890123456789
比較
  1. 鎖的實現
    synchronized是jvm實現的,而ReentrantLock是JDK實現的。
  2. 性能
    新版本Java對synchronized進行了很多優化,例如自旋鎖等,synchronized與ReentrantLock大致相同。
  3. 等待可中斷
    當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改爲處理其他事情。
    ReentrantLock可中斷,synchronized不行。
  4. 公平鎖
    公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖。
    synchronized中的鎖時非公平的,ReentrantLock默認情況下是非公平的,但是也可以是公平的。
  5. 鎖綁定多個條件
    一個ReentrantLock可以同時綁定多個Condition對象。
使用選擇

除非需要使用ReentrantLock的高級功能,否則優先使用synchronized,因爲這是synchronized是JVM實現的一種鎖機制,JVM原生的支持他,而ReentrantLock不是所有的JDK版本都支持,並且使用Synchronized不用擔心沒有釋放鎖而導致死鎖的問題,因爲JVM會確保鎖的釋放。

6.線程之間的協作

當多個線程可以一起工作去解決某個問題時,如果某部分必須在其他部分之前完成,那麼就需要對線程進行協調。

join()

在線程中調用另一個線程的join()方法,會將當前線程掛起,而不是忙等待,直到目標線程結束。
對於以下代碼,雖然b線程先啓動,但是還是因爲在b線程中調用了a線程的join()方法,b線程會等待a線程結束才繼續執行,因此最後能夠保證a線程的輸出先於b線程的輸出。

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