【拒絕一問就懵】之有必要單獨講講線程

有什麼料?

  1. 進一步理解多線程場景下會出現的問題;
  2. 學會正確處理併發操做中的通信和同步。

如今,多瞭解些線程吧

在平常開發中,線程經常被用做爲提高程序效率的重要手段。在CoorChice的這篇文章中,CoorChice介紹了線程的基本運做。連接:html

【拒絕一問就懵】之從Thread講到Handlejava

本篇,CoorChice將從多線程的角度來進一步介紹線程的相關知識。首先,咱們須要瞭解一些基本知識。 面試

主內存和工做內存

  • 主內存
    暫且能夠理解爲內存模型中堆內存。它儲存了進程的全部共享變量。咱們知道,一個進程中可能存在包括主線程在內的多條線程。++主內存中的共享變量是對全部線程可見的。++
  • 工做內存
    爲了提升效率,每一個線程都配有一個私有的工做內存。主內存中的共享變量須要拷貝到線程的私有內存中,以後線程對該變量的操做就是在本身的工做內存中進行的。++當值發生改變時,在線程退出以前,會被更新到主內存中。++

想了解更多和Java內存相關的知識,能夠看看CoorChice的這幾篇文章:
1. 【拒絕一問就懵】之你多少要懂點內存回收機制編程

2.【拒絕一問就懵】之沒據說過內存抖動吧安全

3.【拒絕一問就懵】之不可忽視的內存泄露bash

共享變量和非共享變量

  • 共享變量

    若是一個變量在多條線程的工做內存中都有拷貝,那麼就認定它是一個共享變量。++事實上,類的成員變量、靜態變量都是共享變量。++ 如上所術,共享變量對進程中的全部線程都是可見的。咱們常常遇到的併發問題一般就是由它引發的。網絡

  • 非共享變量:

    就是線程中的私有變量。這些變量對其它線程來講是不可見。當線程退出時,它們會被回收的。非共享變量的值須要經過通信手段才能傳遞到其它線程,這個後面再提。多線程

其它

  • 原子操做:

    就是不可分割的,接二連三的操做。好比後面將要提到的Read操做。併發

  • 可見性:

    一個線程對共享變量值的修改,可以被其它線程即時看到,就稱該共享變量具備可見性。app

由共享變量引起的問題

如今,筒靴們已經知道了共享變量對進程中的全部線程都是可見的。而且當一個線程須要使用它時,須要先拷貝一份到本身的工做內存中,而後再工做內存中操做這個copy的對象。下面這張圖展現線程中操做共享變量的過程。

image

圖中展現了線程對共享變量的讀取/寫入操做。能夠看到,++它們分別由兩個原子操做構成。++ 注意,CoorChice這句話的意思是,一般意義上的讀取一個變量或者寫入一個變量的操做都不是原子操做,而是分兩步完成的。

讀取

  1. read:
    將主內存中的變量值讀取到線程的工做內存中。
  2. load:
    將read到的值賦給新建的拷貝變量。 

寫入

  1. store:
    將線程的工做內存中的,共享變量的拷貝變量的值傳到主內存中。
  2. write:
    將store後的值賦給主內存中共享變量。

你看,不管是讀取仍是寫入,因爲都須要兩步完成,因此就極可能發生中途被中斷的狀況。好比下面這段代碼每次執行的結果都有可能不同。

int goods = 0;

@Test
public void testThread() {
    for (int i = 0; i < 3; i++) {
      new Thread(() -> {
        while (goods != 10) {
          goods++;
          System.out.println(
          Thread.currentThread().getName() + 
            " -> Goods = " + goods);
        }
      }, "Thread - " + i).start();
    }
  }
複製代碼

第一次運行結果:

Thread - 0 -> Goods = 1
Thread - 0 -> Goods = 3
Thread - 0 -> Goods = 4
Thread - 1 -> Goods = 2
Thread - 2 -> Goods = 6
Thread - 2 -> Goods = 8
Thread - 2 -> Goods = 9
Thread - 2 -> Goods = 10
Thread - 0 -> Goods = 5
Thread - 1 -> Goods = 7
複製代碼

第二次運行結果

Thread - 0 -> Goods = 1
Thread - 1 -> Goods = 2
Thread - 0 -> Goods = 3
Thread - 0 -> Goods = 5
Thread - 0 -> Goods = 6
Thread - 2 -> Goods = 7
Thread - 2 -> Goods = 9
Thread - 1 -> Goods = 4
Thread - 2 -> Goods = 10
Thread - 0 -> Goods = 8
複製代碼

