一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - Java多線程中ReentrantLock與Condition詳解

Java多線程中ReentrantLock與Condition詳解

2021-02-18 12:35vernonzheng Java教程

這篇文章主要介紹了Java多線程中ReentrantLock與Condition詳解,需要的朋友可以參考下

一、ReentrantLock

1.1什么是reentrantlock

java.util.concurrent.lock中的Lock框架是鎖定的一個抽象,它允許把鎖定的實現作為Java類,而不是作為語言的特性來實現。這就為Lock的多種實現留下了空間,各種實現可能有不同的調度算法、性能特性或者鎖定語義。ReentrantLock類實現了Lock,它擁有與synchronized相同的并發性和內存語義,但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的性能。(換句話說,當許多線程都想訪問共享資源時,JVM可以花更少的時候來調度線程,把更多時間用在執行線程上。)

reentrant鎖意味著什么呢?簡單來說,它有一個與鎖相關的獲取計數器,如果擁有鎖的某個線程再次得到鎖,那么獲取計數器就加1,然后鎖需要被釋放兩次才能獲得真正釋放。這模仿了synchronized的語義;如果線程進入由線程已經擁有的監控器保護的synchronized塊,就允許線程繼續進行,當線程退出第二個(或者后續)synchronized塊的時候,不釋放鎖,只有線程退出它進入的監控器保護的第一個synchronized塊時,才釋放鎖。

1.2ReentrantLock與synchronized的比較

相同:ReentrantLock提供了synchronized類似的功能和內存語義。

不同:

(1)ReentrantLock功能性方面更全面,比如時間鎖等候,可中斷鎖等候,鎖投票等,因此更有擴展性。在多個條件變量和高度競爭鎖的地方,用ReentrantLock更合適,ReentrantLock還提供了Condition,對線程的等待和喚醒等操作更加靈活,一個ReentrantLock可以有多個Condition實例,所以更有擴展性。

(2)ReentrantLock的性能比synchronized會好點。

(3)ReentrantLock提供了可輪詢的鎖請求,他可以嘗試的去取得鎖,如果取得成功則繼續處理,取得不成功,可以等下次運行的時候處理,所以不容易產生死鎖,而synchronized則一旦進入鎖請求要么成功,要么一直阻塞,所以更容易產生死鎖。

1.3ReentrantLock擴展的功能

1.3.1實現可輪詢的鎖請求

在內部鎖中,死鎖是致命的——唯一的恢復方法是重新啟動程序,唯一的預防方法是在構建程序時不要出錯。而可輪詢的鎖獲取模式具有更完善的錯誤恢復機制,可以規避死鎖的發生。

如果你不能獲得所有需要的鎖,那么使用可輪詢的獲取方式使你能夠重新拿到控制權,它會釋放你已經獲得的這些鎖,然后再重新嘗試。可輪詢的鎖獲取模式,由tryLock()方法實現。此方法僅在調用時鎖為空閑狀態才獲取該鎖。如果鎖可用,則獲取鎖,并立即返回值true。如果鎖不可用,則此方法將立即返回值false。此方法的典型使用語句如下:

?
1
2
3
4
5
6
7
8
9
10
Lock lock = ...;
if (lock.tryLock()) {
try {
// manipulate protected state
} finally {
lock.unlock();
}
} else {
// perform alternative actions
}

1.3.2實現可定時的鎖請求

當使用內部鎖時,一旦開始請求,鎖就不能停止了,所以內部鎖給實現具有時限的活動帶來了風險。為了解決這一問題,可以使用定時鎖。當具有時限的活

動調用了阻塞方法,定時鎖能夠在時間預算內設定相應的超時。如果活動在期待的時間內沒能獲得結果,定時鎖能使程序提前返回。可定時的鎖獲取模式,由tryLock(long,TimeUnit)方法實現。

1.3.3實現可中斷的鎖獲取請求

可中斷的鎖獲取操作允許在可取消的活動中使用。lockInterruptibly()方法能夠使你獲得鎖的時候響應中斷。

1.4ReentrantLock不好與需要注意的地方

