国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Java 8 并發:同步和鎖

andycall / 1018人閱讀

摘要:可重入意味著鎖被綁定到當前線程,線程可以安全地多次獲取相同的鎖,而不會發生死鎖例如同步方法在同一對象上調用另一個同步方法。寫入鎖釋放后,兩個任務并行執行,它們不必等待對方是否完成,因為只要沒有線程持有寫入鎖,它們就可以同時持有讀取鎖。

原文地址: Java 8 Concurrency Tutorial: Synchronization and Locks

為了簡單起見,本教程的示例代碼使用了在這里定義的兩個輔助方法,sleep(seconds)stop(executor)

Synchronized

當我們編寫多線程代碼訪問可共享的變量時需要特別注意,下面是一個多線程去改變一個整數的例子。

定義一個變量 count,定義一個方法 increment() 使 count 增加 1.

int count = 0;

void increment() {
    count = count + 1;
}

當多個線程同時調用 increment() 時就會出現問題:

ExecutorService executor = Executors.newFixedThreadPool(2);

IntStream.range(0, 10000)
    .forEach(i -> executor.submit(this::increment));

stop(executor);

System.out.println(count);  // 9965

上面的代碼執行結果并不是10000,原因是我們在不同的線程上共享一個變量,而沒有給這個變量的訪問設置競爭條件。

為了增加數字,必須執行三個步驟:(i) 讀取當前值;(ii) 將該值增加1;(iii) 將新值寫入變量;如果兩個線程并行執行這些步驟,則兩個線程可能同時執行步驟1,從而讀取相同的當前值。 這導致寫入丟失,所以實際結果較低。 在上面的示例中,35個增量由于并發非同步訪問計數而丟失,但是當你自己執行代碼時可能會看到不同的結果。

幸運的是,Java 早期通過 synchronized 關鍵字支持線程同步。增加計數時,我們可以利用同步來解決上述競爭條件:

synchronized void incrementSync() {
    count = count + 1;
}

當我們使用 incrementSync() 方法時,我們得到了希望的結果,而且每次執行的結果都是這樣的。

ExecutorService executor = Executors.newFixedThreadPool(2);

IntStream.range(0, 10000)
    .forEach(i -> executor.submit(this::incrementSync));

stop(executor);

System.out.println(count);  // 10000

synchronized 關鍵值也可以用在一個語句塊中

void incrementSync() {
    synchronized (this) {
        count = count + 1;
    }
}

JVM 的內部使用了一個監視器,也可以稱為監視器鎖和內部鎖來管理同步。這個監視器被綁定到一個對象上,當使用同步方法時,每個方法共享相應對象的監視器。

所有隱式監視器都實現了可重入特性。 可重入意味著鎖被綁定到當前線程,線程可以安全地多次獲取相同的鎖,而不會發生死鎖(例如同步方法在同一對象上調用另一個同步方法)。

Locks

除了使用關鍵字 synchronized 支持的隱式鎖(對象的內置鎖)外,Concurrency API 支持由 Lock 接口指定的各種顯示鎖。顯示鎖能控制更細的粒度,因此也有更好的性能,在邏輯上也比較清晰。

標準 JDK中提供了多種顯示鎖的實現,將在下面的章節中進行介紹。

ReentrantLock

ReentrantLock 類是一個互斥鎖,它和 synchronized 關鍵字訪問的隱式鎖具有相同的功能,但它具有擴展功能。它也實現了可重入的功能。

下面來看看如何使用 ReentrantLock

ReentrantLock lock = new ReentrantLock();
int count = 0;

void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}

鎖通過 lock() 獲取,通過 unlock() 釋放,將代碼封裝到 try/finally 塊中是非常重要的,以確保在出現異常的時候也能釋放鎖。這個方法和使用關鍵字 synchronized 修飾的方法是一樣是線程安全的。如果一個線程已經獲得了鎖,后續線程調用 lock() 會暫停線程,直到鎖被釋放,永遠只有一個線程能獲取鎖。

