摘要:方法由兩個參數,表示期望的值,表示要給設置的新值。操作包含三個操作數內存位置預期原值和新值。如果處的值尚未同時更改,則操作成功。中就使用了這樣的操作。上面操作還有一點是將事務范圍縮小了,也提升了系統并發處理的性能。
這是java高并發系列第21篇文章。
本文主要內容從網站計數器實現中一步步引出CAS操作
介紹java中的CAS及CAS可能存在的問題
悲觀鎖和樂觀鎖的一些介紹及數據庫樂觀鎖的一個常見示例
使用java中的原子操作實現網站計數器功能
我們需要解決的問題需求:我們開發了一個網站,需要對訪問量進行統計,用戶每次發一次請求,訪問量+1,如何實現呢?
下面我們來模仿有100個人同時訪問,并且每個人對咱們的網站發起10次請求,最后總訪問次數應該是1000次。實現訪問如下。
方式1代碼如下:
package com.itsoku.chat20; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * 跟著阿里p7學并發,微信公眾號:javacode2018 */ public class Demo1 { //訪問次數 static int count = 0; //模擬訪問一次 public static void request() throws InterruptedException { //模擬耗時5毫秒 TimeUnit.MILLISECONDS.sleep(5); count++; } public static void main(String[] args) throws InterruptedException { long starTime = System.currentTimeMillis(); int threadSize = 100; CountDownLatch countDownLatch = new CountDownLatch(threadSize); for (int i = 0; i < threadSize; i++) { Thread thread = new Thread(() -> { try { for (int j = 0; j < 10; j++) { request(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { countDownLatch.countDown(); } }); thread.start(); } countDownLatch.await(); long endTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + ",耗時:" + (endTime - starTime) + ",count=" + count); } }
輸出:
main,耗時:138,count=975
代碼中的count用來記錄總訪問次數,request()方法表示訪問一次,內部休眠5毫秒模擬內部耗時,request方法內部對count++操作。程序最終耗時1秒多,執行還是挺快的,但是count和我們期望的結果不一致,我們期望的是1000,實際輸出的是973(每次運行結果可能都不一樣)。
分析一下問題出在哪呢?
代碼中采用的是多線程的方式來操作count,count++會有線程安全問題,count++操作實際上是由以下三步操作完成的:
獲取count的值,記做A:A=count
將A的值+1,得到B:B = A+1
讓B賦值給count:count = B
如果有A、B兩個線程同時執行count++,他們同時執行到上面步驟的第1步,得到的count是一樣的,3步操作完成之后,count只會+1,導致count只加了一次,從而導致結果不準確。
那么我們應該怎么做的呢?
對count++操作的時候,我們讓多個線程排隊處理,多個線程同時到達request()方法的時候,只能允許一個線程可以進去操作,其他的線程在外面候著,等里面的處理完畢出來之后,外面等著的再進去一個,這樣操作count++就是排隊進行的,結果一定是正確的。
我們前面學了synchronized、ReentrantLock可以對資源加鎖,保證并發的正確性,多線程情況下可以保證被鎖的資源被串行訪問,那么我們用synchronized來實現一下。
使用synchronized實現代碼如下:
package com.itsoku.chat20; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * 跟著阿里p7學并發,微信公眾號:javacode2018 */ public class Demo2 { //訪問次數 static int count = 0; //模擬訪問一次 public static synchronized void request() throws InterruptedException { //模擬耗時5毫秒 TimeUnit.MILLISECONDS.sleep(5); count++; } public static void main(String[] args) throws InterruptedException { long starTime = System.currentTimeMillis(); int threadSize = 100; CountDownLatch countDownLatch = new CountDownLatch(threadSize); for (int i = 0; i < threadSize; i++) { Thread thread = new Thread(() -> { try { for (int j = 0; j < 10; j++) { request(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { countDownLatch.countDown(); } }); thread.start(); } countDownLatch.await(); long endTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + ",耗時:" + (endTime - starTime) + ",count=" + count); } }
輸出:
main,耗時:5563,count=1000
程序中request方法使用synchronized關鍵字,保證了并發情況下,request方法同一時刻只允許一個線程訪問,request加鎖了相當于串行執行了,count的結果和我們預期的結果一致,只是耗時比較長,5秒多。
方式3我們在看一下count++操作,count++操作實際上是被拆分為3步驟執行:
1. 獲取count的值,記做A:A=count 2. 將A的值+1,得到B:B = A+1 3. 讓B賦值給count:count = B
方式2中我們通過加鎖的方式讓上面3步驟同時只能被一個線程操作,從而保證結果的正確性。
我們是否可以只在第3步加鎖,減少加鎖的范圍,對第3步做以下處理:
獲取鎖 第三步獲取一下count最新的值,記做LV 判斷LV是否等于A,如果相等,則將B的值賦給count,并返回true,否者返回false 釋放鎖
如果我們發現第3步返回的是false,我們就再次去獲取count,將count賦值給A,對A+1賦值給B,然后再將A、B的值帶入到上面的過程中執行,直到上面的結果返回true為止。
我們用代碼來實現,如下:
package com.itsoku.chat20; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * 跟著阿里p7學并發,微信公眾號:javacode2018 */ public class Demo3 { //訪問次數 volatile static int count = 0; //模擬訪問一次 public static void request() throws InterruptedException { //模擬耗時5毫秒 TimeUnit.MILLISECONDS.sleep(5); int expectCount; do { expectCount = getCount(); } while (!compareAndSwap(expectCount, expectCount + 1)); } /** * 獲取count當前的值 * * @return */ public static int getCount() { return count; } /** * @param expectCount 期望count的值 * @param newCount 需要給count賦的新值 * @return */ public static synchronized boolean compareAndSwap(int expectCount, int newCount) { //判斷count當前值是否和期望的expectCount一樣,如果一樣將newCount賦值給count if (getCount() == expectCount) { count = newCount; return true; } return false; } public static void main(String[] args) throws InterruptedException { long starTime = System.currentTimeMillis(); int threadSize = 100; CountDownLatch countDownLatch = new CountDownLatch(threadSize); for (int i = 0; i < threadSize; i++) { Thread thread = new Thread(() -> { try { for (int j = 0; j < 10; j++) { request(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { countDownLatch.countDown(); } }); thread.start(); } countDownLatch.await(); long endTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + ",耗時:" + (endTime - starTime) + ",count=" + count); } }
輸出:
main,耗時:116,count=1000
代碼中用了volatile關鍵字修飾了count,可以保證count在多線程情況下的可見性。關于volatile關鍵字的使用,也是非常非常重要的,前面有講過,不太了解的朋友可以去看一下:volatile與Java內存模型
咱們再看一下代碼,compareAndSwap方法,我們給起個簡稱吧叫CAS,這個方法有什么作用呢?這個方法使用synchronized修飾了,能保證此方法是線程安全的,多線程情況下此方法是串行執行的。方法由兩個參數,expectCount:表示期望的值,newCount:表示要給count設置的新值。方法內部通過getCount()獲取count當前的值,然后與期望的值expectCount比較,如果期望的值和count當前的值一致,則將新值newCount賦值給count。
再看一下request()方法,方法中有個do-while循環,循環內部獲取count當前值賦值給了expectCount,循環結束的條件是compareAndSwap返回true,也就是說如果compareAndSwap如果不成功,循環再次獲取count的最新值,然后+1,再次調用compareAndSwap方法,直到compareAndSwap返回成功為止。
代碼中相當于將count++拆分開了,只對最后一步加鎖了,減少了鎖的范圍,此代碼的性能是不是比方式2快不少,還能保證結果的正確性。大家是不是感覺這個compareAndSwap方法挺好的,這東西確實很好,java中已經給我們提供了CAS的操作,功能非常強大,我們繼續向下看。
CASCAS,compare and swap的縮寫,中文翻譯成比較并交換。
CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。 如果內存位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值 。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該 位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前 值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”
通常將 CAS 用于同步的方式是從地址 V 讀取值 A,執行多步計算來獲得新 值 B,然后使用 CAS 將 V 的值從 A 改為 B。如果 V 處的值尚未同時更改,則 CAS 操作成功。
系統底層進行CAS操作的時候,會判斷當前系統是否為多核系統,如果是就給總線加鎖,只有一個線程會對總線加鎖成功,加鎖成功之后會執行cas操作,也就是說CAS的原子性實際上是CPU實現的, 其實在這一點上還是有排他鎖的.,只是比起用synchronized, 這里的排他時間要短的多, 所以在多線程情況下性能會比較好。
java中提供了對CAS操作的支持,具體在sun.misc.Unsafe類中,聲明如下:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
上面三個方法都是類似的,主要對4個參數做一下說明。
var1:表示要操作的對象var2:表示要操作對象中屬性地址的偏移量
var4:表示需要修改數據的期望的值
var5:表示需要修改為的新值
JUC包中大部分功能都是依靠CAS操作完成的,所以這塊也是非常重要的,有關Unsafe類,下篇文章會具體講解。
synchronized、ReentrantLock這種獨占鎖屬于悲觀鎖,它是在假設需要操作的代碼一定會發生沖突的,執行代碼的時候先對代碼加鎖,讓其他線程在外面等候排隊獲取鎖。悲觀鎖如果鎖的時間比較長,會導致其他線程一直處于等待狀態,像我們部署的web應用,一般部署在tomcat中,內部通過線程池來處理用戶的請求,如果很多請求都處于等待獲取鎖的狀態,可能會耗盡tomcat線程池,從而導致系統無法處理后面的請求,導致服務器處于不可用狀態。
除此之外,還有樂觀鎖,樂觀鎖的含義就是假設系統沒有發生并發沖突,先按無鎖方式執行業務,到最后了檢查執行業務期間是否有并發導致數據被修改了,如果有并發導致數據被修改了 ,就快速返回失敗,這樣的操作使系統并發性能更高一些。cas中就使用了這樣的操作。
關于樂觀鎖這塊,想必大家在數據庫中也有用到過,給大家舉個例子,可能以后會用到。
如果你們的網站中有調用支付寶充值接口的,支付寶那邊充值成功了會回調商戶系統,商戶系統接收到請求之后怎么處理呢?假設用戶通過支付寶在商戶系統中充值100,支付寶那邊會從用戶賬戶中扣除100,商戶系統接收到支付寶請求之后應該在商戶系統中給用戶賬戶增加100,并且把訂單狀態置為成功。
處理過程如下:
開啟事務 獲取訂單信息 if(訂單狀態==待處理){ 給用戶賬戶增加100 將訂單狀態更新為成功 } 返回訂單處理成功 提交事務
由于網絡等各種問題,可能支付寶回調商戶系統的時候,回調超時了,支付寶又發起了一筆回調請求,剛好這2筆請求同時到達上面代碼,最終結果是給用戶賬戶增加了200,這樣事情就搞大了,公司蒙受損失,嚴重點可能讓公司就此倒閉了。
那我們可以用樂觀鎖來實現,給訂單表加個版本號version,要求每次更新訂單數據,將版本號+1,那么上面的過程可以改為:
獲取訂單信息,將version的值賦值給V_A if(訂單狀態==待處理){ 開啟事務 給用戶賬戶增加100 update影響行數 = update 訂單表 set version = version + 1 where id = 訂單號 and version = V_A; if(update影響行數==1){ 提交事務 }else{ 回滾事務 } } 返回訂單處理成功
上面的update語句相當于我們說的CAS操作,執行這個update語句的時候,多線程情況下,數據庫會對當前訂單記錄加鎖,保證只有一條執行成功,執行成功的,影響行數為1,執行失敗的影響行數為0,根據影響行數來決定提交還是回滾事務。上面操作還有一點是將事務范圍縮小了,也提升了系統并發處理的性能。這個知識點希望你們能get到。
CAS 的問題cas這么好用,那么有沒有什么問題呢?還真有
ABA問題
CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。這就是CAS的ABA問題。 常見的解決思路是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A。 目前在JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等于預期引用,并且當前標志是否等于預期標志,如果全部相等,則以原子方式將該引用和該標志的值設置為給定的更新值。
循環時間長開銷大
上面我們說過如果CAS不成功,則會原地循環(自旋操作),如果長時間自旋會給CPU帶來非常大的執行開銷。并發量比較大的情況下,CAS成功概率可能比較低,可能會重試很多次才會成功。
使用JUC中的類實現計數器juc框架中提供了一些原子操作,底層是通過Unsafe類中的cas操作實現的。通過原子操作可以保證數據在并發情況下的正確性。
此處我們使用java.util.concurrent.atomic.AtomicInteger類來實現計數器功能,AtomicInteger內部是采用cas操作來保證對int類型數據增減操作在多線程情況下的正確性。
計數器代碼如下:
package com.itsoku.chat20; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * 跟著阿里p7學并發,微信公眾號:javacode2018 */ public class Demo4 { //訪問次數 static AtomicInteger count = new AtomicInteger(); //模擬訪問一次 public static void request() throws InterruptedException { //模擬耗時5毫秒 TimeUnit.MILLISECONDS.sleep(5); //對count原子+1 count.incrementAndGet(); } public static void main(String[] args) throws InterruptedException { long starTime = System.currentTimeMillis(); int threadSize = 100; CountDownLatch countDownLatch = new CountDownLatch(threadSize); for (int i = 0; i < threadSize; i++) { Thread thread = new Thread(() -> { try { for (int j = 0; j < 10; j++) { request(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { countDownLatch.countDown(); } }); thread.start(); } countDownLatch.await(); long endTime = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + ",耗時:" + (endTime - starTime) + ",count=" + count); } }
輸出:
main,耗時:119,count=1000
耗時很短,并且結果和期望的一致。
關于原子類操作,都位于java.util.concurrent.atomic包中,下篇文章我們主要來介紹一下這些常用的類及各自的使用場景。
java高并發系列java高并發系列 - 第1天:必須知道的幾個概念
java高并發系列 - 第2天:并發級別
java高并發系列 - 第3天:有關并行的兩個重要定律
java高并發系列 - 第4天:JMM相關的一些概念
java高并發系列 - 第5天:深入理解進程和線程
java高并發系列 - 第6天:線程的基本操作
java高并發系列 - 第7天:volatile與Java內存模型
java高并發系列 - 第8天:線程組
java高并發系列 - 第9天:用戶線程和守護線程
java高并發系列 - 第10天:線程安全和synchronized關鍵字
java高并發系列 - 第11天:線程中斷的幾種方式
java高并發系列 - 第12天JUC:ReentrantLock重入鎖
java高并發系列 - 第13天:JUC中的Condition對象
java高并發系列 - 第14天:JUC中的LockSupport工具類,必備技能
java高并發系列 - 第15天:JUC中的Semaphore(信號量)
java高并發系列 - 第16天:JUC中等待多線程完成的工具類CountDownLatch,必備技能
java高并發系列 - 第17天:JUC中的循環柵欄CyclicBarrier的6種使用場景
java高并發系列 - 第18天:JAVA線程池,這一篇就夠了
java高并發系列 - 第19天:JUC中的Executor框架詳解1
java高并發系列 - 第20天:JUC中的Executor框架詳解2
阿里p7一起學并發,公眾號:路人甲java,每天獲取最新文章!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/75815.html
摘要:我們需要先了解這些概念。在中,其表現在對于共享變量的某些操作,是不可分的,必須連續的完成。有序性有序性指的是程序按照代碼的先后順序執行。 JMM(java內存模型),由于并發程序要比串行程序復雜很多,其中一個重要原因是并發程序中數據訪問一致性和安全性將會受到嚴重挑戰。如何保證一個線程可以看到正確的數據呢?這個問題看起來很白癡。對于串行程序來說,根本就是小菜一碟,如果你讀取一個變量,這個...
摘要:有時候,由于初期考慮不周,或者后期的需求變化,一些普通變量可能也會有線程安全的需求。它可以讓你在不改動或者極少改動原有代碼的基礎上,讓普通的變量也享受操作帶來的線程安全性,這樣你可以修改極少的代碼,來獲得線程安全的保證。 有時候,由于初期考慮不周,或者后期的需求變化,一些普通變量可能也會有線程安全的需求。如果改動不大,我們可以簡單地修改程序中每一個使用或者讀取這個變量的地方。但顯然,這...
摘要:我們繼續看代碼的意思是這個是一段內嵌匯編代碼。也就是在語言中使用匯編代碼。就是匯編版的比較并交換。就是保證在多線程情況下,不阻塞線程的填充和消費。微觀上看匯編的是實現操作系統級別的原子操作的基石。 原文地址:https://www.xilidou.com/2018/02/01/java-cas/ CAS 是現代操作系統,解決并發問題的一個重要手段,最近在看 eureka 的源碼的時候。...
摘要:在本例中,講述的無鎖來自于并發包我們將這個無鎖的稱為。在這里,我們使用二維數組來表示的內部存儲,如下變量存放所有的內部元素。為什么使用二維數組去實現一個一維的呢這是為了將來進行動態擴展時可以更加方便。 我們已經比較完整得介紹了有關無鎖的概念和使用方法。相對于有鎖的方法,使用無鎖的方式編程更加考驗一個程序員的耐心和智力。但是,無鎖帶來的好處也是顯而易見的,第一,在高并發的情況下,它比有鎖...
摘要:有三種狀態運行關閉終止。類類,提供了一系列工廠方法用于創建線程池,返回的線程池都實現了接口。線程池的大小一旦達到最大值就會保持不變,在提交新任務,任務將會進入等待隊列中等待。此線程池支持定時以及周期性執行任務的需求。 這是java高并發系列第19篇文章。 本文主要內容 介紹Executor框架相關內容 介紹Executor 介紹ExecutorService 介紹線程池ThreadP...
閱讀 2446·2021-10-13 09:40
閱讀 3334·2019-08-30 13:46
閱讀 1120·2019-08-29 14:05
閱讀 2953·2019-08-29 12:48
閱讀 3654·2019-08-26 13:28
閱讀 2142·2019-08-26 11:34
閱讀 2277·2019-08-23 18:11
閱讀 1156·2019-08-23 12:26