## 編程世界的那把鎖
原創?2017-05-08?老劉?[碼農翻身](http://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExNg==&mid=2665513653&idx=1&sn=e30c18c0c1780fb3ef0cdb858ee5201e&chksm=80d67af6b7a1f3e059466302c2c04c14d097c1a5de01cf986df84d4677299542f12b974dfde3&scene=21##)
1共享變量惹得禍
我們這里是個典型的弱肉強食的世界, 人口多而資源少,為了爭搶有限的資源,大家都在自己能運行的CPU時間片里拼了老命,經常為了一個變量的修改而打的頭破血流。
100納秒以前, 我有幸占據了CPU,從內存中讀取了一個變量x == 100, 我把它加了1, 休息了一會兒后我打算把它寫回內存, 但是驚奇的發現: 內存中的x 已經變成102了。
估計是哪個不著調的線程在我休息的時候也讀取并且修改了x, ? 有不少好心的線程在沖我喊:不要寫回了! 但是寫回內存是我的指令啊, 你不讓我執行,難道讓我退出? ?我只能毫不客氣的把101寫入內存, 把那個不符合我邏輯的值102給覆蓋掉, 這樣我才能執行下一條指令。
你看,單線程的邏輯正確并不表示多線程并發運行時的邏輯也能正確。?
這樣的事情發生的多了,程序總是無法正確運行, 引起了人類的強烈不滿,小道消息說他們在考慮kill掉我們, 換編程語言了。
但是換編程語言有什么用,只要有共享變量,多線程讀寫的時候就是會出現不一致啊。
除非你消除共享變量,讓每個線程只訪問一個函數內的局部變量, 這些局部變量我們每個線程都會有一份, 函數結束以后就會銷毀,所以線程之間就隔離了,就安全了。
消除共享變量談何容易, 人類使用的很多語言例如C++, Java,那些共享變量大多數一個對象的字段, 你想把字段去掉, 只留下函數, 那類也沒有存在的必要了, 就類似于函數式編程了, 一切都是函數。 ?有時候我挺羨慕函數式的世界, 那種無狀態應該是一種非常美妙的感覺吧。
2**爭搶吧,線程**
既然共享變量是無法消除的,那就想想別的辦法吧, 線程元老院的那幫家伙們哼哧了半天,終于公布了一個方案: 加鎖!
任何線程,只要你想操作一個共享變量,對不起, 先去申請一把鎖, 拿到這把鎖才能讀取x的值 , 修改x的值, ?把x寫回內存, 最后釋放鎖,讓別人去玩。
元老院設計的這把鎖非常簡單, 類似于一個boolean 變量, boolean lock = false. ? ? ? 誰能搶先把這個變量改成true, 就意味著獲取了這把鎖。
來吧,哥幾個,快來搶吧 !
我運行的時候, 就去檢查lock這個變量是否可以設置為true, 如果被別的家伙給搶到了(已經變成true了), 我就在這里無限循環,拼命的搶, 除非我的時間片到了,被迫讓出CPU, 但是我不會阻塞, 還是就緒狀態,等待下一次的調度, 進入CPU繼續搶。
看到某人把它變成false, 我眼疾手快迅速出手, 終于搶到了,趕緊把lock改成true, 這把鎖現在屬于我了, 趕快去干活,干完活要記住把lock 改成false, ?讓別的家伙們去搶。
我想正是由于這種無限循環的特點, 元老院把他命名為“自旋鎖”吧!
列位看官,可能你已經想到了, 假設有兩個線程,都讀到了lock == false, ?都把lock 改成true, 那這個鎖算誰的?
這個問題元老院的大佬們早就考慮到了, 他們和操作系統(我聽說還有硬件)都商量好了, 這個檢測lock是否為false, 以及設置lock 為true 的操作 其實被合并了, 叫做test_and_set(lock), ?操作系統鄭重承諾,這是一個不可分割的原子操作, 在這個test_and_set執行的時候,總線都被鎖住了, 別人不能訪問內存, 即使有多個CPU在執行也不會亂掉。
如果你感興趣,可以看看下面的實現, 否則直接無視跳過:

3改進
有了自旋鎖, 至少可以保證程序的正確運行了, 我們大家都玩的不亦樂乎。
有一天我遇到了一個遞歸函數, 我是挺喜歡遞歸的, 因為邏輯簡單, 只要遞歸的層次別太深, 別搞出棧溢出就好。 ?
這個遞歸函數中需要獲得自旋鎖,做點事情, 然后繼續調用自己, 類似于這樣:

我第一次調用doSomething, 獲取了自旋鎖, 然后第二次調用doSomething, 還要獲取自旋鎖, ?可是這個鎖已經在我第一次調用的時候持有了, 現在第二次調用只有無限的等待了!?
這下尷尬了, 我進退不得, 自己把自己搞成了死鎖!
看來這個自旋鎖雖然能實現互斥的訪問, 但是不能重新進入同一個函數(簡稱不可重入)啊!
我趕緊把這個問題向元老院做了匯報, 修改方案很快就下來了: 每次成功的申請鎖以后,要記錄下到底是誰申請的, 還要用一個計數器記錄重入的次數, 下一次持有鎖的家伙再次申請鎖只是給計數器加一而已。
釋放的時候也是一樣, 把計數器減一, 如果等于0了才真正的釋放鎖。
可重入性就這么解決了, 但是這么多線程都在那里拼命的搶也不是辦法, 空耗CPU也是巨大的浪費啊。
于是元老院又發布了新的鎖 ReentrantLock, 這個鎖可以重入,如果你搶不到, 不要無限循環了, 乖乖的到等待隊列里待著去, 等到鎖被別人釋放了再通知你去搶。
(在Java 中最初是synchronzied關鍵字,可以用在一個方法上或者一個代碼塊上, 后來又改進為更加靈活的ReentrantLock)
很快就有線程還抱怨說, 明明是我先發出獲得鎖的申請啊, 為什么隔壁老王卻先拿到了鎖? 這不公平啊,不行,以后得排隊, 先來先得。 ? 好吧, 只好加上一個是否公平的參數。
還有線程說, 我是個急性子,申請鎖的時候只想等待5秒鐘, 5秒之內得不到鎖我就放棄了, 能不能支持? ?那就再加上一個參數:等待時間。
4**發揚光大**
體會到鎖帶來的甜頭以后, 各種各樣樣的需求紛至沓來:?
1\. 有時候需要多個線程都獲得同一把鎖,去做一件事情,那怎么辦呢?
沒關系,信號量(Semaphore)出馬,創建信號量的時候得指定一個整數(例如10), 表明同一時刻最多有10個線程可以獲得鎖:?
Semaphore lock= new Semaphore(10);
當然每個線程都需要調用lock.aquire(), lock.release()去申請/釋放鎖。?
2\. ?一個線程要寫共享變量, 可是還有幾個線程要同時讀, 怎么辦? 你寫的時候可以鎖住, 但總不能讀的時候也只允許一個線程吧? ?
只好來一個讀寫鎖了ReadWriteLock, 為了保證可重入性, 元老院體貼的實現了ReentrantReadWriteLock。
?3\. 一個線程需要等待其他多個線程完工以后才能干活,怎么辦??
CountDownLatch前來救駕, 搞一個計數器,某個線程干完了就把計數器減去1, 如果計數器為0了,那個一直耐心等待的線程就可以開始了。
4\. 還有幾個線程必須互相等待, 就像100米賽跑那樣, 所有人都準備好了才能開閘放水, 不,是起跑, 就那就賞你一個CyclicBarrier吧。
你看到的只是冰山一角, 更多精彩文章,請移步《[碼農翻身文章精華](http://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExNg==&mid=2665513504&idx=1&sn=25dd6420e3056101dd3f6fdaedacaa2a&chksm=80d67a63b7a1f37572a5159ff6f53810467c15c8beec94770e8360c45f45036360d77755ee78&scene=21#wechat_redirect)》
有心得想和大家分享? 歡迎投稿 ! 我的聯系方式:微信:liuxinlehan ?QQ: 3340792577

優秀人才不缺工作機會,只缺適合自己的好機會。但是他們往往沒有精力從海量機會中找到最適合的那個。
100offer 會對平臺上的人才和企業進行嚴格篩選,讓「最好的人才」和「最好的公司」相遇。
掃描下方二維碼,注冊 100offer,談談你對下一份工作的期待。一周內,收到 5-10 個滿足你要求的好機會!

* * * * *
create date:2017-7-15 13:47:30
- 開始
- 公益
- 更好的使用看云
- 推薦書單
- 優秀資源整理
- 技術文章寫作規范
- SublimeText - 編碼利器
- PSR-0/PSR-4命名標準
- php的多進程實驗分析
- 高級PHP
- 進程
- 信號
- 事件
- IO模型
- 同步、異步
- socket
- Swoole
- PHP擴展
- Composer
- easyswoole
- php多線程
- 守護程序
- 文件鎖
- s-socket
- aphp
- 隊列&并發
- 隊列
- 講個故事
- 如何最大效率的問題
- 訪問式的web服務(一)
- 訪問式的web服務(二)
- 請求
- 瀏覽器訪問阻塞問題
- Swoole
- 你必須理解的計算機核心概念 - 碼農翻身
- CPU阿甘 - 碼農翻身
- 異步通知,那我要怎么通知你啊?
- 實時操作系統
- 深入實時 Linux
- Redis 實現隊列
- redis與隊列
- 定時-時鐘-阻塞
- 計算機的生命
- 多進程/多線程
- 進程通信
- 拜占庭將軍問題深入探討
- JAVA CAS原理深度分析
- 隊列的思考
- 走進并發的世界
- 鎖
- 事務筆記
- 并發問題帶來的后果
- 為什么說樂觀鎖是安全的
- 內存鎖與內存事務 - 劉小兵2014
- 加鎖還是不加鎖,這是一個問題 - 碼農翻身
- 編程世界的那把鎖 - 碼農翻身
- 如何保證萬無一失
- 傳統事務與柔性事務
- 大白話搞懂什么是同步/異步/阻塞/非阻塞
- redis實現鎖
- 淺談mysql事務
- PHP異常
- php錯誤
- 文件加載
- 路由與偽靜態
- URL模式之分析
- 字符串處理
- 正則表達式
- 數組合并與+
- 文件上傳
- 常用驗證與過濾
- 記錄
- 趣圖
- foreach需要注意的問題
- Discuz!筆記
- 程序設計思維
- 抽象與具體
- 配置
- 關于如何學習的思考
- 編程思維
- 談編程
- 如何安全的修改對象
- 臨時
- 臨時筆記
- 透過問題看本質
- 程序后門
- 邊界檢查
- session
- 安全
- 王垠
- 第三方數據接口
- 驗證碼問題
- 還是少不了虛擬機
- 程序員如何談戀愛
- 程序員為什么要一直改BUG,為什么不能一次性把代碼寫好?
- 碎碎念
- 算法
- 實用代碼
- 相對私密與絕對私密
- 學習目標
- 隨記
- 編程小知識
- foo
- 落盤
- URL編碼的思考
- 字符編碼
- Elasticsearch
- TCP-IP協議
- 碎碎念2
- Grafana
- EFK、ELK
- RPC
- 依賴注入
- 科目一
- 開發筆記
- 經緯度格式轉換
- php時區問題
- 解決本地開發時調用遠程AIP跨域問題
- 后期靜態綁定
- 談tp的跳轉提示頁面
- 無限分類問題
- 生成微縮圖
- MVC名詞
- MVC架構
- 也許模塊不是唯一的答案
- 哈希算法
- 開發后臺
- 軟件設計架構
- mysql表字段設計
- 上傳表如何設計
- 二開心得
- awesomes-tables
- 安全的代碼部署
- 微信開發筆記
- 賬戶授權相關
- 小程序獲取是否關注其公眾號
- 支付相關
- 提交訂單
- 微信支付筆記
- 支付接口筆記
- 支付中心開發
- 下單與支付
- 支付流程設計
- 訂單與支付設計
- 敏感操作驗證
- 排序設計
- 代碼的運行環境
- 搜索關鍵字的顯示處理
- 接口異步更新ip信息
- 圖片處理
- 項目搭建
- 閱讀文檔的新方式
- mysql_insert_id并發問題思考
- 行鎖注意事項
- 細節注意
- 如何處理用戶的輸入
- 不可見的字符
- 抽獎
- 時間處理
- 應用開發實戰
- python 學習記錄
- Scrapy 教程
- Playwright 教程
- stealth.min.js
- Selenium 教程
- requests 教程
- pyautogui 教程
- Flask 教程
- PyInstaller 教程
- 蜘蛛
- python 文檔相似度驗證
- thinkphp5.0數據庫與模型的研究
- workerman進程管理
- workerman網絡分析
- java學習記錄
- docker
- 筆記
- kubernetes
- Kubernetes
- PaddlePaddle
- composer
- oneinstack
- 人工智能 AI
- 京東
- pc_detailpage_wareBusiness
- doc
- 電商網站設計
- iwebshop
- 商品規格分析
- 商品屬性分析
- tpshop
- 商品規格分析
- 商品屬性分析
- 電商表設計
- 設計記錄
- 優惠券
- 生成唯一訂單號
- 購物車技術
- 分類與類型
- 微信登錄與綁定
- 京東到家庫存系統架構設計
- crmeb
- 命名規范
- Nginx https配置
- 關于人工智能
- 從人的思考方式到二叉樹
- 架構
- 今日有感
- 文章保存
- 安全背后: 瀏覽器是如何校驗證書的
- 避不開的分布式事務
- devops自動化運維、部署、測試的最后一公里 —— ApiFox 云時代的接口管理工具
- 找到自己今生要做的事
- 自動化生活
- 開源與漿果
- Apifox: API 接口自動化測試指南