又是三星期的生活。感覺自從有了這個分享之後,會無形多了一份動力,逼著自己不能落後,必須要去不停的學習,這其實是我想要的,各位少年團中的成員也都是有共鳴的,在此很感動,省去一萬字。。。。。這一次會總結對象的安全發佈、不變性,這幾點,在我們工程實踐中,同樣也是非常具有參考與思考價值的基礎知識點。看書枯燥 ...
又是三星期的生活。感覺自從有了這個分享之後,會無形多了一份動力,逼著自己不能落後,必須要去不停的學習,這其實是我想要的,各位少年團中的成員也都是有共鳴的,在此很感動,省去一萬字。。。。。這一次會總結對象的安全發佈、不變性,這幾點,在我們工程實踐中,同樣也是非常具有參考與思考價值的基礎知識點。看書枯燥,理解生澀,可是當你看過,理解一點,再平時業務代碼中就會比別人多思考一分,就會比別人在更“惡劣”的網路環境中,更穩定一分。這幾天想起《三傻》中,那句很經典的話:追求卓越,成功將會悄悄的靠近你。
一、發佈與溢出
“發佈(Publish)”一個對象的意思是指,使對象能夠在當前作用於之外的代碼中使用。這個“之外”,尤為關鍵,各種出問題的地方,都是因為這個“之外”所引起的。例如,如果在對象構造完成之前就發佈該對象,就會破壞線程安全性。當某個不應該發佈的對象被髮布時,這種情況就被稱為“溢出”。下麵使用簡單的例子進行說明:
1. 日常非常不註意的行為
class Status {
private String[] states = new String[]{"AA","BB","CC"};
public String[] getStates(){
return states;
}
}
思考:很多人會不服的來爭吵:這特麽哪裡有問題,跑了這麼久的線上了,一直沒出問題啊!好,那麼問題了來:是不是線上一直沒問題的代碼,就是好代碼?就是正確的代碼?
類似的代碼還有:
class Cache {
private static HashMap<String,Object> cache = new HashMap<>();
public static Object getCacheValue(String key){
return return cache.get(key);
}
public static HashMap<String,Object> getCache(String key){
return cache;
}
public static void addCache(String key, Object object){
cache.push(key, object);
}
}
P.S.:以上代碼是我去年年底,再項目工程中看到的代碼,而且線上上運行著,千真萬確!
2. 分析問題所在
你問我:這錯在哪?如果我要回答,我會說:沒錯,你都沒錯。個人原因,我不喜歡程式員當面懟,因為我知道,大家都不容易,並且還知道:真的有問題那天,你知道痛了,你會主動改的,根本不用我說啥。當然,更嚴重的是,代碼中(恩,線上代碼),有人將states命名成了s,cache命名成了c,這我也說不了啥,什麼叫做“追求卓越”,可能每人心中都會有自己的詮釋吧。如果是下麵代碼出現在一個神不知鬼不覺的地方,請看:
class Controller {
public void cache(){//1
Status status = new Status();
String[] allStatus = status.getStates();
Cache.addCache("ALL_STATUS",allStatus);
}
public void modify(){//2
String[] allStatus = (String[])Cache.getCacheValue("ALL_STATUS");
allStatus[0] = null;// 也許變成了其他值,null是一種比較極端的情況
}
public String getFirstUpcaseStatus(){//3
String[] allStatus = (String[])Cache.getCacheValue("ALL_STATUS");
return allStatus[0].toUpperCase(); // oh no! NPE!
}
public void remove(){//4
Cache.getCache().remove("ALL_STATUS");
}
}
- 1、2、3、4四個方法我們並不知道是什麼時候觸發的
- 就是說時間順序上,有可能是4號方法首先被觸發,那1、2、3都將有問題
- 即使4不被觸發,先1、2,後3,也是出問題的
- 也許我們代碼寫的很複雜,例如在2號程式中調用了非常多的service,用了非常多的設計模式,最終我們將修改數組中的值
- 也許我們知道問題所在不去修改數組中的狀態值,可是你能保證你能維護這個代碼一輩子嗎?
- 以後交給兩個人維護,兩個人由於沒啥子追求,別人代碼不看,一個人在一邊修改了數組,而另一個人在另一邊使用了數組中的狀態值,後果不堪設想
3. 更加隱蔽式的危險發佈
下麵這種,新學到的一種危險性行為發佈:
public class ThisEscape{
public ThisEscape(EventSource source){
source.registerListener(e->doSomething(e));
}
}
心得:請大家儘量使用Java8語法,整潔、大方、可擼(這是什麼鬼!)
思考:註意doSomething方法,會有什麼問題呢?
4. 構造器與構造者
- 作為構造者不要在構造器裡面添加過多的邏輯,出錯之後,這個鍋你背不起!
- 即使在一個構造器的最後一行,這個對象也是沒有沒初始化完成的!
- this指針被髮布出去,後果不堪設想,對象沒初始化完成,而使用this指針。
- 上面代碼,可以在doSomething方法內部使用
ThisEscape.this
來訪問父類 - 如果父類沒有初始化完,而訪問父類,那將報錯,這就是問題所在
5.針對這種隱蔽式的情況,我們怎麼做
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = e->doSomething(e);
}
public static SafeListener newInstance(EventSource source){
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return source;
}
}
6. 再舉例一些不安全發佈的例子
class Holder{
// 曝露類屬性,大忌~
public Holder holder;
public void initialize(){
holder = new Holder();
}
}
//由於未被正確發佈,因此這個類可能出現故障
public class Holder{
private int n;
public Holder(int n){
this.n = n;
}
public void assertSanity(){
if(n != n){
throw new Exception("initial erroe");
}
}
}
說明:拋出異常這個類是很玄乎的,因為線程可見性的原因,線程初次讀取n的時候是老的值,可是這之後n值被其他線程更新,這個線程再次讀取的時候,讀取到一個失效的值,這就是拋出異常的原因。可以見得普普通通的自身與自身的比較,在多線程的環境下,都是很有問題的!!
6-線程封閉
- 常見的封閉模式:棧封閉。就是在局部方法中使用一個變數,而不把他暴露出去。另外我自己的理解,每次方法返回一個新對象,也是一種使用方式。
public int loadThe Ark(Collection<Animal> candidates){
// 將animals封閉在方法內部
SortedSet<Animal> animals;
int numberParies = 0;
Animal candidate = null;
//針對animals容器進行各種統計
return numberParise;
}
class Status {
public String[] getStates(){
//每次都返回新的對象數組
return new String[]{"AA","BB","CC"};
}
}
- 另一種封閉模式:ThreadLocal模式。這種模式也比較常用,每次在web項目中保存session的時候,常常使用這種模式,來標記當前訪問線程的登陸情況。不過這個要註意的是,再web中使用TreadLocal容易導致溢出,具體的分析,請期待到springMVC系列。
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
public Connection initialValue(){
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection(){
return connectionHolder.get();
}
7-給出寫安全發佈的模式
- 在靜態初始化函數中初始化一個對象引用
- 將對象的引用保存到volatile類型的域或者AtomicReferance對象中
- 將對象的引用保存到某個正確構造對象的final類型域中
- 將對象的引用保存到一個由鎖保護的域中
二、不變性
滿足線程安全的另外一種方式,就是使用不可變對象。如果想要創建不可變對象的話,要滿足以下條件:
- 對象創建以後其狀態就不能修改
- 對象的所有域都是final類型
- 對象是正確創建的(在對象的創建期間,this引用沒有溢出)
1. 基礎的不可能變模型
這種方式,有點像我在《CC》觀後感那篇文章中,講到的一個觀點:儘量對原始工具包中的類進行封裝,有節制的使用其中的功能。下麵代碼就展示了,再可變對象的基礎上構建不可變類
public final class ThreeStooges{
//註意,這個stooges變數是可變的!
private final Set<String> stooges = new HashSet<>();
public ThreeStooges(){
stooges.add("1");
stooges.add("2");
stooges.add("3");
}
public boolean isStooges(String name){
return stooges.contains(name);
}
}
2. 有點高端的貨:使用不可變對象與volatile保證線程同步
這裡使用了三個內在的基本功點:對象不可變、對象讀寫分離、對象可見性。上代碼:
class Value{
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public Value(BigInteger lastNumber, BigInteger[] factors){
this.lastNumber = lastNumber;
this.lastFactors = Arrays.copyOf(factors,factors.length);//這裡進行寫複製
}
public BigInteger[] getFactors(BigInteger i){
if(lastNumber = null || !lastNumber.equals(i))
return null;
else
return Arrays.copyOf(lastFactors,lastFactors.length);//這裡進行讀複製
}
}
說明:由於每次初始化時候都進行類屬性的初始化,並與外界分離,因為factors數組每次都是複製一個副本進行初始化的!並且每次讀的時候,也是講數組對象進行複製分離。這樣,只要一初始化對象之後,實際上,類對象裡面的兩個類屬性都是不可變的了,因為全部與外界隔離了
下麵我們看看怎麼使用:
public class VolatileCacheFactorizer implements Servlet{
private volatile Value cache = new Value(null,new BigInteger[0]);
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if(factors == null){
factors = factor(i);
cache = new Value(i,factors);
}
encodeIntoResponse(resp,factors);
}
}
說明:這裡cache類屬性使用volatile,保證多線程寫入的時候,都能夠同步到主記憶體中,在這種情況下,多線程即是可見的,而通過Value對象的不變性又保證了對cache對象訪問的安全性,那這樣,整個service就是線程安全的了!
三、設計線程安全的類
這一部分,我看書中涉及到很多名詞,需要上網搜搜資料看看解釋,否則讀這一部分會很懵逼。我下麵從一些名詞解釋入手來說說這一章。
1. 什麼叫做監視器模式
乍看之下還以為這是一種設計模式,的確是一種設計模式!不過還想不起來是什麼樣子的。我一google才發現是非常簡單的,其實就是一段互斥訪問的代碼段(管程):
class SynClass{
private long value = 0;
public synchronized long getValue(){
return value;
}
public synchronized long increment(){
if(value == Long.MAX_VALUE)
throw new IllegalStateException("counter overflow");
return ++value;
}
}
說明:加了synchronized關鍵字的代碼段,就相當一個屋子,每次只允許一個線程訪問,如果訪問有需求了,還可能進行掛起工作,那監視器是誰能?監視器就是對象本身,synchronized是加鎖操作,這個鎖也是這個對象持有的一個內部鎖,如果要掛起代碼,可是使用對象本身就天然繼承自Object的wait方法,這就是監視器的作用。我看網上解釋說:監視器(其實就是每個對象自己,因為每個對象都繼承了Object)就像一個屋子的管理者,然後把對象這個“屋子”分成了三個地方:互斥訪問區域、準備訪問的區域和等待區域。
2. 什麼叫做先驗條件和後驗條件
- 先驗條件(precondition):針對方法(method),它規定了在調用該方法之前必須為真的條件。
- 後驗條件(postcondition):也是針對方法,它規定了方法順利執行完畢之後必須為真的條件。
3. 設計線程安全的類的三要素
- 找出構成對象狀態的所有變數
- 找出約束狀態變數的不變性條件
- 建立對象狀態的併發訪問管理策略
4. 什麼叫做不變性條件
這個也是要做一定解釋:程式在一系列的操作之後,還能夠滿足自己的先驗條件和後驗條件的,就叫做不變性條件(這個理解有點困難,大致我自己的想法是這樣)
5. 收集同步的需求
class SafeClass{
private long value = 0;
public synchronized long increment(){
if(value == Long.MAX_VALUE)
throw new IllegalStateException("counter overflow");
return ++value;
}
}
- 我們要做的,確定本類中的那些狀態,會再多線程的操作下影響對象的不變性
- 如果一個狀態轉變是依賴於前一個狀態的話,那就會複合操作,需要同步機制
- 當然,有些狀態轉變不依賴之前,例如溫度
- 上例中increment加上了synchronized就是一種保護程式不變性與後驗條件的機制
6. 註意狀態的所有權
舉個簡單的例子
class Owner{
private SunChild sub;
}
其實這個sub對象就是Owner所擁有的一個子對象,所有權歸Owner。但是如果加上如下代碼
class Owner{
private SunChild sub;
SubChild getSub(){
return sub;
}
}
這種情況下,所有權就被髮布了出去,這樣的情況就要考慮同步機制進行保護。
註意:交出所有權的時候一定要多加思考程式的運行情景,以防不備!
7. 實例封閉
如果某個對象不是線程安全的,我們可以將其進行封裝,或者通過單一鎖進行保護。下麵是使用實例封閉模式進行的一種樣例:
public class PersonSet{
private final Set<Person> mySet = new HashSet<>();//mySet本身並非線程安全
public synchronized void addPerson(Person p){
mySet.add(p)
}
public synchronized boolean contains(Person p){
return mySet.contain(p);
}
}
說明:將數據封裝在對象內部,可以將數據的訪問限制在對象的方法上,從而更容易確保線程再訪問數據時總能持有正確的鎖
8. 線程安全的又一方式:委托
委托,其實就是將對象的涉及到的影響可變性條件的狀態,放到JDK提供的一些線程安全的容器中去,進行統一管理。同樣也是一個簡單的例子:
public class Tracher{
private final ConcurrentMap<String,Object> localMap;
public Tracher(){
localMap = new ConcurrentHashMap<>();
}
public Map<String,Object> getLocations(){
return localMap;
}
public Object getLocation(String key){
return lcoalMap.get(key);
}
}
上面講統一使用ConcurrentMap進行管理。如果想要獲取一個不變的狀態的話,可以進行讀複製:
public Map<String,Object> getLocations(){
return new ConcurrentHashMap<>(localMap);
}
9. 委托不是萬能的
過分依賴原子類所造成的“殘局”:
public class NumberRange {
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i){
if(i>upper.get()){//註意這裡
throw new Exception("error");
}
lower.set(i)
}
public AtomicInteger getUpper(){
return upper;
}
}
說明:由於upper被暴露了出去,可是setLower方法內部進行了“先檢查後執行”的步驟,依賴於upper值,這樣,lower屬性的值就出現了不可預估性,原子操作沒達成,原子類失效了。可以使用加鎖來修改上述代碼
10. 特別需要註意的由委托引起的非線程安全
這種模式屬於一種叫做“客戶端加鎖”,其實就是寫程式中很不註意的,將內置鎖和屬性對象的鎖混淆所致,下麵是問題代碼:
public class ListHelper<E>{
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
...
public synchronized boolean putIfAbsent(E e){
//註意這裡synchronized使用的是內置鎖
boolean absent = !list.contains(e);
if(absent){
//這裡add使用的是list對象裡面的同步鎖
list.add(e);
}
return absent;
}
}
兩種鎖並不一樣,導致並沒有對“先判斷再執行”進行同步操作,還是會存在不安全性問題。下麵是解決的方式:
public class ListHelper<E>{
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
...
public boolean putIfAbsent(E e){
synchronized (list){//統一使用屬性對象的鎖
boolean absent = !list.contains(e);
if(absent){
list.add(e);
}
return absent;
}
}
}
四、總結
本次主要講了三個方面:
- 對象的發佈
- 不變性
- 設計線程安全的類
相對來說比較枯燥,儘量都是用簡潔明瞭的例子來混合講解了,望給位看官多多包涵~哈哈哈。接下來要分享的東西,就會實用很多,涉及到JDK線程工具的良好實用(如閉鎖、FutureTask等),並且我在接下來的線程分享文章中,會每次安排一個大章節,逐步進行生活必備品之一的java.util.concurrent.ThreadPoolExecutor源碼分析,敬請期待!