(1)lock必須在finally塊中釋放。否則,如果受保護的代碼將拋出異常,鎖就有可能永遠得不到釋放!這一點區別看起來可能沒什么,但是實際上,它極為重要。忘記在finally塊中釋放鎖,可能會在程序中留下一個定時炸彈,當有一天炸彈爆炸時,您要花費很大力氣才有找到源頭在哪。而使用同步,JVM將確保鎖會獲得自動釋放

(2)當JVM用synchronized管理鎖定請求和釋放時,JVM在生成線程轉儲時能夠包括鎖定信息。這些對調試非常有價值,因為它們能標識死鎖或者其他異常行為的來源。Lock類只是普通的類,JVM不知道具體哪個線程擁有Lock對象。

二、條件變量Condition

條件變量很大一個程度上是為了解決Object.wait/notify/notifyAll難以使用的問題。

我們通過一個實際的例子來解釋Condition的用法:

我們要打印1到9這9個數字,由A線程先打印1,2,3,然后由B線程打印4,5,6,然后再由A線程打印7,8,9. 這道題有很多種解法,現在我們使用Condition來做這道題

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package cn.outofmemory.locks;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class App {
    static class NumberWrapper {
        public int value = 1;
    }
    public static void main(String[] args) {
        //初始化可重入鎖
        final Lock lock = new ReentrantLock();
        //第一個條件當屏幕上輸出到3
        final Condition reachThreeCondition = lock.newCondition();
        //第二個條件當屏幕上輸出到6
        final Condition reachSixCondition = lock.newCondition();
        //NumberWrapper只是為了封裝一個數字,一邊可以將數字對象共享,并可以設置為final
        //注意這里不要用Integer, Integer 是不可變對象
        final NumberWrapper num = new NumberWrapper();
        //初始化A線程
        Thread threadA = new Thread(new Runnable() {
            @Override
                        public void run() {
                //需要先獲得鎖
                lock.lock();
                try {
                    System.out.println("threadA start write");
                    //A線程先輸出前3個數
                    while (num.value <= 3) {
                        System.out.println(num.value);
                        num.value++;
                    }
                    //輸出到3時要signal,告訴B線程可以開始了
                    reachThreeCondition.signal();
                }
                finally {
                    lock.unlock();
                }
                lock.lock();
                try {
                    //等待輸出6的條件
                    reachSixCondition.await();
                    System.out.println("threadA start write");
                    //輸出剩余數字
                    while (num.value <= 9) {
                        System.out.println(num.value);
                        num.value++;
                    }
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
                finally {
                    lock.unlock();
                }
            }
        }
        );
        Thread threadB = new Thread(new Runnable() {
            @Override
                        public void run() {
                try {
                    lock.lock();
                    while (num.value <= 3) {
                        //等待3輸出完畢的信號
                        reachThreeCondition.await();
                    }
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
                finally {
                    lock.unlock();
                }
                try {
                    lock.lock();
                    //已經收到信號,開始輸出4,5,6
                    System.out.println("threadB start write");
                    while (num.value <= 6) {
                        System.out.println(num.value);
                        num.value++;
                    }
                    //4,5,6輸出完畢,告訴A線程6輸出完了
                    reachSixCondition.signal();
                }
                finally {
                    lock.unlock();
                }
            }
        }
        );
        //啟動兩個線程
        threadB.start();
        threadA.start();
    }
}

上述代碼中有完整的注釋,請參考注釋,理解Condition的用法。

基本思路就是首先要A線程先寫1,2,3,這時候B線程應該等待reachThredCondition信號,而當A線程寫完3之后就通過signal告訴B線程“我寫到3了,該你了”,這時候A線程要等嗲reachSixCondition信號,同時B線程得到通知,開始寫4,5,6,寫完4,5,6之后B線程通知A線程reachSixCondition條件成立了,這時候A線程就開始寫剩下的7,8,9了。條件(也稱為條件隊列或條件變量)為線程提供了一個含義,以便在某個狀態條件現在可能為true的另一個線程通知它之前,一直掛起該線程(即讓其“等待”)。因為訪問此共享狀態信息發生在不同的線程中,所以它必須受保護,因此要將某種形式的鎖與該條件相關聯。等待提供一個條件的主要屬性是:以原子方式釋放相關的鎖,并掛起當前線程,就像Object.wait做的那樣。

上述API說明表明條件變量需要與鎖綁定,而且多個Condition需要綁定到同一鎖上。前面的Lock中提到,獲取一個條件變量的方法是Lock.newCondition()。

?
1
2
3
4
5
6
7
voidawait()throwsInterruptedException;
voidawaitUninterruptibly();
longawaitNanos(longnanosTimeout)throwsInterruptedException;
booleanawait(longtime,TimeUnitunit)throwsInterruptedException;
booleanawaitUntil(Datedeadline)throwsInterruptedException;
voidsignal();
voidsignalAll();

以上是Condition接口定義的方法,await*對應于Object.wait,signal對應于Object.notify,signalAll對應于Object.notifyAll。特別說明的是Condition的接口改變名稱就是為了避免與Object中的wait/notify/notifyAll的語義和使用上混淆,因為Condition同樣有wait/notify/notifyAll方法。

每一個Lock可以有任意數據的Condition對象,Condition是與Lock綁定的,所以就有Lock的公平性特性:如果是公平鎖,線程為按照FIFO的順序從Condition.await中釋放,如果是非公平鎖,那么后續的鎖競爭就不保證FIFO順序了。

一個使用Condition實現生產者消費者的模型例子如下。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProductQueue<T> {
    private final T[] items;
    private final Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();
    //
    private int head, tail, count;
    public ProductQueue(int maxSize) {
        items = (T[]) new Object[maxSize];
    }
    public ProductQueue() {
        this(10);
    }
    public void put(T t) throws InterruptedException {
        lock.lock();
        try {
            while (count == getCapacity()) {
                notFull.await();
            }
            items[tail] = t;
            if (++tail == getCapacity()) {
                tail = 0;
            }
            ++count;
            notEmpty.signalAll();
        }
        finally {
            lock.unlock();
        }
    }
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();
            }
            T ret = items[head];
            items[head] = null;
            //GC
            //
            if (++head == getCapacity()) {
                head = 0;
            }
            --count;
            notFull.signalAll();
            return ret;
        }
        finally {
            lock.unlock();
        }
    }
    public int getCapacity() {
        return items.length;
    }
    public int size() {
        lock.lock();
        try {
            return count;
        }
        finally {
            lock.unlock();
        }
    }
}

