白話說Java線程(一)之讓線程先跑起來
一、什么是多線程
要理解什么是多線程,先理清楚什么是進程,什么是線程。
先看看百度百科上如何解釋進程的概念:
進程(Process)是計算機中的程序關于某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。程序是指令、數據及其組織形式的描述,進程是程序的實體。
那么線程就可以被理解成進程中可獨立運行的子任務。
既然是在一個進程內獨立運行的子任務,那么單進程意思就是當前進程只能同時允許一個進程在運行,而多進程可以允許多個進程間來回切換,進而更快的完成更多的任務。
舉個例子:
有一把錘子,兩家人共用。小明家需要用這把錘子打造一個柜子(需要一天時間),小新家只需要用這個錘子在墻上釘一個釘子(最多五分鐘)。既然是共用的錘子,而兩家人都需要使用,在單線程的環境下,如果想小明家先拿到錘子,那么就需要獨占一天的時間,小新家雖然只需要使用五分鐘,但是卻要等一天時間小明家才會把錘子空出來。而在多線程的環境下,雖然小明家先拿到錘子,但是也不是一天都在使用,中間也需要吃飯,也需要等待其他材料到齊,總有錘子空閑的時期,這個時候可以先讓小新家拿去使用五分鐘,然后使用完歸還小明家繼續使用。
在多進程的環境下,CPU 完全可以在兩個任務間來回切換,使耗時短的任務不致于等待耗時長的任務完成才能得到執行,系統的運行效率將大大的得到提升。
但是需要注意的是,凡事都有一個度,雖然多線程間切換任務可以加快多個任務執行的效率,但是同時,在切換任務的時候,也是有一定的開銷的,頻繁的切換任務可能切換任務消耗的時間會更多。
二、使用多線程
在 Java 的 JDK 中,已經存在來對多線程技術的支持,如果想使用多線程,有兩種方式:
繼承 Thread 類。
實現 Runnable 接口。
可以看到 Thread 是一個 class,而 Runnable 是一個 Interface 。由于 Java 的單繼承限制,如果有更靈活的繼承要求,可以考慮實現 Runnable 。但是不管是實用那種方式,都需要一個 Thread 對象來承載。
這里為來偷懶,直接用一個 Android 項目來講解來。可以看到,實用起來非常的簡單,只需要實現它們的 run() 方法,然后在需要的時候,調用 Thread.start() 方法即可。
1、start()和run()有什么不同
那么問題來來,既然我們重寫的是 run() 方法,但是為什么最終調用的卻是 start() 方法?
Thread 這個類中的 start() 方法就是通知「線程調度器」此線程已經準備就緒,隨時等待創建好線程,并且調用 run() 方法執行。這樣的啟動線程,由「線程調度器」來調用 run() 方法,具有異步執行的效果。而如果我們直接調用 run() 方法的話,就只是和調用一個普通方法一樣,是在當前線程的同步執行。
2、線程的執行是無序的
雖然我們啟動線程去執行的代碼,可以有先后,但是實際上,線程的執行是無序的。也就是說,線程被執行的順序是隨機的,看誰運氣好。
上面例子中,分別在各個階段輸出來對應的 Log,可以看到,其實它們執行的順序是無序的。
三、線程安全
已經講解完線程的基本使用,如果多條線程之間,沒有任何共享的實例對象被改變,那么到這里就算是完結了。
但是實際情況來說,并不是這樣的。實際上,我們經常會使用多線程的技術操作同一個實例變量。在多個線程之間,操作同一個實例變量,就變成了多線程的技術難點,如何保持多個線程能準確并且安全的操作到同一個實例變量,這就是線程安全。
拿上面的錘子的例子來說,如果錘子是由物業來保管的,這個時候在物業的管理系統上顯示錘子是有一個的,這個小明家和小新家都來借錘子。兩個不同的物業管理員在同一個系統中查到還有一個錘子,就同時做了一個出庫的動作。這個時候就看物業系統是如何設計的了。
使用計數自減的方式,那么就會在錘子的庫存上出現 -1 的計數。
使用直接改變庫存的方式,那么雖然兩個物業管理員都會講庫存從 1 改為 0,這個時候庫存數據是對的。
雖然這兩種方式,看著第二種在賬面上庫存數據是正確的,而第一種出現了負數的情況。但是實際上當兩個物業管理員真的去庫房取錘子的時候,一定有一個是取不到的,就看這個時候誰運氣好了。這就是一個典型的「非線程安全問題」。
這個時候就需要考慮線程安全的問題,在同時操作同一個實例對象的時候,需要保證數據一致性。
簡單的保證線程安全,最常用的方式,就是使用 synchronized 關鍵字。synchronized 可以被加在任意的對象或者方法上,表示對這個方法或者對象加鎖,而加鎖的這段代碼被稱為「互斥區」。當一個線程想要執行 synchronized 中的代碼的時候,會首先嘗試拿這把鎖,如果能夠拿到的話,就可以繼續執行 synchronized 內的代碼。如果拿不到的話,就會不斷嘗試去拿這把鎖,直到能拿到并且完成執行為止。當獲取到鎖并且執行完 synchronized 中的代碼之后,就會放棄鎖的持有,以便于其他線程能得到鎖。
下面舉個例子來說明使用方法。
new 五個線程來操作同一個計數,然后對齊進行自減,并輸出 Log ,先看看沒有加 synchronized 的情況。
可以看到,線程指定的順序是無章的,輸出的 count 的值也是無序的。
如果想讓他們有序的輸出可以使用 synchronized 來標記 run() 方法。
這個時候,雖然線程的執行順序依然是無需的,但是卻可以保證對數據的處理是有序進行的。
四、Thread的一些其他方法
Thread 中還提供了一些其他的API,可以供我們使用,這里簡單的介紹一下。
1、currentThread()
currentThread() 方法啊可以獲取到當前代碼正運行的線程的信息。
從簽名可以看到,它是一個靜態的方法,可以在任何地方調用它。
2、isAlive()
isAlive() 方法用于判斷當前指定線程數否處于活動狀態。活動狀態并不僅僅指的是在運行的狀態,只要是已經準備運行并且沒有結束運行,使用 isAlive() 都會返回 true。
也就是說,在調用了 Thread.start() 之后,一直到 Thread.run() 執行完成之前,isAlive()都是 true。
3、sleep()
sleep() 方法和名稱一樣,可以使當前進程休眠指定的毫秒數。
它也是一個 static 的方法,可以直接調用,用于休眠當前運行的線程。注意 sleep() 方法可能會拋出一個 InterruptedException 的異常,需要我們對其 catch 住。
4、getId()
雖然在 new Thread() 的時候,可以通過構造方法對 Thread 指定一個名稱,但是名稱是可以重復的,如果要唯一確定一個 Thread 的 ID,可以使用 getId() 來獲取一個線程的唯一標志。
五、結語
今天簡單介紹了如何正確的開啟一個線程和包裝開啟的線程是安全的,之后再講如何優雅的停止一個線程。
【本文為51CTO專欄作者“張旸”的原創稿件,轉載請通過微信公眾號聯系作者獲取授權】