在工作中,我發(fā)現(xiàn)很多同學(xué)在設(shè)計之初都是直接按照單線程的思路來寫程序的。
而忽略了本應(yīng)該重視的并發(fā)問題。
關(guān)于這個問題,我們今天來聊聊 如何用面向?qū)ο笏枷雽懞貌l(fā)程序。
面向?qū)ο笏枷敫l(fā)編程有關(guān)系嘛?本來是沒有關(guān)系的,他們分屬兩個不同的領(lǐng)域,但是在Java 語言里,這兩個領(lǐng)域被無情的融合在一起了。**在 Java 語言里,面向?qū)ο笏枷肽軌蜃尣l(fā)編程變得更簡單。 **
那么如何才能用面向?qū)ο笏枷雽懞貌l(fā)程序呢?結(jié)合我自己的經(jīng)驗來看,我覺得可以從封裝共享變量,識別共享變量間的約束條件和指定并發(fā)訪問策略這三個方面下手。
一、封裝共享變量
并發(fā)程序,我們關(guān)注的一個核心問題,不過是解決多線程同時訪問共享變量的問題。編程領(lǐng)域?qū)τ诠蚕碜兞康脑L問,必須嚴(yán)格控制,好在有了面向?qū)ο笏枷?,對于變量的訪問路徑可以輕松把控。
面型對象里面有一個很重要的特性就是封裝,封裝的通俗解釋就是將屬性和細(xì)節(jié)封裝在對象內(nèi)部。外界對象只能通過目標(biāo)對象提供的公共方法來間接訪問這些內(nèi)部屬性,我們把共享變量作為對象的屬性,那對于共享變量的訪問路徑就是對象的公共方法。
利用面向?qū)ο笏枷雽懖l(fā)程序,其實就這么簡單,**將共享變量作為對象屬性封裝在內(nèi)部,對所有公共方法制定并發(fā)訪問策略。 **
就拿很多都會用到的計數(shù)器來說,下面的計數(shù)器程序共享變量只有一個,就是value ,我們把他作為Counter 類的屬性,并且將兩個公共方法 get() 和 addOne() 聲明為同步方法,這樣 Counter 類就成為一個線程安全的類了。
public class Counter {
private long value;
synchronized long get(){
return value;
}
synchronized long addOne(){
return ++value;
}
}
當(dāng)然很多實際工作中要復(fù)雜的多,對于那些不會發(fā)生變化的共享變量來說,建議你用fianl 關(guān)鍵字修飾。這樣既可以避免并發(fā)問題,也能很明了的表明你的設(shè)計意圖,讓后面接手你的兄弟知道,你已經(jīng)考慮這些共享變量的安全性問題了。
二、識別共享變量間的約束條件
識別共享變量的約束條件非常重要,因為這些約束條件,決定了并發(fā)訪問策略,例如:庫存管理里面有個合理庫存的概念,庫存量不能太高,也不能太低,他有一個上限和一個下限,我們可以通過下面代碼模擬以下:
在類 SafeWM 中,聲明了兩個成員變量 upper 和 lower,分別代表庫存上限和庫存下限,這兩個變量用了 AtomicLong 這個原子類,原子類是線程安全的,所以這兩個成員變量的 set 方法就不需要同步了。
public class SafeWM {
// 庫存上限
private final AtomicLong upper =
new AtomicLong(0);
// 庫存下限
private final AtomicLong lower =
new AtomicLong(0);
// 設(shè)置庫存上限
void setUpper(long v){
upper.set(v);
}
// 設(shè)置庫存下限
void setLower(long v){
lower.set(v);
}
// 省略其他業(yè)務(wù)代碼
}
在沒有識別出庫存下限要小于庫存上限這個約束條件之前,我們制定的并發(fā)訪問策略是利用原子類,但是這個策略,完全不能保證庫存下限要小于庫存上限這個約束條件。所以說,在設(shè)計階段,我們一定要識別出所有共享變量之間的約束條件,如果約束條件識別不足,很可能導(dǎo)致制定的并發(fā)訪問策略南轅北轍。共享變量之間的約束條件,反映在代碼里,基本上都會有 if 語句,所以,一定要特別注意競態(tài)條件。
三、制定并發(fā)訪問策略
制定并發(fā)訪問策略,是一個非常復(fù)雜的事情。應(yīng)該說整個專欄都是在嘗試搞定它。不過從方案上來看,無外乎就是以下“三件事”。
1、避免共享:避免共享的技術(shù)主要是利于線程本地存儲以及為每個任務(wù)分配獨立的線程。
2、不變模式:這個在 Java 領(lǐng)域應(yīng)用的很少,但在其他領(lǐng)域卻有著廣泛的應(yīng)用,例如 Actor 模式、CSP 模式以及函數(shù)式編程的基礎(chǔ)都是不變模式。
3、管程及其他同步工具:Java 領(lǐng)域萬能的解決方案是管程,但是對于很多特定場景,使用 Java 并發(fā)包提供的讀寫鎖、并發(fā)容器等同步工具會更好。
除了這些方案之外,還有一些宏觀的原則需要你了解。這些宏觀原則,有助于你寫出“健壯”的并發(fā)程序。這些原則主要有以下三條。
1、優(yōu)先使用成熟的工具類:Java SDK 并發(fā)包里提供了豐富的工具類,基本上能滿足你日常的需要,建議你熟悉它們,用好它們,而不是自己再“發(fā)明輪子”,畢竟并發(fā)工具類不是隨隨便便就能發(fā)明成功的。
2、迫不得已時才使用低級的同步原語:低級的同步原語主要指的是 synchronized、Lock、Semaphore 等,這些雖然感覺簡單,但實際上并沒那么簡單,一定要小心使用。
3、避免過早優(yōu)化:安全第一,并發(fā)程序首先要保證安全,出現(xiàn)性能瓶頸后再優(yōu)化。在設(shè)計期和開發(fā)期,很多人經(jīng)常會情不自禁地預(yù)估性能的瓶頸,并對此實施優(yōu)化,但殘酷的現(xiàn)實卻是:性能瓶頸不是你想預(yù)估就能預(yù)估的。