lock 支持更細粒度的去控制一個方法的同步,如下面的代碼:

ExecutorService executor = Executors.newFixedThreadPool(2);
ReentrantLock lock = new ReentrantLock();

executor.submit(() -> {
    lock.lock();
    try {
        sleep(1000);
    } finally {
        lock.unlock();
    }
});

executor.submit(() -> {
    System.out.println("Locked: " + lock.isLocked());
    System.out.println("Held by me: " + lock.isHeldByCurrentThread());
    boolean locked = lock.tryLock();
    System.out.println("Lock acquired: " + locked);
});

stop(executor);

當第一個任務獲取鎖時,第二個任務獲取鎖的狀態信息:

Locked: true
Held by me: false
Lock acquired: false

作為 lock() 方法的替代方法 tryLock() 嘗試去獲取鎖而不暫停當前線程,必須使用 bool 結果去判斷是否真的獲取到了鎖。

ReadWriteLock

ReadWriteLock 指定了另一種類型的鎖,即讀寫鎖。讀寫鎖實現的邏輯是,當沒有線程在寫這個變量時,其他的線程可以讀取這個變量,所以就是當沒有線程持有寫鎖時,讀鎖就可以被所有的線程持有。如果讀取比寫更頻繁,這將增加系統的性能和吞吐量。

ExecutorService executor = Executors.newFixedThreadPool(2);
Map map = new HashMap<>();
ReadWriteLock lock = new ReentrantReadWriteLock();

executor.submit(() -> {
    lock.writeLock().lock();
    try {
        sleep(1000);
        map.put("foo", "bar");
    } finally {
        lock.writeLock().unlock();
    }
});

上面的例子首先獲取一個寫入鎖,在 sleep 1秒后在 map 中寫入值,在這個任務完成之前,還有兩個任務正在提交,試圖從 map 讀取值:

Runnable readTask = () -> {
    lock.readLock().lock();
    try {
        System.out.println(map.get("foo"));
        sleep(1000);
    } finally {
        lock.readLock().unlock();
    }
};

executor.submit(readTask);
executor.submit(readTask);

stop(executor);

當執行上面的代碼時,你會注意到兩人讀取的任務必須等待直到寫入完成(當在讀取的時候,寫是不能獲取鎖的)。寫入鎖釋放后,兩個任務并行執行,它們不必等待對方是否完成,因為只要沒有線程持有寫入鎖,它們就可以同時持有讀取鎖。

StampedLock

Java 8 提供了一種新類型的鎖 StampedLock,像上面的例子一樣它也支持讀寫鎖,與 ReadWriteLock 不同的是,StampedLock 的鎖定方法返回一個 long 值,可以利用這個值檢查是否釋放鎖和鎖仍然有效。另外 StampedLock 支持另外一種稱為樂觀鎖的模式。

下面使用 StampedLock 來替換 ReadWriteLock

ExecutorService executor = Executors.newFixedThreadPool(2);
Map map = new HashMap<>();
StampedLock lock = new StampedLock();

executor.submit(() -> {
    long stamp = lock.writeLock();
    try {
        sleep(1000);
        map.put("foo", "bar");
    } finally {
        lock.unlockWrite(stamp);
    }
});

Runnable readTask = () -> {
    long stamp = lock.readLock();
    try {
        System.out.println(map.get("foo"));
        sleep(1000);
    } finally {
        lock.unlockRead(stamp);
    }
};

executor.submit(readTask);
executor.submit(readTask);

stop(executor);

通過 readLock()writeLock() 方法來獲取讀寫鎖會返回一個稍后用于在 finally 塊中釋放鎖的值。注意,這里的鎖不是可重入的。每次鎖定都會返回一個新的值,并在沒有鎖的情況下阻塞,在使用的時候要注意不要死鎖。

