這是java高併發系列第21篇文章。java
需求:咱們開發了一個網站,須要對訪問量進行統計,用戶每次發一次請求,訪問量+1,如何實現呢?web
下面咱們來模仿有100我的同時訪問,而且每一個人對我們的網站發起10次請求,最後總訪問次數應該是1000次。實現訪問以下。數據庫
代碼以下:tomcat
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++操做其實是由如下三步操做完成的:網絡
若是有A、B兩個線程同時執行count++,他們同時執行到上面步驟的第1步,獲得的count是同樣的,3步操做完成以後,count只會+1,致使count只加了一次,從而致使結果不許確。多線程
那麼咱們應該怎麼作的呢?併發
對count++操做的時候,咱們讓多個線程排隊處理,多個線程同時到達request()方法的時候,只能容許一個線程能夠進去操做,其餘的線程在外面候着,等裏面的處理完畢出來以後,外面等着的再進去一個,這樣操做count++就是排隊進行的,結果必定是正確的。
咱們前面學了synchronized、ReentrantLock能夠對資源加鎖,保證併發的正確性,多線程狀況下能夠保證被鎖的資源被串行訪問,那麼咱們用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秒多。
咱們在看一下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的操做,功能很是強大,咱們繼續向下看。
CAS,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這麼好用,那麼有沒有什麼問題呢?還真有
ABA問題
CAS須要在操做值的時候檢查下值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,可是實際上卻變化了。這就是CAS的ABA問題。 常見的解決思路是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A
就會變成1A-2B-3A
。 目前在JDK的atomic包裏提供了一個類AtomicStampedReference
來解決ABA問題。這個類的compareAndSet方法做用是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。
循環時間長開銷大
上面咱們說過若是CAS不成功,則會原地循環(自旋操做),若是長時間自旋會給CPU帶來很是大的執行開銷。併發量比較大的狀況下,CAS成功機率可能比較低,可能會重試不少次纔會成功。
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
包中,下篇文章咱們主要來介紹一下這些經常使用的類及各自的使用場景。
阿里p7一塊兒學併發,公衆號:路人甲java,天天獲取最新文章!