對於線程和線程池還有線程安全的理解

進程和線程

進程和線程都是一個時間段的描述,是CPU工作時間段的描述,不過是顆粒大小不同。

來自知乎的圖片

 

他們主要區別是:進程不共享內存,線程可以共享內存。

 

線程:

  • CPU中的Thread:
    CPU中的線程,我們也叫它們Thread,和OS中的線程的名字一樣。他們和cpu相關,常說的4核心8線程就是指cpu線程。CPU的Thread就那麼固定幾個,是稀缺資源。
  • 操作系統中的Thread: 操作系統中的進程可以很多,進程中的線程就更多了。軟件操作系統調度的基本單位是OS的Thread。我們開發中所指的就是這個線程。

Thread和Runnable

Java中線程的創建有兩種方式: 1.通過繼承Thread類,重寫Thread的run()方法,將線程運行的邏輯放在其中。

2.通過實現Runnable接口,實例化Thread類。

我們通常使用第二種,因爲可以複用Runnable,更容易實現資源共享,能多個線程同時處理一個資源。

// 1
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("this is a Runnable");
    }
}
// 2
public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("this is thread");
    }
}

// 具體使用
public class Main {
    public static void main(String[] args) {
        // 第一種
        Thread thread1 = new Thread(new MyRunnable());
        thread1.start();
        // 第二種
        MyThread thread2 = new MyThread();
        thread2.start();
    }
}

而實際Android開發工作中,以上兩種都不用,我們通常使用Android提供的Handler和java.util包裏的Executor。

Executor

Executor 是一個接口,execute執行Runnable。

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

看下使用:

      val executor: Executor = Executors.newCachedThreadPool()
        executor.execute { }

點進去newCachedThreadPool,發現返回的是一個ExecutorService。ExecutorService就是Executor的實現了。

        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

ExecutorService

ExecutorService有兩個方法:

void shutdown();是指不再添加任務,執行完已有任務後結束。 List<Runnable> shutdownNow();是立即調用線程的interrupt()結束所有的線程。

ThreadPoolExecutor

上面看到Executors裏面new的是ThreadPoolExecutor,我們看下ThreadPoolExecutor的構造方法:

//五個參數的構造函數
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue)

//六個參數的構造函數-1
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory)

//六個參數的構造函數-2
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler)

//七個參數的構造函數
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 
  • corePoolSize: 該線程池中核心線程數最大值

核心線程:在創建完線程池之後,核心線程先不創建,在接到任務之後創建核心線程。並且會一直存在於線程池中(即使這個線程啥都不幹),有任務要執行時,如果核心線程沒有被佔用,會優先用核心線程執行任務。數量一般情況下設置爲CPU核數的二倍即可。

  • maximumPoolSize: 該線程池中線程總數最大值

線程總數=核心線程數+非核心線程數。

非核心線程:簡單理解,即核心線程都被佔用,但還有任務要做,就創建非核心線程。

  • keepAliveTime: 非核心線程閒置超時時長

這個參數可以理解爲,任務少,但池中線程多,非核心線程不能白養着,超過這個時間不工作的就會被幹掉,但是核心線程會保留。

  • TimeUnit: keepAliveTime的單位

TimeUnit是一個枚舉類型,其包括:

NANOSECONDS:1微毫秒 = 1微秒 / 1000
MICROSECONDS:1微秒 = 1毫秒 / 1000
MILLISECONDS:1毫秒 = 1秒 /1000
SECONDS:秒
MINUTES:分
HOURS:小時
DAYS:天
  • BlockingQueue workQueue: 線程池中的任務隊列

默認情況下,任務進來之後先分配給核心線程執行,核心線程如果都被佔用,並不會立刻開啓非核心線程執行任務,而是將任務插入任務隊列等待執行,核心線程會從任務隊列取任務來執行,任務隊列可以設置最大值,一旦插入的任務足夠多,達到最大值,纔會創建非核心線程執行任務。

