萬字超強圖文講解AQS以及ReentrantLock應用(建議收藏)_台中搬家公司

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

| 好看請贊,養成習慣

  • 你有一個思想,我有一個思想,我們交換后,一個人就有兩個思想

  • If you can NOT explain it simply, you do NOT understand it well enough

現陸續將Demo代碼和技術文章整理在一起 Github實踐精選 ,方便大家閱讀查看,本文同樣收錄在此,覺得不錯,還請Star

寫在前面

進入源碼階段了,寫了十幾篇的 併發系列 知識鋪墊終於要派上用場了。相信很多人已經忘了其中的一些理論知識,別擔心,我會在源碼環節帶入相應的理論知識點幫助大家回憶,做到理論與實踐相結合,另外這是超長圖文,建議收藏,如果對你有用還請點贊讓更多人看到

Java SDK 為什麼要設計 Lock

曾幾何時幻想過,如果 Java 併發控制只有 synchronized 多好,只有下面三種使用方式,簡單方便

public class ThreeSync {

	private static final Object object = new Object();

	public synchronized void normalSyncMethod(){
		//臨界區
	}

	public static synchronized void staticSyncMethod(){
		//臨界區
	}

	public void syncBlockMethod(){
		synchronized (object){
			//臨界區
		}
	}
}

如果在 Java 1.5之前,確實是這樣,自從 1.5 版本 Doug Lea 大師就重新造了一個輪子 Lock

我們常說:“避免重複造輪子”,如果有了輪子還是要堅持再造個輪子,那麼肯定傳統的輪子在某些應用場景中不能很好的解決問題

不知你是否還記得 Coffman 總結的四個可以發生死鎖的情形 ,其中【不可剝奪條件】是指:

線程已經獲得資源,在未使用完之前,不能被剝奪,只能在使用完時自己釋放

要想破壞這個條件,就需要具有申請不到進一步資源就釋放已有資源的能力

很顯然,這個能力是 synchronized 不具備的,使用 synchronized ,如果線程申請不到資源就會進入阻塞狀態,我們做什麼也改變不了它的狀態,這是 synchronized 輪子的致命弱點,這就強有力的給了重造輪子 Lock 的理由

顯式鎖 Lock

舊輪子有弱點,新輪子就要解決這些問題,所以要具備不會阻塞的功能,下面的三個方案都是解決這個問題的好辦法(看下面表格描述你就明白三個方案的含義了)

特性 描述 API
能響應中斷 如果不能自己釋放,那可以響應中斷也是很好的。Java多線程中斷機制 專門描述了中斷過程,目的是通過中斷信號來跳出某種狀態,比如阻塞 lockInterruptbly()
非阻塞式的獲取鎖 嘗試獲取,獲取不到不會阻塞,直接返回 tryLock()
支持超時 給定一個時間限制,如果一段時間內沒獲取到,不是進入阻塞狀態,同樣直接返回 tryLock(long time, timeUnit)

好的方案有了,但魚和熊掌不可兼得,Lock 多了 synchronized 不具備的特性,自然不會像 synchronized 那樣一個關鍵字三個玩法走遍全天下,在使用上也相對複雜了一丟丟

Lock 使用範式

synchronized 有標準用法,這樣的優良傳統咱 Lock 也得有,相信很多人都知道使用 Lock 的一個範式

Lock lock = new ReentrantLock();
lock.lock();
try{
	...
}finally{
	lock.unlock();
}

既然是範式(沒事不要挑戰更改寫法的那種),肯定有其理由,我們來看一下

標準1—finally 中釋放鎖

這個大家應該都會明白,在 finally 中釋放鎖,目的是保證在獲取到鎖之後,最終能被釋放

標準2—在 try{} 外面獲取鎖

不知道你有沒有想過,為什麼會有標準 2 的存在,我們通常是“喜歡” try 住所有內容,生怕發生異常不能捕獲的

try{} 外獲取鎖主要考慮兩個方面:

  1. 如果沒有獲取到鎖就拋出異常,最終釋放鎖肯定是有問題的,因為還未曾擁有鎖談何釋放鎖呢
  2. 如果在獲取鎖時拋出了異常,也就是當前線程並未獲取到鎖,但執行到 finally 代碼時,如果恰巧別的線程獲取到了鎖,則會被釋放掉(無故釋放)

不同鎖的實現方式略有不同,範式的存在就是要避免一切問題的出現,所以大家盡量遵守範式

Lock 是怎樣起到鎖的作用呢?

如果你熟悉 synchronized,你知道程序編譯成 CPU 指令后,在臨界區會有 moniterentermoniterexit 指令的出現,可以理解成進出臨界區的標識

從範式上來看:

  • lock.lock() 獲取鎖,“等同於” synchronized 的 moniterenter指令

  • lock.unlock() 釋放鎖,“等同於” synchronized 的 moniterexit 指令

那 Lock 是怎麼做到的呢?

這裏先簡單說明一下,這樣一會到源碼分析時,你可以遠觀設計輪廓,近觀實現細節,會變得越發輕鬆

其實很簡單,比如在 ReentrantLock 內部維護了一個 volatile 修飾的變量 state,通過 CAS 來進行讀寫(最底層還是交給硬件來保證原子性和可見性),如果CAS更改成功,即獲取到鎖,線程進入到 try 代碼塊繼續執行;如果沒有更改成功,線程會被【掛起】,不會向下執行

但 Lock 是一個接口,裏面根本沒有 state 這個變量的存在:

它怎麼處理這個 state 呢?很顯然需要一點設計的加成了,接口定義行為,具體都是需要實現類的

Lock 接口的實現類基本都是通過【聚合】了一個【隊列同步器】的子類完成線程訪問控制的

那什麼是隊列同步器呢? (這應該是你見過的最強標題黨,聊了半個世紀才入正題,評論區留言罵我)

隊列同步器 AQS

隊列同步器 (AbstractQueuedSynchronizer),簡稱同步器或AQS,就是我們今天的主人公

問:為什麼你分析 JUC 源碼,要從 AQS 說起呢?

答:看下圖

相信看到這個截圖你就明白一二了,你聽過的,面試常被問起的,工作中常用的

  • ReentrantLock
  • ReentrantReadWriteLock
  • Semaphore(信號量)
  • CountDownLatch
  • 公平鎖
  • 非公平鎖
  • ThreadPoolExecutor (關於線程池的理解,可以查看 為什麼要使用線程池? )

都和 AQS 有直接關係,所以了解 AQS 的抽象實現,在此基礎上再稍稍查看上述各類的實現細節,很快就可以全部搞定,不至於查看源碼時一頭霧水,丟失主線

上面提到,在鎖的實現類中會聚合同步器,然後利同步器實現鎖的語義,那麼問題來了:

為什麼要用聚合模式,怎麼進一步理解鎖和同步器的關係呢?

我們絕大多數都是在使用鎖,實現鎖之後,其核心就是要使用方便

從 AQS 的類名稱和修飾上來看,這是一個抽象類,所以從設計模式的角度來看同步器一定是基於【模版模式】來設計的,使用者需要繼承同步器,實現自定義同步器,並重寫指定方法,隨後將同步器組合在自定義的同步組件中,並調用同步器的模版方法,而這些模版方法又回調用使用者重寫的方法

我不想將上面的解釋說的這麼抽象,其實想理解上面這句話,我們只需要知道下面兩個問題就好了

  1. 哪些是自定義同步器可重寫的方法?
  2. 哪些是抽象同步器提供的模版方法?

同步器可重寫的方法

同步器提供的可重寫方法只有5個,這大大方便了鎖的使用者:

按理說,需要重寫的方法也應該有 abstract 來修飾的,為什麼這裏沒有?原因其實很簡單,上面的方法我已經用顏色區分成了兩類:

  • 獨佔式
  • 共享式

自定義的同步組件或者鎖不可能既是獨佔式又是共享式,為了避免強制重寫不相干方法,所以就沒有 abstract 來修飾了,但要拋出異常告知不能直接使用該方法:

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

暖暖的很貼心(如果你有類似的需求也可以仿照這樣的設計)

表格方法描述中所說的同步狀態就是上文提到的有 volatile 修飾的 state,所以我們在重寫上面幾個方法時,還要通過同步器提供的下面三個方法(AQS 提供的)來獲取或修改同步狀態:

而獨佔式和共享式操作 state 變量的區別也就很簡單了

所以你看到的 ReentrantLock ReentrantReadWriteLock Semaphore(信號量) CountDownLatch 這幾個類其實僅僅是在實現以上幾個方法上略有差別,其他的實現都是通過同步器的模版方法來實現的,到這裡是不是心情放鬆了許多呢?我們來看一看模版方法:

同步器提供的模版方法

上面我們將同步器的實現方法分為獨佔式和共享式兩類,模版方法其實除了提供以上兩類模版方法之外,只是多了響應中斷超時限制 的模版方法供 Lock 使用,來看一下

先不用記上述方法的功能,目前你只需要了解個大概功能就好。另外,相信你也注意到了:

上面的方法都有 final 關鍵字修飾,說明子類不能重寫這個方法

看到這你也許有點亂了,我們稍微歸納一下:

程序員還是看代碼心裏踏實一點,我們再來用代碼說明一下上面的關係(注意代碼中的註釋,以下的代碼並不是很嚴謹,只是為了簡單說明上圖的代碼實現):

package top.dayarch.myjuc;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 自定義互斥鎖
 *
 * @author tanrgyb
 * @date 2020/5/23 9:33 PM
 */
public class MyMutex implements Lock {

	// 靜態內部類-自定義同步器
	private static class MySync extends AbstractQueuedSynchronizer{
		@Override
		protected boolean tryAcquire(int arg) {
			// 調用AQS提供的方法,通過CAS保證原子性
			if (compareAndSetState(0, arg)){
				// 我們實現的是互斥鎖,所以標記獲取到同步狀態(更新state成功)的線程,
				// 主要為了判斷是否可重入(一會兒會說明)
				setExclusiveOwnerThread(Thread.currentThread());
				//獲取同步狀態成功,返回 true
				return true;
			}
			// 獲取同步狀態失敗,返回 false
			return false;
		}

		@Override
		protected boolean tryRelease(int arg) {
			// 未擁有鎖卻讓釋放,會拋出IMSE
			if (getState() == 0){
				throw new IllegalMonitorStateException();
			}
			// 可以釋放,清空排它線程標記
			setExclusiveOwnerThread(null);
			// 設置同步狀態為0,表示釋放鎖
			setState(0);
			return true;
		}

