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

Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(33)