在這個例子中消費take()需要隊列不為空,如果為空就掛起(await()),直到收到notEmpty的信號;生產put()需要隊列不滿,如果滿了就掛起(await()),直到收到notFull的信號。

可能有人會問題,如果一個線程lock()對象后被掛起還沒有unlock,那么另外一個線程就拿不到鎖了(lock()操作會掛起),那么就無法通知(notify)前一個線程,這樣豈不是“死鎖”了?

2.1await*操作

上一節中說過多次ReentrantLock是獨占鎖,一個線程拿到鎖后如果不釋放,那么另外一個線程肯定是拿不到鎖,所以在lock.lock()和lock.unlock()之間可能有一次釋放鎖的操作(同樣也必然還有一次獲取鎖的操作)。我們再回頭看代碼,不管take()還是put(),在進入lock.lock()后唯一可能釋放鎖的操作就是await()了。也就是說await()操作實際上就是釋放鎖,然后掛起線程,一旦條件滿足就被喚醒,再次獲取鎖!

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final void await() throws InterruptedException {
 if (Thread.interrupted())
  throw new InterruptedException();
 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)
  unlinkCancelledWaiters();
 if (interruptMode != 0)
  reportInterruptAfterWait(interruptMode);
}

上面是await()的代碼片段。上一節中說過,AQS在獲取鎖的時候需要有一個CHL的FIFO隊列,所以對于一個Condition.await()而言,如果釋放了鎖,要想再一次獲取鎖那么就需要進入隊列,等待被通知獲取鎖。完整的await()操作是安裝如下步驟進行的:

將當前線程加入Condition鎖隊列。特別說明的是,這里不同于AQS的隊列,這里進入的是Condition的FIFO隊列。后面會具體談到此結構。進行2。

釋放鎖。這里可以看到將鎖釋放了,否則別的線程就無法拿到鎖而發生死鎖。進行3。