常見的workQueue有四種:

  1. SynchronousQueue:這個隊列接收到任務的時候,會直接提交給線程處理,而不保留它,如果所有線程都在工作怎麼辦?那就新建一個線程來處理這個任務!所以爲了保證不出現<線程數達到了maximumPoolSize而不能新建線程>的錯誤,使用這個類型隊列的時候,maximumPoolSize一般指定成Integer.MAX_VALUE,即無限大。

  2. LinkedBlockingQueue:這個隊列接收到任務的時候,如果當前已經創建的核心線程數小於線程池的核心線程數上限,則新建線程(核心線程)處理任務;如果當前已經創建的核心線程數等於核心線程數上限,則進入隊列等待。由於這個隊列沒有最大值限制,即所有超過核心線程數的任務都將被添加到隊列中,這也就導致了maximumPoolSize的設定失效,因爲總線程數永遠不會超過corePoolSize

  3. ArrayBlockingQueue:可以限定隊列的長度,接收到任務的時候,如果沒有達到corePoolSize的值,則新建線程(核心線程)執行任務,如果達到了,則入隊等候,如果隊列已滿,則新建線程(非核心線程)執行任務,又如果總線程數到了maximumPoolSize,並且隊列也滿了,則發生錯誤,或是執行實現定義好的飽和策略。

  4. DelayQueue:隊列內元素必須實現Delayed接口,這就意味着你傳進去的任務必須先實現Delayed接口。這個隊列接收到任務時,首先先入隊,只有達到了指定的延時時間,纔會執行任務。

  • ThreadFactory threadFactory -> 創建線程的工廠

可以用線程工廠給每個創建出來的線程設置名字。一般情況下無須設置該參數。

  • RejectedExecutionHandler handler -> 飽和拒絕策略

這是當任務隊列和線程池都滿了時所採取的應對策略,默認是AbordPolicy。

AbordPolicy:表示無法處理新任務,並拋出 RejectedExecutionException 異常。此外還有3種策略,它們分別如下。

CallerRunsPolicy:用調用者所在的線程來處理任務。此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。

DiscardPolicy:不能執行的任務,並將該任務刪除。

DiscardOldestPolicy:丟棄隊列最近的任務,並執行當前的任務。

四種線程池

Executors類爲我們提供的四種簡單創建線程池的方法:

private val fix = Executors.newFixedThreadPool(4)
private val cache = Executors.newCachedThreadPool()
private val single = Executors.newSingleThreadExecutor()
private val scheduled = Executors.newScheduledThreadPool(4)

其實就是調用不同的ThreadPoolExecutor的構造方法。下面一個一個分析:

 

CachedThreadPool有點像去衝浪,因爲海洋無限大,隨時去都有位置衝浪,一個人衝完60秒內可以免費給下一個人玩。超過60秒衝浪板就被商家回收。

 

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }

可以看出corePoolSize是傳進來的固定值,maximumPoolSize無限大,因爲採用的隊列DelayedWorkQueue是無解的,所以maximumPoolSize參數無效。如果運行的線程達到了corePoolSize時,則將任務添加到DelayedWorkQueue中。DelayedWorkQueue會將任務進行排序,先要執行的任務會放在隊列的前面。在跟此前介紹的線程池不同的是,當執行完任務後,會將ScheduledFutureTask中的time變量改爲下次要執行的時間並放回到DelayedWorkQueue中。

ScheduledThreadPool主要用於執行定時任務以及有固定週期的重複任務。

Callable

Callable是java1.5添加進來的一個增強版本。類似於Runnable,卻又有差異:

 

下面看下使用:

    val executor: ExecutorService = Executors.newSingleThreadExecutor()
    val future: Future<String> = executor.submit(MyCallable())
    try {
        val string: String = future.get()
    } catch (e: ExecutionException) {

    }
    executor.shutdown()
    class MyCallable() : Callable<String> {
        override fun call(): String {
            return "done"
        }
    }

線程安全

JMM

因爲硬件架構,會導致一些問題,特別在多線程的時候更爲突出:

 

 

 

1)線程A把本地內存A中更新過的共享變量刷新到主內存中去。

2)線程B到主內存中去讀取線程A之前已更新過的共享變量。

當對象和變量被存放在計算機中各種不同的內存區域中時,就可能會出現一些具體的問題。Java內存模型建立所圍繞的問題:在多線程併發過程中,如何處理多線程讀同步問題與可見性(多線程緩存與指令重排序)、多線程寫同步問題與原子性。