		// 是否獨佔式持有
		@Override
		protected boolean isHeldExclusively() {
			return getState() == 1;
		}

		// 後續會用到,主要用於等待/通知機制,每個condition都有一個與之對應的條件等待隊列,在鎖模型中說明過
		Condition newCondition() {
			return new ConditionObject();
		}
	}

  // 聚合自定義同步器
	private final MySync sync = new MySync();


	@Override
	public void lock() {
		// 阻塞式的獲取鎖,調用同步器模版方法獨佔式,獲取同步狀態
		sync.acquire(1);
	}

	@Override
	public void lockInterruptibly() throws InterruptedException {
		// 調用同步器模版方法可中斷式獲取同步狀態
		sync.acquireInterruptibly(1);
	}

	@Override
	public boolean tryLock() {
		// 調用自己重寫的方法,非阻塞式的獲取同步狀態
		return sync.tryAcquire(1);
	}

	@Override
	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
		// 調用同步器模版方法,可響應中斷和超時時間限制
		return sync.tryAcquireNanos(1, unit.toNanos(time));
	}

	@Override
	public void unlock() {
		// 釋放鎖
		sync.release(1);
	}

	@Override
	public Condition newCondition() {
		// 使用自定義的條件
		return sync.newCondition();
	}
}

如果你現在打開 IDE, 你會發現上文提到的 ReentrantLock ReentrantReadWriteLock Semaphore(信號量) CountDownLatch 都是按照這個結構實現,所以我們就來看一看 AQS 的模版方法到底是怎麼實現鎖

AQS實現分析

從上面的代碼中,你應該理解了lock.tryLock() 非阻塞式獲取鎖就是調用自定義同步器重寫的 tryAcquire() 方法,通過 CAS 設置state 狀態,不管成功與否都會馬上返回;那麼 lock.lock() 這種阻塞式的鎖是如何實現的呢?

有阻塞就需要排隊,實現排隊必然需要隊列

CLH:Craig、Landin and Hagersten 隊列,是一個單向鏈表,AQS中的隊列是CLH變體的虛擬雙向隊列(FIFO)——概念了解就好,不要記

隊列中每個排隊的個體就是一個 Node,所以我們來看一下 Node 的結構

Node 節點

AQS 內部維護了一個同步隊列,用於管理同步狀態。

  • 當線程獲取同步狀態失敗時,就會將當前線程以及等待狀態等信息構造成一個 Node 節點,將其加入到同步隊列中尾部,阻塞該線程
  • 當同步狀態被釋放時,會喚醒同步隊列中“首節點”的線程獲取同步狀態

為了將上述步驟弄清楚,我們需要來看一看 Node 結構 (如果你能打開 IDE 一起看那是極好的)

乍一看有點雜亂,我們還是將其歸類說明一下:

上面這幾個狀態說明有個印象就好,有了Node 的結構說明鋪墊,你也就能想象同步隊列的接本結構了:

前置知識基本鋪墊完畢,我們來看一看獨佔式獲取同步狀態的整個過程

獨佔式獲取同步狀態

故事要從範式lock.lock() 開始

public void lock() {
	// 阻塞式的獲取鎖,調用同步器模版方法,獲取同步狀態
	sync.acquire(1);
}

進入AQS的模版方法 acquire()