這個例子之因此會獲得這種結果,是由於當一個線程執行時,另外一個線程插入執行。關鍵插入的地方可能有:


1. 在共享變量goods讀/寫的過程當中。


2. goods++操做包含的+一、賦值等操做中。

這樣的結果咱們確定是不能接受的,事實上若是操做的是非基本類型變量,那麼你的程序可能會脆弱不堪,隨時面臨着崩潰。咱們但願程序可以高效且正確的運行,就須要解決多線程場景下的通信(信息或數據傳遞)和同步(有序執行)的問題。

image

多線程的通信和同步

目前,咱們大體有兩套解決多線程問題的模型。


  • 基於內存共享的模型。

就是線程之間經過共享內存實現通信,即共享內存中的信息是公共可見的,但須要顯示的進行同步。否則就會出現上面例子中錯亂的問題。不難看出,共享內存模型特色是是隱式通信,顯示同步的。Java選擇的併發解決方案就是基於共享內存的。這就是爲何咱們經常須要在Java使用synchronized或者Lock來進行同步操做的緣由。


  • 基於消息傳遞的模型。

就是線程之間經過發送/接收消息來實現同步。因爲發送消息和接收消息老是具備前後順序的(先有發送,後有接收),因此這種模型的特色是隱式同步,顯示通信,即須要在發送消息的時候附加須要傳遞的信息來進行通訊。Android中的 Handler 就是基於消息傳遞模型的。關於Handler機制 CoorChice的這篇文章中有詳細的講述:【拒絕一問就懵】之從Thread講到Handle

下面,咱們瞭解下Java中的同步手段。

線程同步手段

synchronized

synchronized 關鍵字相信你們都不陌生,咱們經常把它加到方法或代碼塊上用於同步:

public synchronized void testThread() {
    ...
  }
複製代碼

或者這樣來同步代碼塊:

public void testThread() {
    Object object = new Object();
    
    synchronized (this){ //本類實例的對象鎖
      ...
    }
    
    synchronized (object){ //指定的對象鎖
      ...
    }
    
    synchronized (Object.class){ //類鎖。注意,這表示該類全部對象實例同時只能有一個訪問該代碼塊。
      ...
    }
}
複製代碼

在進行同步時,須要時刻注意,你須要把同步加在真正須要同步的地方,而不是大段的進行同步,那樣會有效下降程序效率的!記住:同步粒度儘量的小!

Lock

與sycnhronized相比,Lock至關因而手動實現同步。在Java中,實現了一個ReentrantLock來幫助咱們實現同步。使用起來也比較簡單,咱們只須要在須要同步的代碼塊前段加鎖,末端釋放鎖便可。看個例子吧。

int goods = 0;

public void testThread() {
    Lock lock = new ReentrantLock();
    for (int i = 0; i < 3; i++) {
      new Thread(() -> {
        lock.lock();
        while (goods < 10) {
          goods++;
          System.out.println(
            Thread.currentThread().getName() +
              " -> Goods = " + goods);
        }
        lock.unlock();
      }, "Thread - " + i).start();
    }
  }
複製代碼

一樣是上面那個例子,此次看看運行結果吧。

Thread - 0 -> Goods = 1
Thread - 0 -> Goods = 2
Thread - 0 -> Goods = 3
Thread - 0 -> Goods = 4
Thread - 0 -> Goods = 5
Thread - 0 -> Goods = 6
Thread - 0 -> Goods = 7
Thread - 0 -> Goods = 8
Thread - 0 -> Goods = 9
Thread - 0 -> Goods = 10
複製代碼

使用Lock實現同步須要注意在發生異常的地方及時釋放鎖,不然將會致使其它等待獲取鎖的線程一直阻塞下去!此外,若是使用mLock.tryLock()獲取鎖能夠根據返回值判斷是否成功獲取到了鎖。

final有同步做用嗎?

答案是確定的,可是它只能保證某些狀況下的同步。它們是什麼狀況呢?就是對於不可變對象而言的。不可變對象(成員變量由基本類型或final修飾,或其它不可變對象組成的對象)意味着在安全發佈後,咱們不能再修改它,因此對於全部能夠見到它的線程而言,它是相同的。

對於可變對象(就是非不可變對象嘍,例如普通的List、Map等),即便使用了final進行修飾,在併發場景下,你仍然須要進行顯示的同步。由於可變對象的內容是能夠被修改的。看個例子,筒靴們可能會理解得更清晰。

