一文詳解Java中的原子操作
第1章:什么是原子操作
大家好,我是小黑,面試中一個經常被提起的話題就是“原子操作”。那么,到底什么是原子操作呢?在編程里,當咱們談論“原子操作”時,其實是指那些在執(zhí)行過程中不會被線程調度機制打斷的操作。這種操作要么完全執(zhí)行,要么完全不執(zhí)行,沒有中間狀態(tài)。這就像是化學里的原子,不可分割,要么存在,要么不存在。
舉個簡單的例子,想象一下,小黑在網上銀行轉賬給朋友。這個操作要么完整完成——錢從小黑的賬戶轉到朋友賬戶,要么根本就不發(fā)生——錢還在小黑的賬戶里。如果轉賬過程中出現問題,系統(tǒng)不會說“轉了一半的錢”,要么就是全轉了,要么就是一分沒轉。這就是原子操作的一個生活實例。
在Java中,原子操作尤為重要,尤其是在多線程環(huán)境中。想象一下,如果小黑在操作一個共享變量時,這個操作被其他線程打斷,那會發(fā)生什么?可能會導致數據不一致,或者更糟糕的情況。因此,保證操作的原子性在并發(fā)編程中是非常重要的。
第2章:Java中原子操作的基礎
要理解Java中的原子操作,首先得了解一下Java內存模型(JMM)。簡單來說,JMM是一種抽象的概念,它描述了Java在內存中如何存儲共享變量以及這些變量如何在多線程間交互。JMM定義了線程和主內存之間的關系,以及如何通過同步來保證共享變量的可見性和順序性。
在多線程環(huán)境中,每個線程都有自己的工作內存,用于存儲使用中的共享變量的副本。當線程對這些變量進行讀寫操作時,它們實際上是在操作這些副本。只有通過同步操作,這些變量的值才會真正地在主內存和工作內存間傳遞。
那么,原子操作和JMM有什么關系呢?原子操作保證了在單個操作中,變量的讀取、修改和寫回都是不可分割的。這就確保了即使在多線程環(huán)境中,這些操作也能保持一致性和正確性。
讓咱們來看一個簡單的Java代碼示例,展示非原子操作的問題:
public class Counter { private int count = 0; public void increment() { count++; // 非原子操作 } public int getCount() { return count; } }
在這個例子中,count++
看似是一個簡單的操作,但實際上它包含了三個步驟:讀取count
的值,增加1,然后寫回新的值。在多線程環(huán)境中,如果兩個線程同時執(zhí)行increment()
方法,它們可能讀到同一個count
值,結果就是count
只增加了1,而不是2。這就是非原子操作可能帶來的問題。
第3章:Java原子類概覽
原子類的基本概念
原子類,顧名思義,就是用來進行原子操作的類。在Java中,這些類提供了一種線程安全的方式來操作單個變量,無需使用synchronized
關鍵字。原子類的操作是基于CAS(Compare-And-Swap,比較并交換)機制實現的,這是一種輕量級的同步策略,比傳統(tǒng)的鎖機制更高效。
常見的原子類
讓咱們來看幾個常用的原子類:
AtomicInteger
:提供了一個可以原子性更新的int
值。AtomicLong
:和AtomicInteger
類似,但它是針對long
類型的。AtomicBoolean
:提供了一個可以原子性更新的boolean
值。AtomicReference
:提供了一個可以原子性更新的對象引用。
AtomicInteger的使用示例
來看個例子,如何使用AtomicInteger
:
import java.util.concurrent.atomic.AtomicInteger; public class AtomicCounter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); // 原子性地增加count的值 } public int getCount() { return count.get(); } } class Main { public static void main(String[] args) { AtomicCounter counter = new AtomicCounter(); // 模擬多線程環(huán)境 for (int i = 0; i < 1000; i++) { new Thread(() -> { counter.increment(); }).start(); } // 等待所有線程完成 try { Thread.sleep(2000); // 等待足夠長的時間以確保所有線程都執(zhí)行完畢 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("最終計數: " + counter.getCount()); // 正確輸出1000 } }
在這個示例中,AtomicCounter
類使用了AtomicInteger
來保證count
的增加操作是原子的。這就解決了之前提到的非原子操作可能導致的問題。無論多少線程同時調用increment()
方法,每次調用都會安全地將count
增加1。
第4章:深入探討原子類的實現原理
CAS機制簡介
CAS機制包含三個主要的操作數:內存位置(V)、預期原值(A)和新值(B)。操作的邏輯是:“我認為V應該是A,如果是,就把V更新成B,否則,不做任何操作。”這個過程是原子的,意味著在這個操作執(zhí)行期間,沒有其他線程可以改變這個內存位置的值。
CAS的優(yōu)點和挑戰(zhàn)
CAS的主要優(yōu)點是它提供了一種無鎖的方式來實現并發(fā)控制。相比于傳統(tǒng)的鎖機制,CAS通常能提供更好的性能,并減少了死鎖的風險。
但CAS也有它的挑戰(zhàn)。其中一個主要問題是“ABA問題”。如果一個變量原來是A,變成了B,然后又變回A,使用CAS的線程可能會錯誤地認為這個變量沒有被其他線程修改過。為了解決這個問題,Java提供了AtomicStampedReference
類,它通過維護一個“時間戳”來記錄變量的修改次數。
CAS在AtomicInteger中的應用
來看一個具體的例子,展示AtomicInteger
是如何利用CAS機制的:
public class CASExample { private AtomicInteger count = new AtomicInteger(0); public void increment() { int currentValue; int newValue; do { currentValue = count.get(); // 獲取當前值 newValue = currentValue + 1; // 計算新值 } while (!count.compareAndSet(currentValue, newValue)); // CAS操作 // 如果當前值不等于預期值,則循環(huán)嘗試,直到成功 } public int getCount() { return count.get(); } } class Main { public static void main(String[] args) { CASExample example = new CASExample(); // 模擬多線程環(huán)境 for (int i = 0; i < 1000; i++) { new Thread(() -> { example.increment(); }).start(); } // 等待所有線程完成 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("最終計數: " + example.getCount()); } }
在這個例子中,increment()
方法用一個do-while
循環(huán)來保證增加操作的原子性。如果在執(zhí)行CAS操作時,當前值不是預期值,意味著其他線程已經修改了這個值,那么循環(huán)會繼續(xù),直到成功更新。
通過這種方式,AtomicInteger
確保了即使在高并發(fā)的環(huán)境下,增加操作也是原子的,保證了數據的一致性和線程安全。
第5章:原子操作與并發(fā)編程
原子操作在并發(fā)編程中的應用
在并發(fā)編程中,原子操作確保了當多個線程嘗試同時更新同一個變量時,這個變量的值不會丟失或者損壞。這對于維護數據的一致性和程序的穩(wěn)定性至關重要。
案例分析
讓我們通過一個具體的案例來看看原子操作是如何工作的。假設咱們需要編寫一個程序,來統(tǒng)計一個網站的訪問量。在高并訪問量的情況下,可能有成百上千的用戶同時訪問這個網站,這時就需要一個能夠安全地處理多線程并發(fā)訪問的計數器。
使用非原子操作的計數器可能會導致一些訪問量丟失,因為當多個線程同時讀取和更新計數器的值時,一些更新可能會被覆蓋。而使用原子操作的計數器可以確保每次訪問都被準確地記錄。
import java.util.concurrent.atomic.AtomicInteger; public class WebsiteVisitsCounter { private AtomicInteger visitsCount = new AtomicInteger(); public void visit() { visitsCount.incrementAndGet(); // 原子操作 } public int getTotalVisits() { return visitsCount.get(); } } class Main { public static void main(String[] args) { WebsiteVisitsCounter counter = new WebsiteVisitsCounter(); // 模擬1000個用戶同時訪問網站 for (int i = 0; i < 1000; i++) { new Thread(counter::visit).start(); } // 等待線程結束 try { Thread.sleep(2000); // 假設足夠的時間讓所有線程執(zhí)行完畢 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("總訪問量: " + counter.getTotalVisits()); // 應該輸出1000 } }
在這個例子中,WebsiteVisitsCounter
使用了AtomicInteger
來確保訪問次數的計數是準確的,即使在高并發(fā)的情況下。每次調用visit
方法都會安全地增加visitsCount
,無論多少線程同時訪問它。
第6章:原子類的性能考量
原子類與傳統(tǒng)同步機制的性能比較
傳統(tǒng)的同步機制,比如使用synchronized
關鍵字,通過鎖來控制對共享資源的訪問。這種方法簡單直觀,但在高并發(fā)環(huán)境下可能導致性能瓶頸。原因是當一個線程持有鎖時,其他所有需要這個鎖的線程都必須等待,這就可能導致大量線程處于等待狀態(tài),從而降低了應用程序的整體性能。
相比之下,原子類利用CAS(Compare-And-Swap)機制來實現同步。這種機制不需要阻塞線程,因此在處理高并發(fā)數據時通常能提供更好的性能。但CAS也不是完美無缺的,特別是在高沖突環(huán)境下,頻繁的CAS操作可能會導致性能下降,這種情況被稱為“自旋”。
性能分析實例
讓我們通過一個簡單的實驗來比較使用synchronized
和原子類的性能差異。假設有一個簡單的計數器,咱們分別用synchronized
方法和AtomicInteger
來實現,然后比較它們在多線程環(huán)境下的性能。
public class SynchronizedCounter { private int count = 0; public synchronized void increment() { count++; } public int getCount() { return count; } } public class AtomicCounter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } public int getCount() { return count.get(); } }
在這個例子中,SynchronizedCounter
使用synchronized
關鍵字來保證計數器的線程安全,而AtomicCounter
則使用AtomicInteger
。如果咱們在一個高并發(fā)的環(huán)境下測試這兩個計數器,通常會發(fā)現AtomicCounter
的性能要優(yōu)于SynchronizedCounter
。原因在于synchronized
會引起線程的阻塞和喚醒,而AtomicInteger
利用CAS實現非阻塞的同步。
第7章:原子類的高級用法
AtomicReference的使用
AtomicReference
類提供了一種方式來原子性地更新對象引用。這意味著咱們可以安全地在多線程環(huán)境中更改對象引用,而無需擔心線程安全問題。
來看一個AtomicReference
的例子:
import java.util.concurrent.atomic.AtomicReference; public class AtomicReferenceExample { private AtomicReference<String> currentSetting = new AtomicReference<>("默認設置"); public void updateSetting(String newSetting) { currentSetting.set(newSetting); } public String getSetting() { return currentSetting.get(); } } class Main { public static void main(String[] args) { AtomicReferenceExample example = new AtomicReferenceExample(); // 模擬多線程環(huán)境更新設置 new Thread(() -> example.updateSetting("自定義設置1")).start(); new Thread(() -> example.updateSetting("自定義設置2")).start(); System.out.println("當前設置: " + example.getSetting()); } }
在這個例子中,AtomicReferenceExample
類使用AtomicReference
來保持currentSetting
的線程安全。無論多少線程嘗試更新這個設置,AtomicReference
都能確保操作的原子性。
AtomicStampedReference的使用
AtomicStampedReference
類是對AtomicReference
的一個擴展,它解決了所謂的“ABA問題”。除了對象引用之外,AtomicStampedReference
還維護了一個“時間戳”,這個時間戳在每次對象更新時都會改變。
來看一個AtomicStampedReference
的例子:
import java.util.concurrent.atomic.AtomicStampedReference; public class AtomicStampedReferenceExample { private AtomicStampedReference<String> currentSetting = new AtomicStampedReference<>("默認設置", 0); public void updateSetting(String newSetting) { int stamp = currentSetting.getStamp(); currentSetting.compareAndSet(currentSetting.getReference(), newSetting, stamp, stamp + 1); } public String getSetting() { return currentSetting.getReference(); } } class Main { public static void main(String[] args) { AtomicStampedReferenceExample example = new AtomicStampedReferenceExample(); // 模擬多線程環(huán)境更新設置 new Thread(() -> example.updateSetting("自定義設置1")).start(); new Thread(() -> example.updateSetting("自定義設置2")).start(); System.out.println("當前設置: " + example.getSetting()); } }
在這個例子中,每次更新currentSetting
時,時間戳stamp
都會增加。這就意味著即使一個值從A變成B再變回A,時間戳也會反映出這個變化,從而避免了ABA問題。
第8章:總結
原子操作的重要性
在多線程編程中,原子操作是確保數據一致性和線程安全的關鍵。通過原子類,如AtomicInteger
、AtomicBoolean
、AtomicReference
等,Java提供了一種有效的方式來進行線程安全的操作,無需擔心數據損壞或者線程沖突的問題。這對于構建高效且健壯的并發(fā)應用程序至關重要。
原子類的高效性
我們還討論了原子類相比傳統(tǒng)鎖機制(如synchronized
)在性能上的優(yōu)勢。特別是在高并發(fā)環(huán)境下,原子類能夠提供更好的性能,減少資源的等待時間,從而提高程序的整體效率。
面臨的挑戰(zhàn)和應對策略
盡管原子操作提供了許多優(yōu)勢,但它們也面臨著一些挑戰(zhàn),比如在高沖突環(huán)境下的性能問題。為了應對這些挑戰(zhàn),Java持續(xù)在發(fā)展中,引入了更多高級原子類,如AtomicStampedReference
,來解決這些問題。
相關文章
idea輸入sout無法自動補全System.out.println()的問題
這篇文章主要介紹了idea輸入sout無法自動補全System.out.println()的問題,本文給大家分享解決方案,供大家參考,需要的朋友可以參考下2020-07-07淺析springcloud 整合 zipkin-server 內存日志監(jiān)控
Zipkin是一款開源的分布式實時數據追蹤系統(tǒng)(Distributed Tracking System),其主要功能是聚集來自各個異構系統(tǒng)的實時監(jiān)控數據。這篇文章主要介紹了springcloud 整合 zipkin-server 內存日志監(jiān)控,需要的朋友可以參考下2019-11-11