Java基礎知識 -- 多線程

  1. 多線程的概念

    線程是指一個任務從頭到尾的執行流,線程提供了一個運行的機制。

    Java中,一個程序中可以併發的啓動多個線程,這也就意味着線程可以在多處理器系統上同一時刻運行。

多線程可以使程序反應更快,執行效率更高。

  1. 多線程編程

以上介紹的概念可能還不夠清晰的解釋什麼是多線程,沒關係,我們舉一個例子看一下。當然,如果我們想要創建一個多線程程序,那麼首先我們應該提供多個任務供我們去執行,想要創建一個這樣的任務,我們需要實現 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接口來管理和控制任務。下面我們來看看ExecutorExecutorService這兩個接口。

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();
    }
}

備註:轉載自https://www.jianshu.com/p/6acf0f74dad5

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