自旋(while)掛起,直到被喚醒或者超時或者CACELLED等。進行4。

獲取鎖(acquireQueued)。并將自己從Condition的FIFO隊列中釋放,表明自己不再需要鎖(我已經拿到鎖了)。

這里再回頭介紹Condition的數據結構。我們知道一個Condition可以在多個地方被await*(),那么就需要一個FIFO的結構將這些Condition串聯起來,然后根據需要喚醒一個或者多個(通常是所有)。所以在Condition內部就需要一個FIFO的隊列。

?
1
2
private transient Node firstWaiter;
private transient Node lastWaiter;

上面的兩個節點就是描述一個FIFO的隊列。我們再結合前面提到的節點(Node)數據結構。我們就發現Node.nextWaiter就派上用場了!nextWaiter就是將一系列的Condition.await*串聯起來組成一個FIFO的隊列。

2.2signal/signalAll操作

await*()清楚了,現在再來看signal/signalAll就容易多了。按照signal/signalAll的需求,就是要將Condition.await*()中FIFO隊列中第一個Node喚醒(或者全部Node)喚醒。盡管所有Node可能都被喚醒,但是要知道的是仍然只有一個線程能夠拿到鎖,其它沒有拿到鎖的線程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void doSignal(Node first) {
  do {
    if ( (firstWaiter = first.nextWaiter) == null)
      lastWaiter = null;
    first.nextWaiter = null;
  } while (!transferForSignal(first) &&
       (first = firstWaiter) != null);
}
 
private void doSignalAll(Node first) {
  lastWaiter = firstWaiter = null;
  do {
    Node next = first.nextWaiter;
    first.nextWaiter = null;
    transferForSignal(first);
    first = next;
  } while (first != null);
}

?
1
2
3
4
5
6
7
8
9
10
final boolean transferForSignal(Node node) {
  if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
    return false;
 
  Node p = enq(node);
  int c = p.waitStatus;
  if (c > 0 || !compareAndSetWaitStatus(p, c, Node.SIGNAL))
    LockSupport.unpark(node.thread);
  return true;
}

上面就是喚醒一個await*()線程的過程,根據前面的小節介紹的,如果要unpark線程,并使線程拿到鎖,那么就需要線程節點進入AQS的隊列。所以可以看到在LockSupport.unpark之前調用了enq(node)操作,將當前節點加入到AQS隊列。

總結

以上就是本文關于Java多線程中ReentrantLock與Condition詳解的全部內容,希望對大家有所幫助。有什么問題可以隨時留言,小編會及時回復大家的。

原文鏈接:http://blog.csdn.net/vernonzheng/article/details/8288251

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 女女宿舍互慰h文小说 | 免费看打屁股视频的软件 | 亚洲人成毛片线播放 | 性刺激欧美三级在线现看中文 | 国产成人a∨麻豆精品 | 免费看www | 狠狠插入 | 动态图啪啪120秒免费看 | 色婷婷久久综合中文久久一本` | 欧美free激情野战hd | 激情综合站 | 国产视频久久久久 | 99视频有精品 | 午夜精品久久久 | 高h全肉动漫在线观看免费 高h辣h双处全是肉军婚 | 日韩一区二区三区四区不卡 | 8x8拨擦拨擦华人免费 | 我年轻漂亮的继坶2中字在线播放 | 久久久久国产一级毛片高清片 | 男男视频18免费网站 | 羞羞麻豆国产精品1区2区3区 | 我的漂亮朋友在线观看全集免费 | 亚洲国产99| 深夜视频在线播放 | 亚洲精品第一国产综合 | 2021麻豆剧果冻传媒入口永久 | 欧洲第一页| 99热99在线| 成人在线观看一区 | 成人网视频免费播放 | 亚洲精品在看在线观看 | 九九99亚洲精品久久久久 | 加勒比伊人 | 五月天精品视频在线观看 | 精选国产AV精选一区二区三区 | 每天都要睡男人(nph) | 搡60一70岁的老女人小说 | 日韩特级片 | 脱女学小内内摸出水网站免费 | 强女明星系列小说 | 精品国产欧美一区二区 |