概述
執行緒本身由於建立和切換的開銷,採用多執行緒不會提高程式的執行速度,反而會降低速度,但是對於頻繁IO操作的程式,多執行緒可以有效的併發。 對於包含不同任務的程式,可以考慮每個任務使用一個執行緒。這樣的程式在設計上相對於單執行緒做所有事的程式來說,更為清晰明瞭,如果是單純的計算操作,多執行緒並沒有單執行緒的計算效率高,但是對於一些刻意分散使用計算機系統資源的操作,則適合使用多執行緒。 在實際的開發中對於效能優化的問題需要考慮到具體的場景來考慮是否使用多執行緒技術。一般來說一個程式是執行在一個程式中的,程式是具有一定獨立功能的程式、它是計算機系統進行資源分配和排程的一個獨立單位。而執行緒是程式的一個實體,是CPU排程和分派的基本單位,他是比程式更小的能獨立執行的基本單位。
在JMM中,執行緒可以把變數儲存在本地記憶體(比如機器的暫存器)中,而不是直接在主存中進行讀寫。這就可能造成一個執行緒在主存中修改了一個變數的值,而另一個執行緒還在繼續使用它在暫存器中的變數值的拷貝,造成資料的不一致,這樣就會導致執行緒不安全,下面介紹幾種Java中常見的執行緒同步的方式。
正文
關於執行緒不安全的原因是因為JMM定義了主記憶體跟工作記憶體,造成多個執行緒同事訪問同一個資源時導致的不一致問題,那麼要想解決這個問題其實也很簡單,也是從JMM入手,主要有以下3種方式,
- 保證每個執行緒訪問資源的時候獲取到的都是資源的最新值(可見性)
- 當有執行緒 操作該資源的時候鎖定該資源,禁止別的執行緒訪問(鎖)
- 執行緒本地私有化一份本地變數,執行緒每次讀寫自己的變數(ThreadLocal)
鎖
synchronized
採用synchronized修飾符實現的同步機制叫做互斥鎖機制,它所獲得的鎖叫做互斥鎖。每個物件都有一個鎖標記,當執行緒擁有這個鎖標記時才能訪問這個資源,沒有鎖標記便進入鎖池,互斥鎖分兩種一種是類鎖,一種是物件鎖。 類鎖:用於類的靜態方法或者一個類的class,一個物件只有一個 物件鎖:用於例項化的物件的普通方法,可以有多個
下面還是用程式設計師改bug這個例子來示範一下synchronized的使用方式
Bug類
public class Bug {
private static Integer bugNumber = 0;
public static int getBugNumber() {
return bugNumber;
}
//普通同步方法
public synchronized void addNormal() {
bugNumber++;
System.out.println("normalSynchronized--->" + getBugNumber());
}
//靜態同步方法
public static synchronized void addStatic() {
bugNumber++;
System.out.println("staticSynchronized--->" + getBugNumber());
}
//同步程式碼塊
public synchronized void addBlock() {
synchronized (bugNumber) {
this.bugNumber = ++bugNumber;
System.out.println("blockSynchronized--->" + getBugNumber());
}
}
}
複製程式碼
Runnable
public class BugRunnable implements Runnable {
private Bug mBug=new Bug();
@Override
public void run() {
mBug.addNormal();//普通方法同步
// mBug.addBlock();//同步程式碼塊
// Bug.addStatic();//靜態方法同步
}
}
複製程式碼
測試程式碼
public static void main(String[] args) {
BugRunnable bugRunnable = new BugRunnable();
for (int i = 0; i < 6; i++) {
new Thread(bugRunnable).start();
}
}
複製程式碼
同步程式碼塊
//同步程式碼塊
public synchronized void addBlock() {
synchronized (bugNumber) {
this.bugNumber = ++bugNumber;
System.out.println("blockSynchronized--->" + getBugNumber());
}
}
複製程式碼
測試結果
blockSynchronized--->1
blockSynchronized--->2
blockSynchronized--->3
blockSynchronized--->4
blockSynchronized--->5
blockSynchronized--->6
複製程式碼
普通方法同步
//普通同步方法
public synchronized void addNormal() {
bugNumber++;
System.out.println("normalSynchronized--->" + getBugNumber());
}
複製程式碼
測試結果
normalSynchronized--->1
normalSynchronized--->2
normalSynchronized--->3
normalSynchronized--->4
normalSynchronized--->5
normalSynchronized--->6
複製程式碼
靜態方法同步
//靜態同步方法
public static synchronized void addStatic() {
bugNumber++;
System.out.println("staticSynchronized--->" + getBugNumber());
}
複製程式碼
測試結果
staticSynchronized--->1
staticSynchronized--->2
staticSynchronized--->3
staticSynchronized--->4
staticSynchronized--->5
staticSynchronized--->6
複製程式碼
對比分析
- 類的每個例項都有自己的物件鎖。當一個執行緒訪問例項物件中的synchronized同步程式碼塊或同步方法時,該執行緒便獲取了該例項的物件級別鎖,其他執行緒這時如果要訪問同一個例項(因為物件可以有多個例項)同步程式碼塊或同步方法,必須等待當前執行緒釋放掉物件鎖才可以,如果是訪問類的另外一個例項,則不需要。
- 如果一個物件有多個同步方法或者程式碼塊,沒有獲取到物件鎖的執行緒將會被阻塞在所有同步方法之外,但是可以訪問非同步方法
- 對於靜態方法,實際上可以把它轉化成同步程式碼塊,就拿上面的靜態方法,實際上相當於:
//靜態同步方法
public static synchronized void addStatic() {
bugNumber++;
System.out.println("staticSynchronized--->" + getBugNumber());
}
//用同步程式碼塊
public static void changeStatic() {
synchronized (Bug.class) {
++bugNumber;
System.out.println("blockSynchronized--->" + getBugNumber());
}
}
複製程式碼
下面具體來總結一下三者的區別
- 同步程式碼塊:同步程式碼塊的範圍較小,只是鎖定了某個物件,所以效能較高
- 普通同步方法:給整個方法上鎖,效能較低
- 靜態同步方法:相當於整個類的同步程式碼塊,效能較低
ReentrantLock
除了synchronized這個關鍵字外,我們還能通過concurrent包下的Lock介面來實現這種效果,ReentrantLock是lock的一個實現類,可以在任何你想要的地方進行加鎖,比synchronized關鍵字更加靈活,下面看一下使用方式 使用方式
//ReentrantLock同步
public void addReentrantLock() {
mReentrantLock.lock();//上鎖
bugNumber++;
System.out.println("normalSynchronized--->" + getBugNumber());
mReentrantLock.unlock();//解鎖
}
複製程式碼
執行測試
ReentrantLock--->1
ReentrantLock--->2
ReentrantLock--->3
ReentrantLock--->4
ReentrantLock--->5
ReentrantLock--->6
複製程式碼
我們發現也是可以達到同步的目的,看一下ReentrantLock的繼承關係
ReentrantLock實現了lock介面,而lock介面只是定義了一些方法,所以相當於說ReentrantLock自己實現了一套加鎖機制,下面簡單分析一下ReentrantLock的同步機制,在分析前,需要知道幾個概念:
- CLH:AbstractQueuedSynchronizer中“等待鎖”的執行緒佇列。線上程併發的過程中,沒有獲得鎖的執行緒都會進入一個佇列,CLH就是管理這些等待鎖的佇列。
- CAS:比較並交換函式,它是原子操作函式,也就是說所有通過CAS操作的資料都是以原子方式進行的。
成員變數
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;//同步器
複製程式碼
成員變數除了序列化ID之外,只有一個Sync,那就看一看具體是什麼
Sync有兩個實現類,一個是FairSync,一個是NonfairSync,從名字可以大致推斷出一個是公平鎖,一個是非公平鎖,FairSync(公平鎖) lock方法:
final void lock() {
acquire(1);
}
複製程式碼
ReentrantLock是獨佔鎖,1表示的是鎖的狀態state。對於獨佔鎖而言,如果所處於可獲取狀態,其狀態為0,當鎖初次被執行緒獲取時狀態變成1,acquire最終呼叫的是tryAcquire方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 當c==0表示鎖沒有被任何執行緒佔用
(hasQueuedPredecessors),
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//鎖已經被執行緒佔用
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
複製程式碼
tryAcquire主要是去嘗試獲取鎖,獲取成功則設定鎖狀態並返回true,否則返回false
NonfairSync(非公平鎖) 非公平鎖NonfairSync的lock()與公平鎖的lock()在獲取鎖的流程上是一直的,但是由於它是非公平的,所以獲取鎖機制還是有點不同。通過前面我們瞭解到公平鎖在獲取鎖時採用的是公平策略(CLH佇列),而非公平鎖則採用非公平策略它無視等待佇列,直接嘗試獲取。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
複製程式碼
lock()通過compareAndSetState嘗試設定鎖的狀態,若成功直接將鎖的擁有者設定為當前執行緒(簡單粗暴),否則呼叫acquire()嘗試獲取鎖,對比一下,公平鎖跟非公平鎖的區別在於tryAcquire中
//NonfairSync
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//FairSync
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
複製程式碼
公平鎖中要通過hasQueuedPredecessors()來判斷該執行緒是否位於CLH佇列頭部,是則獲取鎖;而非公平鎖則不管你在哪個位置都直接獲取鎖。
unlock
public void unlock() {
sync.release(1);//釋放鎖
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
複製程式碼
對比分析
等待可中斷
-
synchronized:執行緒A跟執行緒B同時競爭同一把鎖,如果執行緒A獲得鎖之後不釋放,那麼執行緒B會一直等待下去,並不會釋放。
-
ReentrantLock:可以線上程等待了很長時間之後進行中斷,不需要一直等待。
鎖的公平性
公平鎖:是指多個執行緒在等待同一個鎖時,必須按照申請的時間順序來依次獲得鎖;非公平鎖:在鎖被釋放時,任何一個等待鎖的執行緒都有機會獲得鎖;
- synchronized:是非公平鎖
- ReentrantLock:可以是非公平鎖也可以是公平鎖
繫結條件
- synchronized中預設隱含條件。
- ReentrantLock可以繫結多個條件
可見性
volatile
記憶體語義
由於多個執行緒方法同一個變數,導致了執行緒安全問題,主要原因是因為執行緒的工作副本的變數跟主記憶體的不一致,如果能夠解決這個問題就可以保證執行緒同步,而Java提供了volatile關鍵字,可以幫助我們保證記憶體可見性,當我們宣告瞭一個volatile關鍵字,實際上有兩層含義;
- 禁止進行指令重排序。
- 一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
volatile是一種稍弱的同步機制,在訪問volatile變數時不會執行加鎖操作,也就不會執行執行緒阻塞,因此volatile變數是一種比synchronized關鍵字更輕量級的同步機制。
原理
在使用volatile關鍵字的時候,會多出一個lock字首指令,lock字首指令實際上相當於一個記憶體屏障實際上相當於一個記憶體屏障(也成記憶體柵欄),記憶體屏障會提供3個功能:
1)它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;
2)它會強制將對快取的修改操作立即寫入主存;
3)如果是寫操作,它會導致其他CPU中對應的快取行無效。
使用場景
這裡需要強調一點,volatile關鍵字並不一定能保證執行緒同步,如果非要採用volatile關鍵字來保證執行緒同步,則需要滿足以下條件:
- 對變數的寫操作不依賴於當前值
- 該變數沒有包含在具有其他變數的不變式中
其實看了一些書跟部落格,都是這麼寫的,按照我的理解實際上就是隻有當volatile修飾的物件是原子性操作,才能夠保證執行緒同步,為什麼呢。
測試程式碼:
class Volatile {
volatile static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Volatile.add();
}
}).start();
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count--->" + ++count);
}
private static void add() {
count++;
}
}
複製程式碼
執行結果
count--->1001
複製程式碼
理論上是1000才對,但是輸出的值是1001,為什麼呢,這個其實在之前的JMM中已經分析過了,下面再貼一張圖
跟之前一樣,我們每次從主記憶體中獲取到的count確實是最新的,但是由於對count的操作不是原子性操作,假如現在有兩個執行緒,執行緒1跟執行緒2,如果執行緒1讀取到了count值是5,然後read--->load進記憶體了,然後現在被執行緒2搶佔了CPU,那麼執行緒2就開始read--->load,並且完成了工作副本的賦值操作,並且將count 的值回寫到主記憶體中,由於執行緒1已經進行了load操作,所以不會再去主記憶體中讀取,會接著進行自己的操作,這樣的話就出現了執行緒不安全,所以volatile必須是原子性操作才能保證執行緒安全。 基於以上考慮,volatile主要用來做一些標記位的處理:
volatile boolean flag = false;
//執行緒1
while(!flag){
doSomething();
}
//執行緒2
public void setFlag() {
flag = true;
}
複製程式碼
當有多個執行緒進行訪問的時候,只要有一個執行緒改變了flag的狀態,那麼這個狀態會被重新整理到主記憶體,就會對所有執行緒可見,那麼就可以保證執行緒安全。
automatic
automatic是JDK1.5之後Java新增的concurrent包中的一個類,雖然volatile可以保證記憶體可見性,大部分操作都不是原子性操作,那麼volatile的使用場景就比較單一,然後Java提供了automatic這個包,可以幫助我們來保證一些操作是原子性的。
使用方式
替換之前的volatile程式碼
public static AtomicInteger atomicInteger = new AtomicInteger(0);
private static void add() {
atomicInteger.getAndIncrement();
}
複製程式碼
測試一下:
AtomicInteger: 1000
複製程式碼
原理解析
AtomicInteger既保證了volatile保證不了的原子性,同時也實現了可見性,那麼它是如何做到的呢?
成員變數
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value;
複製程式碼
運算方式
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
int compare_and_swap(int reg, int oldval, int newval) {
ATOMIC();
int old_reg_val = reg;
if (old_reg_val == oldval)
reg = newval;
END_ATOMIC();
return old_reg_val;
}
複製程式碼
分析之前需要知道兩個概念:
-
悲觀鎖(Pessimistic Lock), 顧名思義,就是很悲觀,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會block直到它拿到鎖。
-
樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制。
compare_and_swap這個才是核心方法,也就是上面提到的CAS,因為CAS是基於樂觀鎖的,也就是說當寫入的時候,如果暫存器舊值已經不等於現值,說明有其他CPU在修改,那就繼續嘗試。所以這就保證了操作的原子性。
變數私有化
這種方式實際上指的就是ThreadLocal,翻譯過來是執行緒本地變數,ThreadLocal會為每個使用該變數的執行緒提供獨立的變數副本,但是這個副本並不是從主記憶體中進行讀取的,而是自己建立的,每個副本相互之間獨立,互不影響。相對於syncronized的以時間換空間,ThreadLocal剛好相反,可以減少執行緒併發的複雜度。
簡單使用
class ThreadLocalDemo {
public static ThreadLocal<String> local = new ThreadLocal<>();//宣告靜態的threadlocal變數
public static void main(String[] args) {
local.set("Android");
for (int i = 0; i < 5; i++) {
SetThread localThread = new SetThread();//建立5個執行緒
new Thread(localThread).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(local.get());
}
static class SetThread implements Runnable {
@Override
public void run() {
local.set(Thread.currentThread().getName());
}
}
}
複製程式碼
進行 測試
Android
複製程式碼
雖然我用for迴圈建立了好幾個執行緒,但是並沒有改變ThreadLocal中的值,依然是我的大Android,這個就能夠說明我賦的值是跟我的執行緒繫結的,每個執行緒有特定的值。
原始碼分析
成員變數
private final int threadLocalHashCode = nextHashCode();//當前執行緒的hash值
private static AtomicInteger nextHashCode =//下一個執行緒的hash值
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;//hash增長因子
複製程式碼
建構函式
public ThreadLocal() {
}
複製程式碼
空實現。。。。
set方法
public void set(T value) {
Thread t = Thread.currentThread();//獲取到當前執行緒
ThreadLocalMap map = getMap(t);//獲取一個map
if (map != null)
//map不為空,直接進行賦值
map.set(this, value);
else
//map為空,建立一個Map
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
複製程式碼
ThreadLocalMap
上面建立的Map實際上是一個ThreadLocalMap,也即是用來儲存跟執行緒繫結的資料的,之間看過HashMap的原始碼,既然也叫Map,那麼其實應該是差不多的
基本方法
成員變數
private static final int INITIAL_CAPACITY = 16;//初始容量,2的冪
private Entry[] table;//用來存放entry的陣列
private int size = 0;//陣列長度
private int threshold; // 閾值
//Entry繼承了WeakReference,說明key弱引用,便於記憶體回收
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
複製程式碼
構造方法
ThreadLocalMap(java.lang.ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table陣列
table = new Entry[INITIAL_CAPACITY];
// 通過hash值來計算存放的索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 建立entry節點
table[i] = new Entry(firstKey, firstValue);
// 陣列長度由0到1
size = 1;
// 將閾值設定成為初始容量
setThreshold(INITIAL_CAPACITY);
}
複製程式碼
還有一個構造方法是傳一個Map,跟傳key-value大同小異就不解釋了
getEntry
private Entry getEntry(ThreadLocal<?> key) {
//通過key來計算陣列下標
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
//遍歷到直接返回
return e;
else
//沒有遍歷到就會呼叫getEntryAfterMiss,繼續遍歷
return getEntryAfterMiss(key, i, e);
}
複製程式碼
set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;//拿到table陣列
int len = tab.length;//獲取table的長度
int i = key.threadLocalHashCode & (len-1);//計算下標
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
//如便利到相同的可以,那麼取而代之
e.value = value;
return;
}
if (k == null) {
//替換key值為空的entry
replaceStaleEntry(key, value, i);//
return;
}
}
tab[i] = new Entry(key, value);//進行賦值
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
複製程式碼
remove方法
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//遍歷下標尋找i
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);//清理指定的key
return;
}
}
}
複製程式碼
基本上分析到這裡已經將ThreadLocal分析清楚了,它的核心是一個ThreadLocalMap,存放了一個entry陣列,期中key是ThreadLocal的weakreference,value就是set的值,然後每次set跟get都會對已有的entry進行清理,加商weakreference就可以最大限度的放置記憶體洩露。
死鎖
定義
死鎖:是指多個執行緒因競爭資源而造成的一種僵局(互相等待),若無外力作用,這些程式都將無法向前推進。
下面舉一個死鎖的例子
public class DeadLock implements Runnable {
public int flag = 1;
//靜態物件是類的所有物件共享的
private static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
System.out.println("flag=" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;
//td1,td2都處於可執行狀態,但JVM執行緒排程先執行哪個執行緒是不確定的。
//td2的run()可能在td1的run()之前執行
new Thread(td1).start();
new Thread(td2).start();
}
}
複製程式碼
不管哪個執行緒先啟動,啟動的執行緒都會先sleep500ms,讓另外一個執行緒獲得CPU的使用權,這樣一來就保證了執行緒td1獲取到了O1的物件鎖,在競爭O2的物件鎖,td2獲取到了O2的物件鎖,在競爭O1的物件鎖,呵呵,這就尷尬了,然後互不想讓,就卡死了,造成了死鎖。
死鎖產生的必要條件
- 1)互斥條件:指程式對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個程式佔用。如果此時還有其它程式請求資源,則請求者只能等待,直至佔有資源的程式用畢釋放。
- 2)請求和保持條件:指程式已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它程式佔有,此時請求程式阻塞,但又對自己已獲得的其它資源保持不放。
- 3)不剝奪條件:指程式已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
- 4)環路等待條件:指在發生死鎖時,必然存在一個程式——資源的環形鏈。
預防死鎖
- 打破互斥條件。即允許程式同時訪問某些資源。
- 打破不可搶佔條件。即允許程式強行從佔有者那裡奪取某些資源。就是說,當一個程式已佔有了某些資源,它又申請新的資源,但不能立即被滿足時,它必須釋放所佔有的全部資源,以後再重新申請。
- 打破佔有且申請條件。可以實行資源預先分配策略。即程式在執行前一次性地向系統申請它所需要的全部資源。
- 打破迴圈等待條件,實行資源有序分配策略。採用這種策略,即把資源事先分類編號,按號分配,使程式在申請,佔用資源時不會形成環路。所有程式對資源的請求必須嚴格按資源序號遞增的順序提出。