public final void acquire(int arg) {
  // 調用自定義同步器重寫的 tryAcquire 方法
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

首先,也會嘗試非阻塞的獲取同步狀態,如果獲取失敗(tryAcquire返回false),則會調用 addWaiter 方法構造 Node 節點(Node.EXCLUSIVE 獨佔式)並安全的(CAS)加入到同步隊列【尾部】

    private Node addWaiter(Node mode) {
      	// 構造Node節點,包含當前線程信息以及節點模式【獨佔/共享】
        Node node = new Node(Thread.currentThread(), mode);
      	// 新建變量 pred 將指針指向tail指向的節點
        Node pred = tail;
      	// 如果尾節點不為空
        if (pred != null) {
          	// 新加入的節點前驅節點指向尾節點
            node.prev = pred;

          	// 因為如果多個線程同時獲取同步狀態失敗都會執行這段代碼
            // 所以,通過 CAS 方式確保安全的設置當前節點為最新的尾節點
            if (compareAndSetTail(pred, node)) {
              	// 曾經的尾節點的後繼節點指向當前節點
                pred.next = node;
              	// 返回新構建的節點
                return node;
            }
        }
      	// 尾節點為空,說明當前節點是第一個被加入到同步隊列中的節點
      	// 需要一個入隊操作
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
      	// 通過“死循環”確保節點被正確添加,最終將其設置為尾節點之後才會返回,這裏使用 CAS 的理由和上面一樣
        for (;;) {
            Node t = tail;
          	// 第一次循環,如果尾節點為 null
            if (t == null) { // Must initialize
              	// 構建一個哨兵節點,並將頭部指針指向它
                if (compareAndSetHead(new Node()))
                  	// 尾部指針同樣指向哨兵節點
                    tail = head;
            } else {
              	// 第二次循環,將新節點的前驅節點指向t
                node.prev = t;
              	// 將新節點加入到隊列尾節點
                if (compareAndSetTail(t, node)) {
                  	// 前驅節點的後繼節點指向當前新節點,完成雙向隊列
                    t.next = node;
                    return t;
                }
            }
        }
    }

你可能比較迷惑 enq() 的處理方式,進入該方法就是一個“死循環”,我們就用圖來描述它是怎樣跳出循環的

有些同學可能會有疑問,為什麼會有哨兵節點?

哨兵,顧名思義,是用來解決國家之間邊界問題的,不直接參与生產活動。同樣,計算機科學中提到的哨兵,也用來解決邊界問題,如果沒有邊界,指定環節,按照同樣算法可能會在邊界處發生異常,比如要繼續向下分析的 acquireQueued() 方法

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
          	// "死循環",嘗試獲取鎖,或者掛起
            for (;;) {
              	// 獲取當前節點的前驅節點
                final Node p = node.predecessor();
              	// 只有當前節點的前驅節點是頭節點,才會嘗試獲取鎖
              	// 看到這你應該理解添加哨兵節點的含義了吧
                if (p == head && tryAcquire(arg)) {
                  	// 獲取同步狀態成功,將自己設置為頭
                    setHead(node);
                  	// 將哨兵節點的後繼節點置為空,方便GC
                    p.next = null; // help GC
                    failed = false;
                  	// 返回中斷標識
                    return interrupted;
                }
              	// 當前節點的前驅節點不是頭節點
              	//【或者】當前節點的前驅節點是頭節點但獲取同步狀態失敗
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

獲取同步狀態成功會返回可以理解了,但是如果失敗就會一直陷入到“死循環”中浪費資源嗎?很顯然不是,shouldParkAfterFailedAcquire(p, node)parkAndCheckInterrupt() 就會將線程獲取同步狀態失敗的線程掛起,我們繼續向下看

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
      	// 獲取前驅節點的狀態
        int ws = pred.waitStatus;
      	// 如果是 SIGNAL 狀態,即等待被佔用的資源釋放,直接返回 true
      	// 準備繼續調用 parkAndCheckInterrupt 方法
        if (ws == Node.SIGNAL)
            return true;
      	// ws 大於0說明是CANCELLED狀態,
        if (ws > 0) {
            // 循環判斷前驅節點的前驅節點是否也為CANCELLED狀態,忽略該狀態的節點,重新連接隊列
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
          	// 將當前節點的前驅節點設置為設置為 SIGNAL 狀態,用於後續喚醒操作
          	// 程序第一次執行到這返回為false,還會進行外層第二次循環,最終從代碼第7行返回
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

到這裏你也許有個問題:

這個地方設置前驅節點為 SIGNAL 狀態到底有什麼作用?

保留這個問題,我們陸續揭曉

如果前驅節點的 waitStatus 是 SIGNAL狀態,即 shouldParkAfterFailedAcquire 方法會返回 true ,程序會繼續向下執行 parkAndCheckInterrupt 方法,用於將當前線程掛起

    private final boolean parkAndCheckInterrupt() {
      	// 線程掛起,程序不會繼續向下執行
        LockSupport.park(this);
      	// 根據 park 方法 API描述,程序在下述三種情況會繼續向下執行
      	// 	1. 被 unpark 
      	// 	2. 被中斷(interrupt)
      	// 	3. 其他不合邏輯的返回才會繼續向下執行
      	
      	// 因上述三種情況程序執行至此,返回當前線程的中斷狀態,並清空中斷狀態
      	// 如果由於被中斷,該方法會返回 true
        return Thread.interrupted();
    }

被喚醒的程序會繼續執行 acquireQueued 方法里的循環,如果獲取同步狀態成功,則會返回 interrupted = true 的結果

程序繼續向調用棧上層返回,最終回到 AQS 的模版方法 acquire

public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

你也許會有疑惑:

程序已經成功獲取到同步狀態並返回了,怎麼會有個自我中斷呢?

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

如果你不能理解中斷,強烈建議你回看 Java多線程中斷機制

到這裏關於獲取同步狀態我們還遺漏了一條線,acquireQueued 的 finally 代碼塊如果你仔細看你也許馬上就會有疑惑:

到底什麼情況才會執行 if(failed) 裏面的代碼 ?

if (failed)
  cancelAcquire(node);

這段代碼被執行的條件是 failed 為 true,正常情況下,如果跳出循環,failed 的值為false,如果不能跳出循環貌似怎麼也不能執行到這裏,所以只有不正常的情況才會執行到這裏,也就是會發生異常,才會執行到此處

查看 try 代碼塊,只有兩個方法會拋出異常:

  • node.processor() 方法

    台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

    還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

  • 自己重寫的 tryAcquire() 方法

先看前者:

很顯然,這裏拋出的異常不是重點,那就以 ReentrantLock 重寫的 tryAcquire() 方法為例

另外,上面分析 shouldParkAfterFailedAcquire 方法還對 CANCELLED 的狀態進行了判斷,那麼

什麼時候會生成取消狀態的節點呢?

答案就在 cancelAcquire 方法中, 我們來看看 cancelAcquire到底怎麼設置/處理 CANNELLED 的

	private void cancelAcquire(Node node) {
        // 忽略無效節點
        if (node == null)
            return;
				// 將關聯的線程信息清空
        node.thread = null;

        // 跳過同樣是取消狀態的前驅節點
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // 跳出上面循環后找到前驅有效節點,並獲取該有效節點的後繼節點
        Node predNext = pred.next;

        // 將當前節點的狀態置為 CANCELLED
        node.waitStatus = Node.CANCELLED;

        // 如果當前節點處在尾節點,直接從隊列中刪除自己就好
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            int ws;
          	// 1. 如果當前節點的有效前驅節點不是頭節點,也就是說當前節點不是頭節點的後繼節點
            if (pred != head &&
                // 2. 判斷當前節點有效前驅節點的狀態是否為 SIGNAL
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 // 3. 如果不是,嘗試將前驅節點的狀態置為 SIGNAL
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                // 判斷當前節點有效前驅節點的線程信息是否為空
                pred.thread != null) {
              	// 上述條件滿足
                Node next = node.next;
              	// 將當前節點有效前驅節點的後繼節點指針指向當前節點的後繼節點
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
              	// 如果當前節點的前驅節點是頭節點,或者上述其他條件不滿足,就喚醒當前節點的後繼節點
                unparkSuccessor(node);
            }
						
            node.next = node; // help GC
        }

看到這個註釋你可能有些亂了,其核心目的就是從等待隊列中移除 CANCELLED 的節點,並重新拼接整個隊列,總結來看,其實設置 CANCELLED 狀態節點只是有三種情況,我們通過畫圖來分析一下:

至此,獲取同步狀態的過程就結束了,我們簡單的用流程圖說明一下整個過程

獲取鎖的過程就這樣的結束了,先暫停幾分鐘整理一下自己的思路。我們上面還沒有說明 SIGNAL 的作用, SIGNAL 狀態信號到底是干什麼用的?這就涉及到鎖的釋放了,我們來繼續了解,整體思路和鎖的獲取是一樣的, 但是釋放過程就相對簡單很多了

獨佔式釋放同步狀態

故事要從 unlock() 方法說起

	public void unlock() {
		// 釋放鎖
		sync.release(1);
	}

調用 AQS 模版方法 release,進入該方法

    public final boolean release(int arg) {
      	// 調用自定義同步器重寫的 tryRelease 方法嘗試釋放同步狀態
        if (tryRelease(arg)) {
          	// 釋放成功,獲取頭節點
            Node h = head;
          	// 存在頭節點,並且waitStatus不是初始狀態
          	// 通過獲取的過程我們已經分析了,在獲取的過程中會將 waitStatus的值從初始狀態更新成 SIGNAL 狀態
            if (h != null && h.waitStatus != 0)
              	// 解除線程掛起狀態
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

查看 unparkSuccessor 方法,實際是要喚醒頭節點的後繼節點

    private void unparkSuccessor(Node node) {      
      	// 獲取頭節點的waitStatus
        int ws = node.waitStatus;
        if (ws < 0)
          	// 清空頭節點的waitStatus值,即置為0
            compareAndSetWaitStatus(node, ws, 0);
      
      	// 獲取頭節點的後繼節點
        Node s = node.next;
      	// 判斷當前節點的後繼節點是否是取消狀態,如果是,需要移除,重新連接隊列
        if (s == null || s.waitStatus > 0) {
            s = null;
          	// 從尾節點向前查找,找到隊列第一個waitStatus狀態小於0的節點
            for (Node t = tail; t != null && t != node; t = t.prev)
              	// 如果是獨佔式,這裏小於0,其實就是 SIGNAL
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
          	// 解除線程掛起狀態
            LockSupport.unpark(s.thread);
    }

有同學可能有疑問:

為什麼這個地方是從隊列尾部向前查找不是 CANCELLED 的節點?

原因有兩個:

第一,先回看節點加入隊列的情景:

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

節點入隊並不是原子操作,代碼第6、7行

node.prev = pred; 
compareAndSetTail(pred, node) 

這兩個地方可以看作是尾節點入隊的原子操作,如果此時代碼還沒執行到 pred.next = node; 這時又恰巧執行了unparkSuccessor方法,就沒辦法從前往後找了,因為後繼指針還沒有連接起來,所以需要從后往前找

第二點原因,在上面圖解產生 CANCELLED 狀態節點的時候,先斷開的是 Next 指針,Prev指針並未斷開,因此這也是必須要從后往前遍歷才能夠遍歷完全部的Node

同步狀態至此就已經成功釋放了,之前獲取同步狀態被掛起的線程就會被喚醒,繼續從下面代碼第 3 行返回執行:

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

繼續返回上層調用棧, 從下面代碼15行開始執行,重新執行循環,再次嘗試獲取同步狀態

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

到這裏,關於獨佔式獲取/釋放鎖的流程已經閉環了,但是關於 AQS 的另外兩個模版方法還沒有介紹

  • 響應中斷
  • 超時限制

獨佔式響應中斷獲取同步狀態

故事要從lock.lockInterruptibly() 方法說起

	public void lockInterruptibly() throws InterruptedException {
		// 調用同步器模版方法可中斷式獲取同步狀態
		sync.acquireInterruptibly(1);
	}

有了前面的理解,理解獨佔式可響應中斷的獲取同步狀態方式,真是一眼就能明白了:

    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
      	// 嘗試非阻塞式獲取同步狀態失敗,如果沒有獲取到同步狀態,執行代碼7行
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

繼續查看 doAcquireInterruptibly 方法:

    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                  	// 獲取中斷信號后,不再返回 interrupted = true 的值,而是直接拋出 InterruptedException 
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

沒想到 JDK 內部也有如此相近的代碼,可響應中斷獲取鎖沒什麼深奧的,就是被中斷拋出 InterruptedException 異常(代碼第17行),這樣就逐層返回上層調用棧捕獲該異常進行下一步操作了

趁熱打鐵,來看看另外一個模版方法:

獨佔式超時限制獲取同步狀態

這個很好理解,就是給定一個時限,在該時間段內獲取到同步狀態,就返回 true, 否則,返回 false。好比線程給自己定了一個鬧鐘,鬧鈴一響,線程就自己返回了,這就不會使自己是阻塞狀態了

既然涉及到超時限制,其核心邏輯肯定是計算時間間隔,因為在超時時間內,肯定是多次嘗試獲取鎖的,每次獲取鎖肯定有時間消耗,所以計算時間間隔的邏輯就像我們在程序打印程序耗時 log 那麼簡單

nanosTimeout = deadline – System.nanoTime()

故事要從 lock.tryLock(time, unit) 方法說起

	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
		// 調用同步器模版方法,可響應中斷和超時時間限制
		return sync.tryAcquireNanos(1, unit.toNanos(time));
	}

來看 tryAcquireNanos 方法

    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

是不是和上面 acquireInterruptibly 方法長相很詳細了,繼續查看來 doAcquireNanos 方法,看程序, 該方法也是 throws InterruptedException,我們在中斷文章中說過,方法標記上有 throws InterruptedException 說明該方法也是可以響應中斷的,所以你可以理解超時限制是 acquireInterruptibly 方法的加強版,具有超時和非阻塞控制的雙保險

    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
      	// 超時時間內,為獲取到同步狀態,直接返回false
        if (nanosTimeout <= 0L)
            return false;
      	// 計算超時截止時間
        final long deadline = System.nanoTime() + nanosTimeout;
      	// 以獨佔方式加入到同步隊列中
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
              	// 計算新的超時時間
                nanosTimeout = deadline - System.nanoTime();
              	// 如果超時,直接返回 false
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                		// 判斷是最新超時時間是否大於閾值 1000    
                    nanosTimeout > spinForTimeoutThreshold)
                  	// 掛起線程 nanosTimeout 長時間,時間到,自動返回
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

上面的方法應該不是很難懂,但是又同學可能在第 27 行上有所困惑

為什麼 nanosTimeout 和 自旋超時閾值1000進行比較?

    /**
     * The number of nanoseconds for which it is faster to spin
     * rather than to use timed park. A rough estimate suffices
     * to improve responsiveness with very short timeouts.
     */
    static final long spinForTimeoutThreshold = 1000L;

其實 doc 說的很清楚,說白了,1000 nanoseconds 時間已經非常非常短暫了,沒必要再執行掛起和喚醒操作了,不如直接當前線程直接進入下一次循環

到這裏,我們自定義的 MyMutex 只差 Condition 沒有說明了,不知道你累了嗎?我還在堅持

Condition

如果你看過之前寫的 併發編程之等待通知機制 ,你應該對下面這個圖是有印象的:

如果當時你理解了這個模型,再看 Condition 的實現,根本就不是問題了,首先 Condition 還是一個接口,肯定也是需要有實現類的

那故事就從 lock.newnewCondition 說起吧

	public Condition newCondition() {
		// 使用自定義的條件
		return sync.newCondition();
	}

自定義同步器重封裝了該方法:

		Condition newCondition() {
			return new ConditionObject();
		}

ConditionObject 就是 Condition 的實現類,該類就定義在了 AQS 中,只有兩個成員變量:

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

所以,我們只需要來看一下 ConditionObject 實現的 await / signal 方法來使用這兩個成員變量就可以了

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
          	// 同樣構建 Node 節點,並加入到等待隊列中
            Node node = addConditionWaiter();
          	// 釋放同步狀態
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
              	// 掛起當前線程
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

這裏注意用詞,在介紹獲取同步狀態時,addWaiter 是加入到【同步隊列】,就是上圖說的入口等待隊列,這裏說的是【等待隊列】,所以 addConditionWaiter 肯定是構建了一個自己的隊列:

        private Node addConditionWaiter() {
            Node t = lastWaiter;
            
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
          	// 新構建的節點的 waitStatus 是 CONDITION,注意不是 0 或 SIGNAL 了
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
          	// 構建單向同步隊列
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

這裡有朋友可能會有疑問:

為什麼這裡是單向隊列,也沒有使用CAS 來保證加入隊列的安全性呢?

因為 await 是 Lock 範式 try 中使用的,說明已經獲取到鎖了,所以就沒必要使用 CAS 了,至於是單向,因為這裏還不涉及到競爭鎖,只是做一個條件等待隊列

在 Lock 中可以定義多個條件,每個條件都會對應一個 條件等待隊列,所以將上圖豐富說明一下就變成了這個樣子:

線程已經按相應的條件加入到了條件等待隊列中,那如何再嘗試獲取鎖呢?signal / signalAll 方法就已經排上用場了

        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

Signal 方法通過調用 doSignal 方法,只喚醒條件等待隊列中的第一個節點

        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
              	// 調用該方法,將條件等待隊列的線程節點移動到同步隊列中
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

繼續看 transferForSignal 方法

    final boolean transferForSignal(Node node) {       
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

       	// 重新進行入隊操作
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
          	// 喚醒同步隊列中該線程
            LockSupport.unpark(node.thread);
        return true;
    }

所以我們再用圖解一下喚醒的整個過程

到這裏,理解 signalAll 就非常簡單了,只不過循環判斷是否還有 nextWaiter,如果有就像 signal 操作一樣,將其從條件等待隊列中移到同步隊列中

        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

不知你還是否記得,我在併發編程之等待通知機制 中還說過一句話

沒有特殊原因盡量用 signalAll 方法

什麼時候可以用 signal 方法也在其中做了說明,請大家自行查看吧

這裏我還要多說一個細節,從條件等待隊列移到同步隊列是有時間差的,所以使用 await() 方法也是範式的, 同樣在該文章中做了解釋

有時間差,就會有公平和不公平的問題,想要全面了解這個問題,我們就要走近 ReentrantLock 中來看了,除了了解公平/不公平問題,查看 ReentrantLock 的應用還是要反過來驗證它使用的AQS的,我們繼續吧

ReentrantLock 是如何應用的AQS

獨佔式的典型應用就是 ReentrantLock 了,我們來看看它是如何重寫這個方法的

乍一看挺奇怪的,怎麼裏面自定義了三個同步器:其實 NonfairSync,FairSync 只是對 Sync 做了進一步劃分:

從名稱上你應該也知道了,這就是你聽到過的 公平鎖/非公平鎖

何為公平鎖/非公平鎖?

生活中,排隊講求先來後到視為公平。程序中的公平性也是符合請求鎖的絕對時間的,其實就是 FIFO,否則視為不公平

我們來對比一下 ReentrantLock 是如何實現公平鎖和非公平鎖的

其實沒什麼大不了,公平鎖就是判斷同步隊列是否還有先驅節點的存在,只有沒有先驅節點才能獲取鎖;而非公平鎖是不管這個事的,能獲取到同步狀態就可以,就這麼簡單,那問題來了:

為什麼會有公平鎖/非公平鎖的設計?

考慮這個問題,我們需重新回憶上面的鎖獲取實現圖了,其實上面我已經透露了一點

主要有兩點原因:

原因一:

恢復掛起的線程到真正鎖的獲取還是有時間差的,從人類的角度來看這個時間微乎其微,但是從CPU的角度來看,這個時間差存在的還是很明顯的。所以非公平鎖能更充分的利用 CPU 的時間片,盡量減少 CPU 空閑狀態時間

原因二:

不知你是否還記得我在 面試問,創建多少個線程合適? 文章中反覆提到過,使用多線程很重要的考量點是線程切換的開銷,想象一下,如果採用非公平鎖,當一個線程請求鎖獲取同步狀態,然後釋放同步狀態,因為不需要考慮是否還有前驅節點,所以剛釋放鎖的線程在此刻再次獲取同步狀態的幾率就變得非常大,所以就減少了線程的開銷

相信到這裏,你也就明白了,為什麼 ReentrantLock 默認構造器用的是非公平鎖同步器

    public ReentrantLock() {
        sync = new NonfairSync();
    }

看到這裏,感覺非公平鎖 perfect,非也,有得必有失

使用公平鎖會有什麼問題?

公平鎖保證了排隊的公平性,非公平鎖霸氣的忽視這個規則,所以就有可能導致排隊的長時間在排隊,也沒有機會獲取到鎖,這就是傳說中的 “飢餓”

如何選擇公平鎖/非公平鎖?

相信到這裏,答案已經在你心中了,如果為了更高的吞吐量,很顯然非公平鎖是比較合適的,因為節省很多線程切換時間,吞吐量自然就上去了,否則那就用公平鎖還大家一個公平

我們還差最後一個環節,真的要挺住

可重入鎖

到這裏,我們還沒分析 ReentrantLock 的名字,JDK 起名這麼有講究,肯定有其含義,直譯過來【可重入鎖】

為什麼要支持鎖的重入?

試想,如果是一個有 synchronized 修飾的遞歸調用方法,程序第二次進入被自己阻塞了豈不是很大的笑話,所以 synchronized 是支持鎖的重入的

Lock 是新輪子,自然也要支持這個功能,其實現也很簡單,請查看公平鎖和非公平鎖對比圖,其中有一段代碼:

// 判斷當前線程是否和已佔用鎖的線程是同一個
else if (current == getExclusiveOwnerThread())

仔細看代碼, 你也許發現,我前面的一個說明是錯誤的,我要重新解釋一下

重入的線程會一直將 state + 1, 釋放鎖會 state – 1直至等於0,上面這樣寫也是想幫助大家快速的區分

總結

本文是一個長文,說明了為什麼要造 Lock 新輪子,如何標準的使用 Lock,AQS 是什麼,是如何實現鎖的,結合 ReentrantLock 反推 AQS 中的一些應用以及其獨有的一些特性

獨佔式獲取鎖就這樣介紹完了,我們還差 AQS 共享式 xxxShared 沒有分析,結合共享式,接下來我們來閱讀一下 Semaphore,ReentrantReadWriteLock 和 CountLatch 等

最後,也歡迎大家的留言,如有錯誤之處還請指出。我的手酸了,眼睛幹了,我去準備擼下一篇…..

靈魂追問

  1. 為什麼更改 state 有 setState() , compareAndSetState() 兩種方式,感覺後者更安全,但是鎖的視線中有好多地方都使用了 setState(),安全嗎?

  2. 下面代碼是一個轉賬程序,是否存在死鎖或者鎖的其他問題呢?

    
    class Account {
      private int balance;
      private final Lock lock
              = new ReentrantLock();
      // 轉賬
      void transfer(Account tar, int amt){
        while (true) {
          if(this.lock.tryLock()) {
            try {
              if (tar.lock.tryLock()) {
                try {
                  this.balance -= amt;
                  tar.balance += amt;
                } finally {
                  tar.lock.unlock();
                }
              }//if
            } finally {
              this.lock.unlock();
            }
          }//if
        }//while
      }//transfer
    }
    

參考

  1. Java 併發實戰
  2. Java 併發編程的藝術
  3. https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

Windows 10 再傳用 Chrome 訪問一個指定路徑,就會立刻出現藍白當機畫面 Bug_包裝設計

※產品缺大量曝光嗎?你需要的是一流包裝設計!

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

沒想到 Windows 10 除了每次更新很高機率出現一些 Bug 之外,長期以來也有不少奇奇怪怪的問題。幾天前我們曾介紹發現某個短字串會造成硬碟損壞 Bug 的 Bleeping Computer 網站,最近又再度揭露另一個會導致當機藍畫面的 Bug,這次跟 Chrome 有關,只要訪問指定位置就會發生。

Windows 10 的 Chrome 訪問這個指定路徑,就會立刻出現藍白當機畫面 Bug

根據 Bleeping Computer 最新報告,一位曾發現 NTFS 安全漏洞的 Jonas Lykkegaard 研究人員,最近再公開另一個使用 Chrome 瀏覽器訪問下方指定位置時,會導致 BSoD 藍白當機畫面:

  • \\.\globalroot\device\condrv\kernelconnect

測試報告也指出,這個 Bug 從 Windows 10 1709 版本開始,一直到 20H2 都有,甚至有些較久版本也會受影響。

不過好消息是,這 Bug 似乎只會出現藍白當機畫面,重開機即可解決,因此應該不會造成硬體或資料方面的毀損。不過 Bleeping Computer 依舊警告,有心人士可能會利用這個 Bug 來阻擋用戶使用電腦,如:發送會執行 Chrome 訪問此位置的 URL 文件,還舉例 2017 年台灣遠東國際商業銀行曾發生被北韓駭客組織「APT38」攻擊的事件,就曾使用類似的方式。

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

上新台中搬家公司提供您一套專業有效率且人性化的辦公室搬遷、公司行號搬家及工廠遷廠的搬家服務

Bleeping Computer 上週已經跟微軟回報這件事,不過獲得的回覆依舊一樣:「微軟承諾會調查已知的安全問題,並盡快為受影響的設備提供更新。」

這 Bug 雖然看起來好像不是什麼大事,但對於有興趣的朋友來說,建議還是透過虛擬機來測試就好,比較保險。

資料來源:Bleeping Computer

Windows 10X 新版本洩漏,羽量級系統力抗 ChromeOS

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

Java 異常(一) 異常概述及其架構_台中搬家

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

Java 異常(一) 異常概述及其架構

一、異常概述

(一)、概述

Java異常是Java提供的一種識別及響應錯誤的一致性機制。異常指的是程序在執行過程中,出現的非正常的情況,最終會導致JVM的非正常停止。
Java異常機制可以使程序中異常處理代碼和正常業務代碼分離,保證程序代碼更加優雅,並提高程序健壯性。在有效使用異常的情況下,異常能清晰的回答 what, where, why 這3個問題:異常類型回答了“什麼”被拋出,異常堆棧跟蹤回答了“在哪“拋出,異常信息回答了“為什麼“會拋出。

(二)、異常體系

異常機制其實是幫助我們找到程序中的問題,異常的根類是java.lang.Throwable,其下有兩個子類:java.lang.Errorjava.lang.Exception,平常所說的異常指java.lang.Exception

 

 Throwable: 有兩個重要的子類:Exception(異常)和 Error(錯誤),二者都是 Java 異常處理的重要子類,各自都包含大量子類。異常和錯誤的區別是:異常能被程序本身可以處理,錯誤是無法處理。

1、Throwable:Throwable是 Java 語言中所有錯誤或異常的超類。Throwable包含兩個子類: Error 和 Exception。它們通常用於指示發生了異常情況。

Throwable包含了其線程創建時線程執行堆棧的快照,它提供了printStackTrace()等接口用於獲取堆棧跟蹤數據等信息。

Throwable常用API:

public void printStackTrace() // 打印異常的詳細信息。包含了異常的類型,異常的原因,還包括異常出現的位置。
public String getMessage() // 獲取發生異常的原因。
public String toString() // 獲取異常的類型和異常描述信息。

2、Error:嚴重錯誤Error,無法通過處理的錯誤,只能事先避免。

3、Exception:表示異常,異常產生后程序員可以通過代碼的方式糾正,使程序繼續運行,是必須要處理的。

(三)、異常分類

我們平常說的異常就是指Exception,因為這類異常一旦出現,我們就要對代碼進行更正,修復程序。

異常主要分為兩大類:編譯期異常和運行期異常。

編譯期異常:checked異常。在編譯期,就會檢查,如果沒有處理異常,則編譯失敗(如日期格式化異常)。

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

特點: Java編譯器會檢查它。此類異常,要麼通過throws進行聲明拋出,要麼通過try-catch進行捕獲處理,否則不能通過編譯。例如,CloneNotSupportedException就屬於被檢查異常。當通過clone()接口去克隆一個對象,而該對象對應的類沒有實現Cloneable接口,就會拋出CloneNotSupportedException異常。被檢查異常通常都是可以恢復的。

運行期異常:runtime異常。在運行時期,檢查異常.在編譯時期,運行異常不會編譯器檢測(不報錯)(如數學異常)。

特點: Java編譯器不會檢查它。也就是說,當程序中可能出現這類異常時,倘若既”沒有通過throws聲明拋出它”,也”沒有用try-catch語句捕獲它”,還是會編譯通過。例如,除數為零時產生的ArithmeticException異常,數組越界時產生的IndexOutOfBoundsException異常,fail-fail機制產生的ConcurrentModificationException異常等,都屬於運行時異常。

雖然Java編譯器不會檢查運行時異常,但是我們也可以通過throws進行聲明拋出,也可以通過try-catch對它進行捕獲處理。

如果產生運行時異常,則需要通過修改代碼來進行避免。例如,若會發生除數為零的情況,則需要通過代碼避免該情況的發生!

二、異常處理

Java異常處理機制用到的幾個關鍵字:try、catch、finally、throw、throws

  • try:用於監聽。將要被監聽的代碼(可能拋出異常的代碼)放在try語句塊之內,當try語句塊內發生異常時,異常就被拋出。
  • catch:用於捕獲異常。catch用來捕獲try語句塊中發生的異常。
  • finally:finally語句塊總是會被執行。它主要用於回收在try塊里打開的物力資源(如數據庫連接、網絡連接和磁盤文件)。只有finally塊,執行完成之後,才會回來執行try或者catch塊中的return或者throw語句,如果finally中使用了return或者throw等終止方法的語句,則就不會跳回執行,直接停止。
  • throw:用於拋出異常。
  • throws:用在方法簽名中,用於聲明該方法可能拋出的異常。

三、異常注意點

(一)、finally中的異常會覆蓋(消滅)前面try或者catch中的異常

  • 不要在fianlly中使用return。
  • 不要在finally中拋出異常。
  • 減輕finally的任務,不要在finally中做一些其它的事情,finally塊僅僅用來釋放資源是最合適的。
  • 將盡量將所有的return寫在函數的最後面,而不是try … catch … finally中。

(二)、多個異常使用捕獲

  • 多個異常分別處理。
  • 多個異常一次捕獲,多次處理。
  • 多個異常一次捕獲一次處理。

一般情況下,一般我們是使用一次捕獲多次處理方式,如下代碼

try{
     // 編寫可能會出現異常的代碼
}catch(異常類型A  e){  當try中出現A類型異常,就用該catch來捕獲.
     // 處理異常的代碼
     //記錄日誌/打印異常信息/繼續拋出異常
}catch(異常類型B  e){  當try中出現B類型異常,就用該catch來捕獲.
     // 處理異常的代碼
     //記錄日誌/打印異常信息/繼續拋出異常
}

注意:這種異常處理方式,要求多個catch中的異常不能相同,並且若catch中的多個異常之間有子父類異常的關係,那麼子類異常要求在上面的catch處理,父類異常在下面的catch處理。

三、實例

(一)、try – catch用法

try – catch 必須搭配使用,不能單獨使用。

public class ExceptionDemo {
    public static void main(String[] args) {
        try {
            int i = 10 / 0;// 除數不能為0,此行會拋出 ArithmeticException 異常
            System.out.println("i = " + i);// 拋出異常后,此行不會執行
        }catch(ArithmeticException e) { // 捕獲 ArithmeticException 異常
            // 在catch 代碼塊處理異常
            e.printStackTrace(); // 異常最詳細信息
            System.out.println("e.getMessage() : " + e.getMessage());// 發生異常的原因
            System.out.println("e.toString() : " + e.toString());  // 獲取異常的類型和異常描述信息
        }
    }
}

(二)、finally 用法

try – catch – finally搭配使用,或者 try – finally 搭配使用。

public class ExceptionDemo {
    public static void main(String[] args) {
        // try-catch-finally搭配使用
        try {
            int[] arr = {1,2,3};
            int i = arr[3];// 數組索引越界,此行會拋出 ArrayIndexOutOfBoundsException 異常
            System.out.println("i = " + i);// 拋出異常后,此行不會執行
        }catch(ArithmeticException e) { // 捕獲 ArithmeticException
            System.out.println(e.getMessage());// 發生異常的原因
            System.exit(0); // 程序強制退出,finally 代碼塊不會執行
        }finally {// 除了程序強制退出,如(System。exit(0)),無論是否發生異常,finally 代碼塊總會執行
            System.out.println("this is finally");
        }
        
        // try-finally搭配使用
        try {
            int[] arr = {1,2,3};
            int i = arr[3];// 數組索引越界,此行會拋出 ArrayIndexOutOfBoundsException 異常
            System.out.println("i = " + i);// 拋出異常后,此行不會執行
        }finally { 
            // 無論是否發生異常,finally 代碼塊總會執行
            System.out.println("this is finally");
        }
    }
}

注意點:

  • try-catch-finally 搭配:這種形式捕獲異常時,開發者可以在 catch 代碼塊中處理異常(如打印日誌、日誌記錄等),異常處理權在開發者。
  • try-finally 搭配:這種形式捕獲異常時,默認拋出給 JVM 處理,JVM默認處理時調用 e.printStackTrace() 方法打印異常詳細信息。
  • finally 代碼塊:除非程序強制退出,否則無論程序是否發生異常,finally 代碼塊總會執行。
  • finally 中拋出異常會覆蓋(消滅)前面 try 或者 catch 中的異常,盡量避免在 finally 代碼塊中拋出異常。
  • 如果 try 中和 finally 中都有 return 語句,程序會先執行 finally 中的 return 語句,然後程序塊運行結束,而不會執行 try 中的 return 語句。所以盡量不在finally中使用 return 語句。

(三)、throw 用法

throw 是用於拋出異常,將這個異常對象傳遞到調用者處,並結束當前方法的執行

public static void main(String[] args) {
  try {
    int i = 10 / 0;
    System.out.println("i = " + i);
  }catch(ArithmeticException e) { 
    // 拋出異常,傳遞自定義異常信息提示
    // 默認拋出給 JVM 處理打印異常詳細信息    
    throw new ArithmeticException("除數不能為0");
  } 
}

(四)、throws 用法

throws運用於方法聲明之上,用於表示當前方法不處理異常,而是提醒該方法的調用者來處理異常(拋出異常)。

public class ExceptionDemo {
    public static void main(String[] args) {
        demo();
    }
    public static void demo() throws ArrayIndexOutOfBoundsException{
        try {
            int[] arr = {1,2,3};
            int i = arr[3];
            System.out.println("i = " + i);
        }catch(ArrayIndexOutOfBoundsException e) { 
           System.out.println(e.toString());
        } 
    }
}

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

這5台合資代步家轎省油好開 關鍵是優惠大還帶渦輪_台中搬家

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

這款車從外觀上來說,是相當充滿青春活力的,設計師說他們這是流體雕塑2。0的設計。很難想象這樣一款車,是以卡羅拉作為假想敵開發出來的。說到韓系車,肯定要說一下配置。這款領動雖然是1。4T的最低配,但是已經配有了電動天窗,倒車視頻影像,日間行車燈。

中國的A級家轎市場早已拼得刺刀見紅,各路廠商都紛紛拿出自己的看家產品想來搶佔這塊市場。面對琳琅滿目的選擇,消費者可能會不知所措。接下來,編者我分別在五大派系中挑選一個車型來推薦給大家。

東風雪鐵龍 C4世嘉

2016款 1.2THp 自動豪華型

指導價:13.98萬

優惠幅度:2萬元左右(僅供參考)

編者點評:

08年剛剛國產的那一代世嘉可是名副其實的油老虎,連標準版1.6L手動擋都能刷出9.5L每百公里的綜合油耗。時過境遷,1.2T的C4世嘉也變得挺省油了,百公里綜合油耗僅為6.5L。由於並不是熱門車,所以能有兩萬塊左右的優惠,可以說是相當厚道了。

動力總成上是1.2T+6擋手自一體變速箱的組合,發動機最大輸出136馬力和230牛米。從動力來看,還是夠用的,不會說太肉。但是,最大扭矩到3500轉就開始衰減,這就是個挺大的問題。配置上,這個價位就能有后駐車雷達和倒車視頻影像確實難得,另外也配有了發動機啟停這個省油利器。

談到C4世嘉就不得不提它的操控,因為雪鐵龍可是WRC的常勝將軍。C4世嘉無論轉向還是底盤,都明顯偏向操控。轉向上,虛位不多,轉向精準;底盤則偏硬,很好地抑制了車身側傾。另外,百公里剎停只要39.5米,也是相當優秀的。

上汽通用雪佛蘭 科魯茲

2017款 1.4T 雙離合先鋒天窗版

指導價:13.99萬

優惠幅度:1.5萬元左右(僅供參考)

編者點評:

作為美系車中操控比較好的A級家轎,科魯茲往日的銷量並不算理想,因此這一代它的底盤調教明顯多了不少高級感。過坑窪時,不會再像以前那麼顛,路感那麼豐富,反而是多了一份從容。

這款車搭載了1.4T發動機+7擋雙離合變速箱,最大輸出150馬力和240牛米,最大扭矩轉速在2000轉到4000轉之間,2000轉才輸出最大扭矩明顯是偏高了,結果就是每次超車都能聽到發動機的噪音。

油耗上,由於搭載了雙離合變速箱,所以1.4T科魯茲的百公里綜合油耗僅為7.7L。作為一台美系車,這是很少見的,看來現在的美系車都開始了“摘帽行動”。按照官方保養建議,科魯茲6萬公里的保養費用為9713元,價格上算是偏貴。不過,略有安慰的是,科魯茲的保養用的都是全合成機油。

北京現代 領動

2016款 1.4T 雙離合炫動·活力型

指導價:12.98萬

優惠幅度:0.5萬元左右(僅供參考)

編者點評:

2015年現代銷量下滑,所以最近推出了領動。這款車從外觀上來說,是相當充滿青春活力的,設計師說他們這是流體雕塑2.0的設計。很難想象這樣一款車,是以卡羅拉作為假想敵開發出來的。

說到韓系車,肯定要說一下配置。這款領動雖然是1.4T的最低配,但是已經配有了電動天窗,倒車視頻影像,

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

日間行車燈。另外,還有不少實用性配置可以選裝,例如發動機啟停,後座出風口,無鑰匙進入與啟動等等。從配置上看可以說誠意十足。

誠然,在机械質感的匹配上,領動與卡羅拉還是有不少差距的。具體表現在面對大坑窪時,領動的底盤更容易露餡。不過,它的雙離合變速箱調教得還是比較溫順的,在市區中走走停停也不易發生頓挫。

另外,領動的百公里綜合油耗僅為6.8L,確實是同級少見。保養方面,6萬公里的總保養費用為9573元,與科魯茲都是半斤八兩的水平。

廣汽豐田 雷凌

2016款 雙擎 1.8H GS CVT精英版

指導價:13.98萬

優惠幅度:1.5萬元左右(僅供參考)

編者點評:

說到雷凌雙擎這款車,第一要說的必須是它那逆天的油耗,百公里綜合油耗5.1L,而且是怎麼開都不會有太大的波幅。無論在高速,還是市區堵車,油耗都不會有太大的變化。這個油耗放在同級的非插電式混動車中,可以說是虐遍天下無敵手。很多人會擔心電池問題,廠商給出了8年20萬公里的電池保修,這顆“定心丸”着實夠誠意。

這個價格能買到這種高效混動車,所以雷凌雙擎的配置就比較素了,不但沒有電動天窗,就連倒車雷達都沒有。如果是配置控的話,基本可以放棄這款車了。

雷凌的的空間只能說中規中矩,不過後排地板能做到幾乎全平確實比較難得。不過,電池佔用了尾廂的一些深度,但程度可以接受。

這款車有個很重要的點就是在廣州可以上新能源牌,如果在廣州已經搖號搖到絕望,但又不想開純電動車的人們,不妨看一下雷凌雙擎。

上汽大眾 朗逸

2015款 230TSI DSG舒適版

指導價:14.69萬

優惠幅度:2.5萬左右(僅供參考)

編者點評:

我已不記得一代神車朗逸已經連續多少個月摘得A級家轎銷量榜的冠軍,儘管大眾深受排放門的影響,但似乎絲毫沒有影響朗逸的銷量。究其原因,朗逸的外觀是一個很重要的因素,前臉那幾條長長的鍍鉻裝飾條,雖然簡潔,但是讓整個前臉都顯得更為大氣。

動力組合就是那套經典的TSI+DSG,最大輸出131馬力和225牛米,最大扭矩轉速為1400-3500轉。可以看得出,發動機在比較低轉速就釋放最大扭矩,而且持續的轉速也比較寬泛,對超車相當有利。

空間上,其實朗逸的後排坐墊是明顯偏短的,這樣就有空間大的假象。實際上,你的大腿會有很大一部分得不到承托。這個問題已經存在挺長的時間了,但是大眾就是不改。

油耗方面,這款車型的百公里綜合油耗為7.2L,這個油耗算是可以了。

編者總結:

如果說娛樂圈是個大染缸的話,那麼汽車界就是一個大江湖。法系浪漫,美系奔放,韓系活力,日系實用,德系大氣這都是各自品牌在A級家轎表現出來的調性。各個派系的A級轎車都使出渾身解數想統領這個市場,最後形成了這種百花齊放的現象。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

Apple 的折疊機將具備先進的雙相機設計,新專利顯示只要折疊或反折手機就可以拍出不同特色照片_台中搬家

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

新 iPhone 的傳聞總是令果粉們興奮不已,就算是看起來還不太會登場的折疊機也有一定的話題在。最近外媒 Patently Apple 就發現 Apple 獲得了一個關於折疊機功能的專利。在這項專利中,Apple 設想了一個在折疊手機正面上下兩端各裝一個相機的構想。這樣一來,使用者只需要動手調整折疊的角度,就可以讓兩個相機拍攝出不同面向的照片。透過內折疊或外折疊的方式還可以設定成讓相機拍攝廣角畫面與3D相片的狀態,如果在實機上能有這樣的功能,想必能吸引到更多用戶與投資者的目光與錢錢

▲Apple 開發折疊機的傳聞不算什麼新聞,只是折疊機上會有什麼新花樣,實在令人好奇(圖片來源)

Apple 已經被傳了很久正在開發折疊式手機,只是目前並無實機出現,顯見現階段 Apple 仍保密到家。雖然不知道這個折疊機的真貌,甚至也不清楚 Apple 選在什麼時候上市,但可以猜測今年內仍然機會不大。畢竟光是軟性折疊螢幕的部分就已經有足夠的難度,讓 Apple 及其他手機大廠傷神。不過,即便 Apple 還沒完全準備好會推出折疊機,但相關專利仍然持續取得,就算 Apple 推出不了折疊手機,手上的專利也有可能發揮到其他產品上,創造不同的商業利益。

雙鏡頭與折疊機構彼此搭配,創造更多拍攝的功能

最近外媒 Patently Apple 發現,Apple 為折疊手機的相機技術註冊了一個專利。這個技術主要是利用了折疊機獨有的變形機制,讓相機能有不同的拍攝角度,進而拍出不同的畫面。在這項專利中顯示,Apple 準備了兩個相機,分別裝在折疊機正面的上下兩端,當手機朝外折疊時,雙鏡頭的畫面會經過手機演算後重合為廣角畫面。若是手機向內折疊,讓相機彼此相對時,雙鏡頭的畫面會對著同一個目標來拍攝,近而拍出 3D 照片:

▲雙鏡頭會安放在手機的兩端,藉由折疊來讓鏡頭調整拍攝模式(圖片來源)

▲為了讓折疊模式更有彈性,因此可能會用到柔性織物之類的材質來讓手機變得柔軟(圖片來源)

▲理論上這項技術還能延伸到原本就是柔性、可折疊的產品上,例如 Apple Watch 與 Macbook(圖片來源)

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

為了實現正反兩側的彎折,Apple 在專利中提到要利用柔性織物來製作外殼,藉此來增加手機的柔軟度。不過雙鏡頭的拍攝成果肯定需要 Apple 強大的 AI 運算來實現,因此這不光是硬體方面需要專利,想必軟體方面也會有相對應的專利技術。只是Apple 是否真能實現這個技術帶來的新手機設計,目前還很難判斷。如果真的推出這樣的手機,想必會在果粉群組間掀起不小的話題

消息來源

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

誇張!女童泳池溺水瀕死 媽媽低頭玩手機渾不知_租車

※超省錢租車方案

商務出差、學生出遊、旅遊渡假、臨時用車!GO 神州租賃有限公司!合法經營、合法連鎖、合法租賃小客車!

中國河南洛陽近日發生一起離譜案件,一名女童在一處游泳池內游泳,卻發生溺水意外。但誇張的是,她的母親在泳池邊只顧著玩手機,完全不知道自己的孩子已經溺水,隨後有另外一名女泳客發現異狀,立刻救起女童,但女童被救起時,一度失去意識和呼吸。

據中國《看看新聞》報導,事發在本月14日下午,這名女童到泳池玩耍,卻發生溺水意外,但她的媽媽只在泳池邊不斷低頭玩手機,完全不知道自己的女兒出事了。隨後另一名女泳客、高中老師陶曉峰發現了異狀,感覺水底下有人,她立刻將孩子救起,並馬上進行心肺復甦術。

陶曉峰本身也有救生員資格,

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

有別於一般網頁架設公司,除了模組化的架站軟體,我們的營業主軸還包含:資料庫程式開發、網站建置、網頁設計、電子商務專案開發、系統整合、APP設計建置、專業網路行銷。

她立刻為孩子進行急救,但女童一度失去意識和呼吸。但令人驚訝的是,女童的母親只顧著低頭玩手機,完全不知道自己的女兒身陷危險,甚至瀕臨死亡邊緣。等到女童的母親回過神時,才發現女童沒有了意識,在現場大哭。陶曉峰也說,現場由於游泳的人太多,再加上水也不太清澈,「當時感覺水下是一個小孩」。出於本能意識,陶曉峰立刻潛水並將溺水女童拉出水面。

就在陶曉峰急救的同時,消防隊及救難隊也獲報趕抵現場,並將女童送醫急救。經治療好,女童已無大礙,目前正持續接受治療當中。

誇張!女童泳池溺水瀕死,媽媽低頭玩手機渾不知。翻自畫面
女童被救起時一度失去意識和呼吸。翻自畫面
經治療好,女童已無大礙,目前正持續接受治療當中。翻自畫面

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

日本、大陸,發現這些先進的國家已經早就讓電動車優先上路,而且先進國家空氣品質相當好,電動車節能減碳可以減少空污

從linux源碼看socket的阻塞和非阻塞_台中搬家

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

從linux源碼看socket的阻塞和非阻塞

筆者一直覺得如果能知道從應用到框架再到操作系統的每一處代碼,是一件Exciting的事情。
大部分高性能網絡框架採用的是非阻塞模式。筆者這次就從linux源碼的角度來闡述socket阻塞(block)和非阻塞(non_block)的區別。 本文源碼均來自採用Linux-2.6.24內核版本。

一個TCP非阻塞client端簡單的例子

如果我們要產生一個非阻塞的socket,在C語言中如下代碼所示:

// 創建socket
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
...
// 更改socket為nonblock
fcntl(sock_fd, F_SETFL, fdflags | O_NONBLOCK);
// connect
....
while(1)  {  
    int recvlen = recv(sock_fd, recvbuf, RECV_BUF_SIZE) ; 
    ......
} 
...

由於網絡協議非常複雜,內核裏面用到了大量的面向對象的技巧,所以我們從創建連接開始,一步一步追述到最後代碼的調用點。

socket的創建

很明顯,內核的第一步應該是通過AF_INET、SOCK_STREAM以及最後一個參數0定位到需要創建一個TCP的socket,如下圖綠線所示:

我們跟蹤源碼調用

socket(AF_INET, SOCK_STREAM, 0)
	|->sys_socket 進入系統調用
		|->sock_create
			|->__sock_create

進一步分析__sock_create的代碼判斷:

const struct net_proto_family *pf;
// RCU(Read-Copy Update)是linux的一種內核同步方法,在此不闡述
// family=INET
pf = rcu_dereference(net_families[family]);
err = pf->create(net, sock, protocol);

由於family是AF_INET協議,注意在操作系統裏面定義了PF_INET等於AF_INET,
內核通過函數指針實現了對pf(net_proto_family)的重載。如下圖所示:

則通過源碼可知,由於是AF_INET(PF_INET),所以net_families[PF_INET].create=inet_create(以後我們都用PF_INET表示),即
pf->create = inet_create;
進一步追溯調用:

inet_create(struct net *net, struct socket *sock, int protocol){
	Sock* sock;
	......
	// 此處是尋找對應協議處理器的過程
lookup_protocol:
	// 迭代尋找protocol==answer->protocol的情況
	list_for_each_rcu(p, &inetsw[sock->type]) answer = list_entry(p, struct inet_protosw, list);

		/* Check the non-wild match. */
		if (protocol == answer->protocol) {
			if (protocol != IPPROTO_IP)
				break;
		}
	......
	// 這邊answer指的是SOCK_STREAM
	sock->ops = answer->ops;
	answer_no_check = answer->no_check;
	// 這邊sk->prot就是answer_prot=>tcp_prot
	sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);
	sock_init_data(sock, sk);
	......
}

上面的代碼就是在INET中尋找SOCK_STREAM的過程了
我們再看一下inetsw[SOCK_STREAM]的具體配置:

static struct inet_protosw inetsw_array[] =
{
	{
		.type =       SOCK_STREAM,
		.protocol =   IPPROTO_TCP,
		.prot =       &tcp_prot,
		.ops =        &inet_stream_ops,
		.capability = -1,
		.no_check =   0,
		.flags =      INET_PROTOSW_PERMANENT |
			      INET_PROTOSW_ICSK,
	},
	......
}

這邊也用了重載,AF_INET有TCP、UDP以及Raw三種:

從上述代碼,我們可以清楚的發現sock->ops=&inet_stream_ops;

const struct proto_ops inet_stream_ops = {
	.family		   = PF_INET,
	.owner		   = THIS_MODULE,
	......
	.sendmsg	   = tcp_sendmsg,
	.recvmsg	   = sock_common_recvmsg,
	......
}	

即sock->ops->recvmsg = sock_common_recvmsg;
同時sock->sk->sk_prot = tcp_prot;

我們再看下tcp_prot中的各個函數重載的定義:

struct proto tcp_prot = {
	.name			= "TCP",
	.close			= tcp_close,
	.connect		= tcp_v4_connect,
	.disconnect		= tcp_disconnect,
	.accept			= inet_csk_accept,
	......
	// 我們重點考察tcp的讀
	.recvmsg		= tcp_recvmsg,
	......
}

fcntl控制socket的阻塞\非阻塞狀態

我們用fcntl修改socket的阻塞\非阻塞狀態。
事實上:
fcntl的作用就是將O_NONBLOCK標誌位存儲在sock_fd對應的filp結構的f_lags里,如下圖所示。

fcntl(sock_fd, F_SETFL, fdflags | O_NONBLOCK);
	|->setfl

追蹤setfl代碼:

static int setfl(int fd, struct file * filp, unsigned long arg) {
	......
	filp->f_flags = (arg & SETFL_MASK) | (filp->f_flags & ~SETFL_MASK);
	......
}

上圖中,由sock_fd在task_struct(進程結構體)->files_struct->fd_array中找到對應的socket的file描述符,再修改file->flags

在調用socket.recv的時候

我們跟蹤源碼調用:

socket.recv
	|->sys_recv
		|->sys_recvfrom
			|->sock_recvmsg
				|->__sock_recvmsg
					|->sock->ops->recvmsg

由上文可知:
sock->ops->recvmsg = sock_common_recvmsg;

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

sock

值得注意的是,在sock_recmsg中,有對標識O_NONBLOCK的處理

	if (sock->file->f_flags & O_NONBLOCK)
		flags |= MSG_DONTWAIT;

上述代碼中sock關聯的file中獲取其f_flags,如果flags有O_NONBLOCK標識,那麼就設置msg_flags為MSG_DONTWAIT(不等待)。
fcntl與socket就是通過其共同操作File結構關聯起來的。

繼續跟蹤調用

sock_common_recvmsg

int sock_common_recvmsg(struct kiocb *iocb, struct socket *sock,
			struct msghdr *msg, size_t size, int flags) {
	......
	// 如果flags的MSG_DONTWAIT標識置位,則傳給recvmsg的第5個參數為正,否則為0
	err = sk->sk_prot->recvmsg(iocb, sk, msg, size, flags & MSG_DONTWAIT,
				   flags & ~MSG_DONTWAIT, &addr_len);
	.....				   
}

由上文可知:
sk->sk_prot->recvmsg 其中sk_prot=tcp_prot,即最終調用的是tcp_prot->tcp_recvmsg,
上面的代碼可以看出,如果fcntl(O_NONBLOCK)=>MSG_DONTWAIT置位=>(flags & MSG_DONTWAIT)>0, 再結合tcp_recvmsg的函數簽名,即如果設置了O_NONBLOCK的話,設置給tcp_recvmsg的nonblock參數>0,關係如下圖所示:

最終的調用邏輯tcp_recvmsg

首先我們看下tcp_recvmsg的函數簽名:

int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int nonblock, int flags, int *addr_len)

顯然我們關注焦點在(int nonblock這個參數上):

int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int nonblock, int flags, int *addr_len){
	......	
	// copied是指向用戶空間拷貝了多少字節,即讀了多少
	int copied;
	// target指的是期望多少字節
	int target;
	// 等效為timo = nonblock ? 0 : sk->sk_rcvtimeo;
	timeo = sock_rcvtimeo(sk, nonblock);
	......	
	// 如果設置了MSG_WAITALL標識target=需要讀的長度
	// 如果未設置,則為最低低水位值
	target = sock_rcvlowat(sk, flags & MSG_WAITALL, len);
	......

	do{
		// 表明讀到數據
		if (copied) {
			// 注意,這邊只要!timeo,即nonblock設置了就會跳出循環
			if (sk->sk_err ||
			    sk->sk_state == TCP_CLOSE ||
			    (sk->sk_shutdown & RCV_SHUTDOWN) ||
			    !timeo ||
			    signal_pending(current) ||
			    (flags & MSG_PEEK))
			break;
		}else{
			// 到這裏,表明沒有讀到任何數據
			// 且nonblock設置了導致timeo=0,則返回-EAGAIN,符合我們的預期
			if (!timeo) {
				copied = -EAGAIN;
				break;
		}
		// 這邊如果堵到了期望的數據,繼續,否則當前進程阻塞在sk_wait_data上
		if (copied >= target) {
			/* Do not sleep, just process backlog. */
			release_sock(sk);
			lock_sock(sk);
		} else
			sk_wait_data(sk, &timeo);
	} while (len > 0);		
	......
	return copied
}

上面的邏輯歸結起來就是:
(1)在設置了nonblock的時候,如果copied>0,則返回讀了多少字節,如果copied=0,則返回-EAGAIN,提示應用重複調用。
(2)如果沒有設置nonblock,如果讀取的數據>=期望,則返回讀取了多少字節。如果沒有則用sk_wait_data將當前進程等待。
如下流程圖所示:

阻塞函數sk_wait_data

sk_wait_data代碼-函數為:

	// 將進程狀態設置為可打斷INTERRUPTIBLE
	prepare_to_wait(sk->sk_sleep, &wait, TASK_INTERRUPTIBLE);
	set_bit(SOCK_ASYNC_WAITDATA, &sk->sk_socket->flags);
	// 通過調用schedule_timeout讓出CPU,然後進行睡眠
	rc = sk_wait_event(sk, timeo, !skb_queue_empty(&sk->sk_receive_queue));
	// 到這裏的時候,有網絡事件或超時事件喚醒了此進程,繼續運行
	clear_bit(SOCK_ASYNC_WAITDATA, &sk->sk_socket->flags);
	finish_wait(sk->sk_sleep, &wait);

該函數調用schedule_timeout進入睡眠,其進一步調用了schedule函數,首先從運行隊列刪除,其次加入到等待隊列,最後調用和體繫結構相關的switch_to宏來完成進程間的切換。
如下圖所示:

阻塞后什麼時候恢復運行呢

情況1:有對應的網絡數據到來

首先我們看下網絡分組到來的內核路徑,網卡發起中斷後調用netif_rx將事件掛入CPU的等待隊列,並喚起軟中斷(soft_irq),再通過linux的軟中斷機制調用net_rx_action,如下圖所示:

注:上圖來自PLKA(<<深入Linux內核架構>>)
緊接着跟蹤next_rx_action

next_rx_action
	|-process_backlog
		......
			|->packet_type->func 在這裏我們考慮ip_rcv
					|->ipprot->handler 在這裏ipprot重載為tcp_protocol
						(handler 即為tcp_v4_rcv)					

緊接着tcp_v4_rcv:

tcp_input.c
tcp_v4_rcv
	|-tcp_v4_do_rcv
		|-tcp_rcv_state_process
			|-tcp_data_queue
				|-sk->sk_data_ready=sock_def_readable
					|-wake_up_interruptible
						|-__wake_up
							|-__wake_up_common

在這裏__wake_up_common將停在當前wait_queue_head_t中的進程喚醒,即狀態改為task_running,等待CFS調度以進行下一步的動作,如下圖所示。

情況2:設定的超時時間到來

在前面調用sk_wait_event中調用了schedule_timeout

fastcall signed long __sched schedule_timeout(signed long timeout) {
	......
	// 設定超時的回掉函數為process_timeout
	setup_timer(&timer, process_timeout, (unsigned long)current);
	__mod_timer(&timer, expire);
	// 這邊讓出CPU
	schedule();
	del_singleshot_timer_sync(&timer);
	timeout = expire - jiffies;
 out:
 	// 返回經過了多長事件
	return timeout < 0 ? 0 : timeout;	
}

process_timeout函數即是將此進程重新喚醒

static void process_timeout(unsigned long __data)
{
	wake_up_process((struct task_struct *)__data);
}

總結

linux內核源代碼博大精深,閱讀其代碼很費周折。希望筆者這篇文章能幫助到閱讀linux網絡協議棧代碼的人。

公眾號

關注筆者公眾號,獲取更多乾貨文章:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

死磕傳祺GS8!新款七座國貨SUV有望光棍節上市_台中搬家

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

而藍標版本H7L則只增加一款尊貴型,將會裝備自動泊車、360度全景影像等。配置的增加有利於車主獲得更具高級感的用車體驗,但是電器元件的可靠性問題則是廠家需要重點考慮的隱患。動力總成並無變化由於哈弗H7上市並不久,新款的紅標版本H7和H7L兩款車型僅僅是在車身尺寸和科技性配置上進行一定程度的提升,所以動力總成方面將繼續沿用2。



哈弗是國內罕見的專註於SUV產品的汽車品牌,旗下緊湊型SUV哈弗H6更是長期佔據自主品牌SUV銷量榜單頭把交椅的位置。為了填補哈弗在中型SUV中的空缺,在今年早些時候,哈弗推出了旗下中型SUV——哈弗H7。先期僅有藍標版本,而在今年11月11日,紅標版H7以及“藍標加大版本”H7L將有望正式上市銷售。

同名不同尺寸的“三胞胎”

紅藍標的設計差異並不大,不過從前臉的設計方式很容易分辨出來,藍標版本的進氣格柵使用了稜角分明的運動風格,而紅標版本則是更圓潤沉穩的設計語言。

三台車儘管都是以“哈弗H7”命名,但是車身尺寸卻各不相同,現款在售的藍標版本哈弗H7車身尺寸為4700*1925*1718mm,

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

而更新推出的紅標版H7則為4715*1925*1718mm,加長的H7L車身尺寸為4900*1925*1785mm。可見哈弗H7L的長度和高度都更大,所以H7L將會有7座布局出現。然而三款車的軸距相同,都是2850mm。內飾設計與現款並無二致,依舊保持了較為大氣的設計風格。

亮點在於配置更豐富

現在的藍標版本H7僅有舒適、精英、豪華三個配置供消費者選擇,屆時紅標版本H7上市後會有尊享型、尊貴型兩個更高級別的細分配置車型,新增座椅記憶、副駕駛電動調節、自動大燈、自動雨刷等實用性更加便利的科技配置。

而藍標版本H7L則只增加一款尊貴型,將會裝備自動泊車、360度全景影像等。配置的增加有利於車主獲得更具高級感的用車體驗,但是電器元件的可靠性問題則是廠家需要重點考慮的隱患。

動力總成並無變化

由於哈弗H7上市並不久,新款的紅標版本H7和H7L兩款車型僅僅是在車身尺寸和科技性配置上進行一定程度的提升,所以動力總成方面將繼續沿用2.0T的發動機,與之匹配的是一款6速雙離合變速箱。賬面參數上看發動機最大馬力231匹,峰值扭矩355牛米,還是非常可觀的數值。

全文總結:哈弗H7作為填補哈弗品牌在中型SUV車型中的空缺在今年早些時候上市銷售,9月份哈弗H7的銷量成績單為5192台,銷量表現上說也算不錯。如今推出7座版本的H7L或許會在銷量上為哈弗H7家族在銷量上有一定的推動作用。如今藍標版本H7售價在14.98-16.98萬左右,相信在正式上市以後售價將會因配置的豐富而有所提高。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

最個性最拉風!14-25萬你能買到的最騷的幾台SUV都在這_台中搬家

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

謳歌CDX指導價:22。98-30。98萬CDX的外觀設計很多人說丑,但是CDX是一台不怎麼上相的車,如果你真的貼近看了它的實車你就明白它的車身線條以及細節設計真的很騷氣,車身尺寸為4496*1840*1615mm,軸距2660mm,使用1。5T+8擋雙離合變速箱的動力組合,作為一台豪車CDX的氣場也是足夠的,CDX是一台適合年輕人的SUV這話一點都沒錯。

對於年輕人來說,喜歡明星最直接的理由就是顏值,就像最近十分流行的一句話:“你那麼美說什麼都對”,這確實是一個看臉的社會。

比如小編美美的日常寫照↓↓↓

而買車也一樣,汽車的顏值同樣也十分重要,有多少人買車最終因為顏值的問題選擇了一款性價比不那麼高的產品,抑或是因為那款車型長得丑而放棄了性價比不錯的車型。

對於許多年輕人來說買車十分重要的一個特點就是要騷氣,能夠契合自己的氣質,而現在百花齊放的汽車市場越來越多的SUV也更有個性了,比如下面這些選擇。

有人說它是拉高車身的昂克賽拉,也有人說它是拍扁了的CX-5,不過這都不重要,很大一部分最早認識CX-4都是因為CX-4帥氣的外觀設計,轎跑般的設計讓它行駛在街上的回頭率十分高,不過低矮的車身與大多數人預期的那種SUV該有的高高在上的感覺不同,

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

CX-4開着仍然很像一台轎車,而且在空間上也需要作出一定的妥協,不過這麼帥,總得付出點代價吧。

科雷嘉的外觀保持了法國設計的浪漫情調和獨特性,也是屬於很騷氣的外觀,4503*1836*1610mm的車身尺寸足夠大,不過2645mm的軸距就顯得不那麼寬裕了,而且全系目前只有2.0L+CVT的動力組合,未免顯得有些單調了。

CDX的外觀設計很多人說丑,但是CDX是一台不怎麼上相的車,如果你真的貼近看了它的實車你就明白它的車身線條以及細節設計真的很騷氣,車身尺寸為4496*1840*1615mm,軸距2660mm,使用1.5T+8擋雙離合變速箱的動力組合,作為一台豪車CDX的氣場也是足夠的,CDX是一台適合年輕人的SUV這話一點都沒錯。

作為韓系車的顏值擔當,KX5的外觀設計可以說十分驚艷,冰塊式的霧燈設計十分獨特,有點像手電筒的感覺,而大燈造型和位置有些神似卡宴,不過據說它的回頭率比卡宴還要高哦。

吉普自由俠定位於小型SUV,也是吉普目前定位最低價格最便宜的SUV車型,不過這一點也不影響它成為一台很有個性很騷氣的SUV車型。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

iPhone 12 Pro Max 對決三星 Galaxy S21 Ultra 跌落測試,究竟誰更耐摔?

去年在三星 Galaxy Note20 系列推出後,當時 YouTube 頻道 PhoneBuff 以 Galaxy Note20 Ultra 和 iPhone 12 Pro Max 同時進行跌落測試,最終由 iPhone 12 Pro Max 勝出。在今年三星上半年度新旗艦 Galaxy S21 Ultra 推出後,不免俗還是要再次與 iPhone 比較一下跌落測試的耐用度。究竟在經過一連串嚴峻的測試後, Galaxy S21 Ultra 是否能為三星扳回一場勝利呢?

iPhone 12 Pro Max 對決三星 Galaxy S21 Ultra 跌落測試,究竟誰更耐摔?

之前 Phone Buff 進行過許多手機包括效能、續航、耐摔等 PK 測試項目,去年底採用超瓷晶盾面板保護的 iPhone 12 Pro Max 也在和 Galaxy Note20 Ultra 的跌落測試勝出,隨著 Galaxy S21 系列推出後,還是要來看看新推出的 Galaxy S21 Ultra 在耐摔測試方面是否有所進步。

開始測試前比較一下兩款手機的螢幕、材質與機身重量, iPhone 12 Pro Max 具有精緻的平整邊緣設計,採用精美的醫療級不鏽鋼邊框,搭配精密研磨的霧面玻璃機背。 6.7 吋的螢幕並採用全新的超瓷晶盾面板,帶來 4 倍耐摔的優異表現,iPhone 的耐用度獲得歷來最大幅度的提升。
至於全新推出的 Galaxy S21 Ultra 在機身設計雖然和 Galaxy Note20 Ultra 有些不同、螢幕尺寸也些微縮小至 6.8 吋,不過在機身雙面皆延續採用康寧 Gorilla Glass Victus 保護、搭配鋁合金材質邊框, Galaxy S21 Ultra 在機身重量相較 Galaxy Note20 Ultra 增加 21 克,也讓它比 iPhone 12 Pro Max 多了 1 公克。

首先第一回合,採用機身背面朝下進行跌落測試:

兩款手機在靠近機身背面的中框位置略有磨損,雖然 iPhone 12 Pro Max 沒有其餘的傷痕,不過在 Galaxy S21 Ultra 背面出現了一條裂痕,因此第一回合由 iPhone 12 Pro Max 勝出:

第二回合進行邊角的跌落測試:

Phone Buff 歷年在進行這項目的測試,都採用長尾夾固定手機:

iPhone 12 Pro Max 不鏽鋼邊框的傷痕不明顯,只在一些光影照射下能看到傷痕。至於鋁合金邊框的 Galxay S21 Ultra 傷痕略為明顯:

但這回合測試卻發生意外插曲, Galaxy S21 Ultra 機身背面因為長尾夾多了一道裂痕,這是過去在跌落測試未曾發生過的情形,因此這一輪仍由 iPhone 12 Pro Max 以些微差距勝出:

第三回合則輪到螢幕正面跌落測試:

之前在 Galaxy Note20 Ultra 進行這項測試時並未留下傷痕,不過這次在 Galaxy S21 Ultra 則是在螢幕邊角處呈現破裂、螢幕上也出現許多明顯裂痕,至於 iPhone 12 Pro Max 這次依然是穩穩地通過考驗:

進行三回合的測試後,接著是第一場加碼賽讓手機以更高的位置連續 10 次的跌落測試:

經過連續 10 次的跌落後, Galaxy S21 Ultra 雖然螢幕已經有裂痕,不過在相機功能、觸控、螢幕顯示都功能正常:

從另一個視角可見到 Galaxy S21 Ultra 在螢幕邊緣有明顯破裂的傷痕:

在看完 Galaxy S21 Ultra 後,接著輪到 iPhone 12 Pro Max 的加碼賽測試,不過畢竟上一回與 Galaxy Note20 Ultra 對決就已經通過考驗,這次的加碼賽改為更堅硬粗糙的材質作為跌落測試的表面:

iPhone 12 Pro Max 依然在加碼賽勝出:

完整測試影片

圖片/消息來源:PhoneBuff (YouTube)

延伸閱讀:
Apple Watch 發票載具條碼設定教學,一鍵快速喚醒載具條碼超方便!

LINE 應用小技巧: 10 個實用的 LINE 帳號整理術

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!