加鎖是為了避免在並發環境下,同時訪問共享資源產生的風險問題。那么,在並發環境下,是否必須加鎖?答案是否定的。並非所有的並發都需要加鎖。適當地降低鎖的粒度,甚至采用無鎖化的設計,更能提升並發能力。
比如,JDK中的ConcurrentHashMap,巧妙地采用了桶粒度的鎖,避免了put和get中對整個map的鎖定,尤其在get中,只對一個HashEntry做鎖定操作,性能提升是顯而易見的。
又比如,在程序中可以合理考慮業務數據的隔離性,實現無鎖化的並發。例如,程序中預計會有兩個並發任務,每個任務可以對所需要處理的數據進行分組。任務1去處理尾數為0到4的業務數據,任務2處理尾數為5到9的業務數據。那么,這兩個並發任務所要處理的數據天然是隔離的,也就不需要加鎖。
阿里有一道筆試題如下
無鎖化編程有哪些常見方法?
-
針對計數器,可以使用原子加
-
只有一個生產者和一個消費者,那么就可以做到免鎖訪問環形緩沖區(Ring Buffer)
-
RCU(Read-Copy-Update),新舊副本切換機制,對於舊副本可以采用延遲釋放的做法
-
CAS(Compare-and-Swap),如無鎖棧,無鎖隊列等待
這四種都是,
private volatile Thread thread;
任務隊列則是這樣定義:
//SingleThreadEventExecutor類 this.taskQueue = this.newTaskQueue(this.maxPendingTasks);
//NioEventLoop類 protected Queue<Runnable> newTaskQueue(int maxPendingTasks) { return maxPendingTasks == 2147483647 ? PlatformDependent.newMpscQueue() : PlatformDependent.newMpscQueue(maxPendingTasks); }
上面任務隊列的實現就是調用inEventLoop()先通過thread來判斷當前線程是否是創建NioEventLoop時綁定的線程,如果是就直接執行讀寫操作,如果不是就說明是其他線程,把讀寫操作封裝成任務放在任務隊列中。inEventLoop源碼:
//SingleThreadEventExecutor private volatile Thread thread; public boolean inEventLoop(Thread thread){ return thread = this.thread }
NioEventLoop封裝的線程在SingleThreadEventExecutor內定義,並在創建的時候初始化。
這樣一次完整的工作流程就這樣完成,然而這樣設計CPU利用率其實並不高,並發程度不夠。但這種設計是線程安全的,Netty線程間不需要做同步控制,Netty可以通過調整NIO線程池的線程參數,可以同時啟動多個串行化的線程並行運行,這樣性能就提升了。