就像前面 ReadWriteLock 中的示例一樣,兩個讀取任務必須等待寫入任務釋放鎖。然后同時并行執行打印結果到控制臺。

下面的例子演示了樂觀鎖

ExecutorService executor = Executors.newFixedThreadPool(2);
StampedLock lock = new StampedLock();

executor.submit(() -> {
    long stamp = lock.tryOptimisticRead();
    try {
        System.out.println("Optimistic Lock Valid: " + lock.validate(stamp));
        sleep(1000);
        System.out.println("Optimistic Lock Valid: " + lock.validate(stamp));
        sleep(2000);
        System.out.println("Optimistic Lock Valid: " + lock.validate(stamp));
    } finally {
        lock.unlock(stamp);
    }
});

executor.submit(() -> {
    long stamp = lock.writeLock();
    try {
        System.out.println("Write Lock acquired");
        sleep(2000);
    } finally {
        lock.unlock(stamp);
        System.out.println("Write done");
    }
});

stop(executor);

通過調用 tryOptimisticRead() 來獲取樂觀讀寫鎖tryOptimisticRead()總是返回一個值,而不會阻塞當前線程,也不關鎖是否可用。如果有一個寫鎖激活則返回0。可以通過 lock.validate(stamp) 來檢查返回的標記(long 值)是否有效。

執行上面的代碼輸出:

Optimistic Lock Valid: true
Write Lock acquired
Optimistic Lock Valid: false
Write done
Optimistic Lock Valid: false

樂觀鎖在獲得鎖后立即生效。與普通讀鎖相反,樂觀鎖不會阻止其他線程立即獲得寫鎖。在第一個線程休眠一秒之后,第二個線程獲得一個寫鎖,而不用等待樂觀讀鎖解除。樂觀的讀鎖不再有效,即使寫入鎖定被釋放,樂觀的讀取鎖仍然無效。

因此,在使用樂觀鎖時,必須在每次訪問任何共享的變量后驗證鎖,以確保讀取仍然有效。

有時將讀鎖轉換為寫鎖并不需要再次解鎖和鎖定是有用的。StampedLock 為此提供了tryConvertToWriteLock() 方法,如下面的示例所示:

ExecutorService executor = Executors.newFixedThreadPool(2);
StampedLock lock = new StampedLock();

executor.submit(() -> {
    long stamp = lock.readLock();
    try {
        if (count == 0) {
            stamp = lock.tryConvertToWriteLock(stamp);
            if (stamp == 0L) {
                System.out.println("Could not convert to write lock");
                stamp = lock.writeLock();
            }
            count = 23;
        }
        System.out.println(count);
    } finally {
        lock.unlock(stamp);
    }
});

stop(executor);

該任務首先獲得一個讀鎖,并將當前的變量計數值打印到控制臺。 但是,如果當前值為 0,我們要分配一個新的值23。我們首先必須將讀鎖轉換為寫鎖,以不打破其他線程的潛在并發訪問。 調用 tryConvertToWriteLock() 不會阻塞,但可能會返回 0,指示當前沒有寫鎖定可用。 在這種情況下,我們調用writeLock()來阻塞當前線程,直到寫鎖可用。

Semaphores

除了鎖之外,并發API還支持計數信號量。 鎖通常授予對變量或資源的獨占訪問權,而信號量則能夠維護整套許可證。 在不同的情況下,必須限制對應用程序某些部分的并發訪問量。

下面是一個如何限制對長時間任務的訪問的例子:

ExecutorService executor = Executors.newFixedThreadPool(10);

Semaphore semaphore = new Semaphore(5);

Runnable longRunningTask = () -> {
    boolean permit = false;
    try {
        permit = semaphore.tryAcquire(1, TimeUnit.SECONDS);
        if (permit) {
            System.out.println("Semaphore acquired");
            sleep(5000);
        } else {
            System.out.println("Could not acquire semaphore");
        }
    } catch (InterruptedException e) {
        throw new IllegalStateException(e);
    } finally {
        if (permit) {
            semaphore.release();
        }
    }
};