final AlterableObj obj = new AlterableObj();
@Test
public void testThread_2() {
  for (int i = 0; i < 10; i++) {
    new Thread(() -> {
      while (obj.var < 100) {
        obj.var++;
        System.out.println(
          Thread.currentThread().getName() +
            " -> AlterableObj.var = " + obj.var)
      }
    }, "Thread - " + i).start();
  }
}
class AlterableObj{
  public int var = 0;
}
複製代碼

運行結果比較長,我僅截取一部分能說明問題的:

...
Thread - 2 -> AlterableObj.var = 42
Thread - 2 -> AlterableObj.var = 43
Thread - 2 -> AlterableObj.var = 44
Thread - 1 -> AlterableObj.var = 40
Thread - 4 -> AlterableObj.var = 46
Thread - 4 -> AlterableObj.var = 48
Thread - 4 -> AlterableObj.var = 49
Thread - 4 -> AlterableObj.var = 50
Thread - 4 -> AlterableObj.var = 51
Thread - 3 -> AlterableObj.var = 39
...
複製代碼

看,已經發生錯亂了!因此fianl並不能保證可變對象的同步。

image

volatile有同步做用嗎?

++volatile的主要做用是保證被修飾變量的可見性。++ 這意味着,++被volatile修飾的變量的讀/寫操做相似因而原子性的++,即read和load,stroe和write過程變得連續而不可被中斷。因此,某種意義上說,volatile是有同步做用的,可是範圍很是小,一般不能知足咱們的需求。

此外,volatile 可以在必定程度上保證程序的有序性。JVM在編譯時會對程序進行指令重排,但這不會影響執行結果。若是一個變量被volatile修飾,那麼發生在它讀/寫操做以前的程序指令,必定不會被重排到它的讀/寫操做以後。好比:

volatile int a = 0;

int b = 1;
int c = 2;

int a = 3;

int d = 4;
int e = 5;
複製代碼

上面代碼中,int a = 3像一道屏障同樣,使得int b = 1int c = 2必定發生在int d = 4int e = 5以前。

它們自帶同步屬性

java.util.concurrent包下,Java爲咱們提供了很多經常使用對象的線程安全版,好比AtomicXXX系列ConcurrentXXX系列CopyOnWriteXXX系列等。通常狀況下,你能夠放心的使用它們,而不用擔憂多線程場景下的各類麻煩問題!

使用多線程吧!

如今,筒靴們應該可以合理的使用多線程來提升程序效率了吧。

在Android中,因爲主線程(UI線程)負責繪製界面,因此是萬萬阻塞不得!若是在主線程中不當心混入了耗時操做,後果是很可怕的。輕則致使界面卡頓,重則致使ANR!相關知識能夠看看CoorChice的這篇文章:【拒絕一問就懵】之Activity的啓動流程

對於複雜計算、數據讀/寫、網絡訪問等耗時操做,咱們都應該放到線程中進行。如今設備一般都具有多個cpu,好比8核設備能夠至少並行運行8條線程!不搞點併發操做簡直是暴遣天物啊。咱們只須要謹慎的處理好線程間的通信及同步問題便可。固然,這並不像說的那麼容易,須要多花點時間去思考和嘗試。Java也提供了一些高效且簡化的類來幫助咱們合理的進行併發編程,好比CoorChice在這篇文章中介紹的:【面試必備】簡單瞭解下ExecutorService

總結

  • 抽出空餘時間寫文章分享須要動力,還請各位看官動動小手 【點個贊】,給CoorChice點鼓勵
  • CoorChice一直在不按期的創做新的乾貨,想要上車只需進到【我的主頁】點個關注就行了哦。發車嘍~

本篇主要介紹了關於多線程場景下一些須要注意的點,筒靴們在進行併發操做時須要根據這些特色謹慎的處理線程間的通信和同步。

參考連接

  1. Java Volatile Keyword:http://tutorials.jenkov.com/java-concurrency/volatile.html
  2. Java內存模型(一):http://www.cloudchou.com/softdesign/post-631.html
  3. Java 多線程-可見性問題:https://mritd.me/2016/03/20/Java-%E5%A4%9A%E7%BA%BF%E7%A8%8B-%E5%8F%AF%E8%A7%81%E6%80%A7%E9%97%AE%E9%A2%98/
  4. Java多線程乾貨系列—(四)volatile關鍵字| 掘金技術徵文:https://juejin.im/post/590f451c44d904007beaba1b

看到這裏的童鞋快獎勵本身一口辣條吧!

相關文章
相關標籤/搜索