-
多線程的概念
線程是指一個任務從頭到尾的執行流,線程提供了一個運行的機制。
在
Java
中,一個程序中可以併發的啓動多個線程,這也就意味着線程可以在多處理器系統上同一時刻運行。
多線程可以使程序反應更快,執行效率更高。
- 多線程編程
以上介紹的概念可能還不夠清晰的解釋什麼是多線程,沒關係,我們舉一個例子看一下。當然,如果我們想要創建一個多線程程序,那麼首先我們應該提供多個任務供我們去執行,想要創建一個這樣的任務,我們需要實現 Runnable
接口,打開Runnable
源碼如下
public interface Runnable {
public abstract void run();
}
很簡單,其中只是包含了一個 run()
的抽象方法,只要我們在子類中去實現它就可以了。
創建一個打印 “ I am task a!” 的任務
public class A implements Runnable{
@Override
public void run() {
System.out.println("I am task a !");
}
}
基於同樣的原理,實現一個打印 “ I am task b !" 和一個 "I am task c !"的方法,代碼同上。然後在主函數中去開啓線程,實現打印任務。
主函數實現如下:
public class Main {
public static void main(String[] args){
int i = 0;
while(50 >= i){
Thread threadA = new Thread(new A());
Thread threadB = new Thread(new B());
Thread threadC = new Thread(new C());
threadA.start();
threadB.start();
threadC.start();
i++;
}
}
}
在主函數中我們開啓了這三個線程,去實現打印任務,按照我們平常的理解,該程序肯定是依次執行A,B,C任務,但是打印結果卻是:
I am task b !
I am task c !
I am task a !
I am task c !
I am task a !
I am task b !
I am task b !
I am task a !
I am task c !
I am task b !
怎麼樣,看到這裏,你應該知道什麼是多線程了,但是你肯定又有一個疑問,難道我們每次開線程都要去new Thread()
嗎?
當然不是,這種方法對於執行單個任務而言確實很方便,但是當我們大量的任務時,這種方法顯然是十分繁瑣的,而且會降低性能,所以我們要引出另一個概念,也就是線程池。
3 . 線程池
Java
提供了Executor
接口來執行線程池中的任務,提供ExecutorServive
接口來管理和控制任務。下面我們來看看Executor
和 ExecutorService
這兩個接口。
public interface Executor {
void execute(Runnable command);
}
可見,Executor
接口只是提供了一個 execute()
方法,通過其函數,我們應該可以得出這個方法是我們執行任務時使用的一個函數,類似於 Thread
類中的start()
方法。
public interface ExecutorService extends Executor {
void shutdown(); //關閉執行器,但是允許完成當前任務。一旦關閉,無法再接收新的任務
List<Runnable> shutdownNow(); //強制關閉執行器,就算當前線程池中存在尚未完成的任務,仍然立即關閉
boolean isShutdown();//如果執行器已經關閉,則返回 true
boolean isTerminated(); //如果線程池中的任務都被終止,則返回true
}
當然,ExecutorService
中還包含其他方法,以上列出的只是幾個常用的方法,可以看到,ExecutorService
是繼承Executor
接口的,但是這些都只是接口,並沒有提供具體的實現方法,我們該如何利用線程池開啓我們的任務呢?當然,還存在一個 Executors
類,這個類中實現了創建線程池的方法,主要有兩種方式
//創建一個線程池,該線程池可併發執行的線程數固定不變。
+ newFixedThreadPool(numberOfThreads : int) : ExecutorService
//創建一個線程池,可以按需要創建新線程,也即線程數可變
+ newCachedThreadPool():ExecutorService
下面我就用線程池的方法,實現A,B,C任務。
public class Main {
public static void main(String[] args){
int i = 0;
ExecutorService executor = Executors.newFixedThreadPool(3);
while(10 > i){
executor.execute(new A());
executor.execute(new B());
executor.execute(new C());
executor.shutdown();
i++;
}
}
}
學過計算機組成原理的人可能知道指令流水線設計,影響流水線性能的有三大原因,其中有一點就是數據相關,也就是由於重疊操作,改變了對操作數讀寫的訪問順序,從而導致了數據相關衝突。在多線程裏面,會不會發生這種情況呢? 答案是肯定的,下面我們來舉一個例子:
設計一個IPhoneShop 類
public class IPhoneShop {
private static int number = 3;
public static void sellIPhone(){
if(0 < number){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
number --;
System.out.println("Sell a IPhone !");
} else {
System.out.println("Sell out !");
}
}
}
類的設計十分簡單,初始化店內只有3臺IPhone,如果賣出去一部,就打印賣出一部IPhone,如IPhone數量爲0,則打印已經賣完了,在sellIPhone()
方法中,我設置了線程睡眠 5 毫秒,是爲了增大差異,更明顯的看出線程的競爭狀態。
這時的主函數爲
public class Main {
public static IPhoneShop mShop = new IPhoneShop();
public static void main(String[] args){
int i = 1;
ExecutorService executor = Executors.newFixedThreadPool(100);
while(100 >= i){
executor.execute(new A());
i++;
}
executor.shutdown();
}
}
A類
public class A implements Runnable{
@Override
public void run() {
Main.mShop.sellIPhone();
}
}
執行之後你會發現有意思的事情
I got a IPhone !
I got a IPhone !
Sell out !
I got a IPhone !
I got a IPhone !
I got a IPhone !
I got a IPhone !
I got a IPhone !
Sell out !
Sell out !
Sell out !
I got a IPhone !
I got a IPhone !
I got a IPhone !
I got a IPhone !
I got a IPhone !
這裏只是粘貼了部分結果,顯然,已經超出了IPhone的數量,這時爲什麼呢?當一個線程通過 sellIPhone()
的 if 判斷時,我們設置了線程睡眠,這個時候,IPhone的數量並沒有減少,而另一個線程又進來了,這個時候 if 判斷也就失去了其意義,因爲在number數量還未來得及減少的時候,很多子線程已經通過了if判斷。
爲了解決這個問題,Java
引入了synchronized
關鍵字,這個關鍵字用於同步線程,以便一次只有一個線程能夠訪問這個方法。也就是說,當一個線程進入這個方法後,這個方法便被加了鎖,別的線程只能夠選擇等待,直到前一個進入的線程執行完爲止,這樣就解決了線程間的競爭問題。
public static synchronized void sellIPhone()
只需要這樣更改,便不會出現以上問題了,這也是synchronized
關鍵字的簡單用法。但是synchronized
是隱式加鎖,具體實現我們並看不到,那麼有沒有一種方法,能夠顯式給方法加鎖呢?
4.使用 Lock 加鎖同步
首先來介紹一下Lock
接口,看看定義
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition()
}
我們主要能夠用到lock()
, unlock()
, newCondition()
三個方法,顧名思義,lock()
用於加鎖,unlock()
用於解鎖, newCondition()
返回綁定到Lock
實例的新的Condition
實例。下面就讓我們使用Lock
方法來進行方法的顯式加鎖。
public class IPhoneShop {
private static int number = 3;
private static Lock mLock = new ReentrantLock();
public static void sellIPhone(){
mLock.lock();
if(0 < number){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
number --;
System.out.println("I got a IPhone !");
} else {
System.out.println("Sell out !");
}
mLock.unlock();
}
}
這樣也就實現了和synchronized
一樣的效果,這樣也顯得更加靈活和直觀。
想象一下剛纔賣IPhone 的場景,如果在賣的同時,又進了新貨,可不可以讓那麼沒有沒到IPhone的用戶先等待一下,等到進新貨時,由店家通知所有沒有買到IPhone的用戶,我們又進了新貨,你們可以來買了。
這就涉及到了線程間的協作問題,還記得Lock 接口中的那個 newCondition()
方法嗎?Condition
接口中存在三個重要的方法,也就是await()
,signal()
, signalAll()
方法。await()
方法可以讓當前線程都處於等待狀態,signal() 方法可以喚醒一個等待的線程,signalAll()
方法可以喚醒所有的等待線程.
public class IPhoneShop {
private static int number = 3;
private static Lock mLock = new ReentrantLock();
private static Condition newCondition = mLock.newCondition();
public static void sellIPhone(){
mLock.lock();
if(0 < number){
number --;
System.out.println("I got a IPhone !");
} else {
System.out.println("Sell out !");
try {
newCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mLock.unlock();
}
public static void getIPhone(){
mLock.lock();
number += 3;
newCondition.signalAll();
mLock.unlock();
}
}
這樣,當IPhone售完後,所有線程將進入等待階段,直到再次進貨,newCondition
喚醒所有的等待線程,再次進入銷售環節。這樣就實現了使用Condition
線程間的協作。
5.信號量
信號量可以用於限制訪問共享資源的線程數,在訪問資源前,必須獲得信號的許可,所以,使用這種方法,也可以達到給方法加鎖的目的,其效果和synchronized
關鍵字和Lock
是類似的。
使用信號量的方法爲
Semaphore mSemaphore = new Semaphore(numberOfPermits:int);
或者
Semaphore mSemaphore = new Semaphore(Permits:int,fairs:boolean);//可以創建一種具有公平策略的信號量,所謂公平策略也就是說等待時間最長的線程獲得方法的使用權
Semaphore
類中有兩個重要方法,acquire()
和 release()
兩個方法,第一個方法表示這個線程已經獲得了方法的使用權,如果獲得使用權的線程數等於指定的數,那麼其他線程將不能再獲得方法的使用權。release()
用於線程釋放使用權。
具體使用如下
Semaphore mSemaphore = new Semaphore(2);
public void method(){
try {
mSemaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
mSemaphore.release();
}
}