IntStream.range(0, 10)
    .forEach(i -> executor.submit(longRunningTask));

stop(executor);

執行程序可以同時運行10個任務,但是我們使用5信號量,因此限制并發訪問為5個。使用try/finally塊,即使在異常的情況下正確釋放信號量也是非常重要的。

運行上面的代碼輸出:

Semaphore acquired
Semaphore acquired
Semaphore acquired
Semaphore acquired
Semaphore acquired
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore

當有 5 個任務獲取型號量后,隨后的任務便不能獲取信號量了。但是如果前面 5 的任務執行完成,finally 塊釋放了型號量,隨后的線程就可以獲取星號量了,總數不會超過5個。這里調用 tryAcquire() 獲取型號量設置了超時時間1秒,意味著當線程獲取信號量失敗后可以阻塞等待1秒再獲取。

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70892.html

相關文章

  • Java 8 并發教程:同步和鎖

    摘要:在接下來的分鐘,你將會學會如何通過同步關鍵字,鎖和信號量來同步訪問共享可變變量。所以在使用樂觀鎖時,你需要每次在訪問任何共享可變變量之后都要檢查鎖,來確保讀鎖仍然有效。 原文:Java 8 Concurrency Tutorial: Synchronization and Locks譯者:飛龍 協議:CC BY-NC-SA 4.0 歡迎閱讀我的Java8并發教程的第二部分。這份指南將...

    wyk1184 評論0 收藏0
  • Java 8 并發教程:原子變量和 ConcurrentMa

    摘要:并發教程原子變量和原文譯者飛龍協議歡迎閱讀我的多線程編程系列教程的第三部分。如果你能夠在多線程中同時且安全地執行某個操作,而不需要關鍵字或上一章中的鎖,那么這個操作就是原子的。當多線程的更新比讀取更頻繁時,這個類通常比原子數值類性能更好。 Java 8 并發教程:原子變量和 ConcurrentMap 原文:Java 8 Concurrency Tutorial: Synchroni...

    bitkylin 評論0 收藏0
  • Java 8 并發教程:線程和執行器

    摘要:在這個示例中我們使用了一個單線程線程池的。在延遲消逝后,任務將會并發執行。這是并發系列教程的第一部分。第一部分線程和執行器第二部分同步和鎖第三部分原子操作和 Java 8 并發教程:線程和執行器 原文:Java 8 Concurrency Tutorial: Threads and Executors 譯者:BlankKelly 來源:Java8并發教程:Threads和Execut...

    jsdt 評論0 收藏0
  • 不可不說的Java“鎖”事

    摘要:本文旨在對鎖相關源碼本文中的源碼來自使用場景進行舉例,為讀者介紹主流鎖的知識點,以及不同的鎖的適用場景。中,關鍵字和的實現類都是悲觀鎖。自適應意味著自旋的時間次數不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。 前言 Java提供了種類豐富的鎖,每種鎖因其特性的不同,在適當的場景下能夠展現出非常高的效率。本文旨在對鎖相關源碼(本文中的源碼來自JDK 8)、使用場景...

    galaxy_robot 評論0 收藏0
  • 手撕面試官系列(七):面試必備之常問并發編程高級面試專題

    摘要:如何在線程池中提交線程內存模型相關問題什么是的內存模型,中各個線程是怎么彼此看到對方的變量的請談談有什么特點,為什么它能保證變量對所有線程的可見性既然能夠保證線程間的變量可見性,是不是就意味著基于變量的運算就是并發安全的請對比下對比的異同。 并發編程高級面試面試題 showImg(https://upload-images.jianshu.io/upload_images/133416...

    Charles 評論0 收藏0

發表評論

0條評論

最新活動
閱讀需要支付1元查看
<