Java內存模型(即Java Memory Model,簡稱JMM)本身是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,通過這組規範定義了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式。

線程間通信必須要經過主內存。

如下,如果線程A與線程B之間要通信的話,必須要經歷下面2個步驟:

  1. FixedThreadPool

        public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
    

    FixedThreadPool的corePoolSize和maximumPoolSize都設置爲參數nThreads,也就是隻有固定數量的核心線程,不存在非核心線程。keepAliveTime爲0L表示多餘的線程立刻終止,因爲不會產生多餘的線程,所以這個參數是無效的,也就是說線程不會被回收一直保存在線程池。FixedThreadPool的任務隊列採用的是LinkedBlockingQueue。一般我們設置爲cpu核心數+1。

    private val fix = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1)

    FixThreadPool其實就像一堆人排隊上公廁一樣,可以無數多人排隊,但是廁所位置就那麼多,而且沒人上時,廁所閒置着也不會搬走。

  2. SingleThreadPool

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    

    我們可以看到總線程數和核心線程數都是1,所以就只有一個核心線程。該線程池才用鏈表阻塞隊列LinkedBlockingQueue,先進先出原則,所以保證了任務的按順序逐一進行。

    SingleThreadPool可以理解爲公廁裏只有一個坑位,先來先上。

  3. CachedThreadPool

        public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
    

    CachedThreadPool的corePoolSize是0,maximumPoolSize是Int的最大值,也就是說CachedThreadPool沒有核心線程,全部都是非核心線程,並且沒有上限。keepAliveTime是60秒,就是說空閒線程等待新任務60秒,超時則銷燬。此處用到的隊列是阻塞隊列SynchronousQueue,這個隊列沒有緩衝區,所以其中最多隻能存在一個元素,有新的任務則阻塞等待。

    適用於頻繁IO的操作,因爲他們的任務量小,但是任務基數非常龐大,使用核心線程處理的話,數量創建方面就很成問題。

  4. ScheduledThreadPool
  5. Runnable是自從java1.1就有了,而Callable是1.5之後才加上去的。
  6. Callable規定的方法是call(),Runnable規定的方法是run()。
  7. Callable的任務執行後可返回值,而Runnable的任務是不能返回值(是void)。
  8. call方法可以拋出異常,run方法不可以。
  9. 運行Callable任務可以拿到一個Future對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。通過Future對象可以瞭解任務執行情況,可取消任務的執行,還可獲取執行結果。
  10. 加入線程池運行,Runnable使用ExecutorService的execute方法,Callable使用submit方法。
  11. 緩存一致性問題:在多處理器系統中,每個處理器都有自己的高速緩存,而它們又共享同一主內存(MainMemory)。基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但是也引入了新的問題:緩存一致性(CacheCoherence)。當多個處理器的運算任務都涉及同一塊主內存區域時,將可能導致各自的緩存數據不一致的情況,如果真的發生這種情況,那同步回到主內存時以誰的緩存數據爲準呢?爲了解決一致性的問題,需要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操作,這類協議有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等。

     

  12. 指令重排序問題:爲了使得處理器內部的運算單元能儘量被充分利用,處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化,處理器會在計算之後將亂序執行的結果重組,保證該結果與順序執行的結果是一致的,但並不保證程序中各個語句計算的先後順序與輸入代碼中的順序一致。因此,如果存在一個計算任務依賴另一個計算任務的中間結果,那麼其順序性並不能靠代碼的先後順序來保證。與處理器的亂序執行優化類似,Java虛擬機的即時編譯器中也有類似的指令重排序(Instruction Reorder)優化。

  1. 多線程讀同步與可見性 線程對共享變量修改的可見性。當一個線程修改了共享變量的值,其他線程能夠立刻得知這個修改。

  2. 原子性 指一個操作是按原子的方式執行的。要麼該操作不被執行;要麼以原子方式執行,即執行過程中不會被其它線程中斷。

  3. 有序性 有序性是指對於單線程的執行代碼,我們總是認爲代碼的執行是按順序依次執行的,這樣的理解並沒有毛病,畢竟對於單線程而言確實如此,但對於多線程環境,則可能出現亂序現象,因爲程序編譯成機器碼指令後可能會出現指令重排現象,重排後的指令與原指令的順序未必一致,要明白的是,在Java程序中,倘若在本線程內,所有操作都視爲有序行爲,如果是多線程環境下,一個線程中觀察另外一個線程,所有操作都是無序的,前半句指的是單線程內保證串行語義執行的一致性,後半句則指指令重排現象和工作內存與主內存同步延遲現象。

volatile

volatile關鍵字有如下兩個作用

  1. 保證被volatile修飾的共享變量對所有線程總數可見的,也就是當一個線程修改了一個被volatile修飾共享變量的值,新值總數可以被其他線程立即得知。
  2. 禁止指令重排序優化。
//線程1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//線程2
stop = true;

如果線程2改變了stop的值,線程1一定會停止嗎?不一定。當線程2更改了stop變量的值之後,但是還沒來得及寫入主存當中,線程2轉去做其他事情了,那麼線程1由於不知道線程2對stop變量的更改,因此還會一直循環下去。

但是用volatile修飾之後就變得不一樣了:

//線程1
volatile boolean stop = false;
while(!stop){
    doSomething();
}
 
//線程2
stop = true;

第一:使用volatile關鍵字會強制將修改的值立即寫入主存;

第二:使用volatile關鍵字的話,當線程2進行修改時,會導致線程1的工作內存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);

第三:由於線程1的工作內存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時會去主存讀取。

那麼在線程2修改stop值時(當然這裏包括2個操作,修改線程2工作內存中的值,然後將修改後的值寫入內存),會使得線程1的工作內存中緩存變量stop的緩存行無效,然後線程1讀取時,發現自己的緩存行無效,它會等待緩存行對應的主存地址被更新之後,然後去對應的主存讀取最新的值。

那麼線程1讀取到的就是最新的正確的值

這也就是內存模型JMM的內存可見性。

   private volatile int inc = 0;

    void count() {
        inc++;
    }

    void add() {
        new Thread() {
            @Override
            public void run() {
                for (int j = 0; j < 100_00_00; j++) {
                    count();
                }
                System.out.println(inc);
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                for (int j = 0; j < 100_00_00; j++) {
                    count();
                }
                System.out.println(inc);
            }
        }.start();

    }

看這段代碼,2個線程分別加一百萬次。結果會打印出兩百萬次嗎?不會的。可能有的人就會有疑問,不對啊,上面是對變量inc進行自增操作,由於volatile保證了可見性,那麼在每個線程中對inc自增完之後,在其他線程中都能看到修改後的值啊,所以有兩個線程分別進行了一百萬次操作,那麼最終inc的值應該是兩百萬啊。

  這裏面就有一個誤區了,volatile關鍵字能保證可見性沒有錯,但是上面的程序錯在沒能保證原子性。可見性只能保證每次讀取的是最新的值,但是volatile沒辦法保證對變量的操作的原子性。

inc++; 其實是兩個步驟,先加加,然後再賦值。不是原子性操作,所以volatile不能保證線程安全。

synchronized

synchronized是Java中的關鍵字,是利用鎖的機制來實現同步的。Synchronized的作用主要有三個:

  1. 原子性:確保線程互斥的訪問同步代碼;
  2. 可見性:保證共享變量的修改能夠及時可見,其實是通過Java內存模型中的 “對一個變量unlock操作之前,必須要同步到主內存中;如果對一個變量進行lock操作,則將會清空工作內存中此變量的值,在執行引擎使用此變量前,需要重新從主內存中load操作或assign操作初始化變量值” 來保證的;
  3. 有序性:有效解決重排序問題,即 “一個unlock操作先行發生(happen-before)於後面對同一個鎖的lock操作”;

synchronized 可以修飾方法和代碼塊,進入synchronized修飾的方法或者代碼塊的線程,就會獲取monitor對象,monitor也就是Java裏的對象鎖。

下面看下經典的賣票案例:

class Ticket implements Runnable {
    /* 五百張票 */
    private int tickets = 500;

    @Override
    public void run() {

        while (true) {
            //同步鎖
            synchronized (this) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.printf("%s窗口正在賣出第%d張票!\n", Thread.currentThread().getName(), tickets--);
                } else {
                    System.out.printf("%s窗口已售罄\n", Thread.currentThread().getName());
                    System.exit(0);
                }
            }
        }
    }
}
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        Thread thread1= new Thread(ticket);
        Thread thread2 = new Thread(ticket);
        Thread thread3 = new Thread(ticket);
        thread1.start();
        thread2.start();
        thread3.start();
    }

3個線程賣500張票。利用synchronized實現線程安全,下面修改下實現:

class Ticket  {
    /* 五百張票 */
    private int tickets = 500;

    public void sellTckets() {
        while (true) {
            //同步鎖
            synchronized (this) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.printf("%s窗口正在賣出第%d張票!\n", Thread.currentThread().getName(), tickets--);
                } else {
                    System.out.printf("%s窗口已售罄\n", Thread.currentThread().getName());
                    System.exit(0);
                }
            }
        }
    }
}
public static void main(String[] args) {
        final Ticket ticket = new Ticket();
        Thread thread1= new Thread(){
            @Override
            public void run() {
                ticket.sellTckets();
            }
        };
        Thread thread2 = new Thread(){
            @Override
            public void run() {
                ticket.sellTckets();
            }
        };
        Thread thread3 = new Thread(){
            @Override
            public void run() {
                ticket.sellTckets();
            }
        };
        thread1.start();
        thread2.start();
        thread3.start();
    }

一樣的線程安全,多線程賣票,但是現在我不僅要賣票,還要訂餐,賣票和訂餐是兩個互不干涉的操作,但是因爲 synchronized (this)拿到的是同一個對象鎖,所以如果線程1在賣票,那麼線程2就不能拿到對象鎖去訂餐:

class Ticket  {
    /* 二百張票 */
    private int tickets = 200;
    /* 二百份盒飯 */
    private int foods = 200;

    public void sell​​Tckets() {
        while (true) {
            //同步鎖
            synchronized (this) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.printf("%s窗口正在賣出第%d張票!\n", Thread.currentThread().getName(), tickets--);
                } else {
                    System.out.printf("%s窗口車票已售罄\n", Thread.currentThread().getName());
                    System.exit(0);
                }
            }
        }
    }

    public void sellFoods() {
        while (true) {
            //同步鎖
            synchronized (this) {
                if (foods > 0) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.printf("%s窗口正在賣出第%d份盒飯!\n", Thread.currentThread().getName(), foods--);
                } else {
                    System.out.printf("%s窗口盒飯已售罄\n", Thread.currentThread().getName());
                    System.exit(0);
                }
            }
        }
    }

那麼怎麼能多線程訂票的同時,別的線程也可以訂餐呢?用不同的對象即可:

class Ticket {
    private int tickets = 200;
 
    private int foods = 200;
    Object object1 = new Object();
    Object object2 = new Object();

    public void sellTickets() {
        while (true) {
            //同步鎖
            synchronized (object1) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.printf("%s窗口正在賣出第%d張票!\n", Thread.currentThread().getName(), tickets--);
                } else {
                    System.out.printf("%s窗口車票已售罄\n", Thread.currentThread().getName());
                    System.exit(0);
                }
            }
        }
    }

    public void sellFoods() {
        while (true) {
            //同步鎖
            synchronized (object2) {
                if (foods > 0) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.printf("%s窗口正在賣出第%d份盒飯!\n", Thread.currentThread().getName(), foods--);
                } else {
                    System.out.printf("%s窗口盒飯已售罄\n", Thread.currentThread().getName());
                    System.exit(0);
                }
            }
        }
    }
}

這就像你家裏2個臥室,門鎖是一樣的鎖所以都用同一把鑰匙。老王拿着鑰匙進入主臥反鎖了門睡覺,你想去次臥睡,但是鑰匙被老王拿進主臥了。你去不了次臥。只能等他出來把鑰匙給你。怎麼能你倆都去睡覺呢?那就配兩把鑰匙。老王拿着主臥的鑰匙去了主臥,你拿着次臥的鑰匙去次臥睡。

 

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