上一篇文章,我們介紹了如何利用二階段停止協議進行優雅停止線程和線程池,本篇介紹在并發編程中數據安全性,我們知道針對于數據的操作,讀和寫(添加、刪除、修改), 在并發線程讀寫的時候,變量不加鎖的情況下,一定會有線程安全問題。但是如果變量只有讀操作,多個線程就不存在資源的競爭操作,因為變量 i = 10, 多個線程不修改,都讀取到的一定是10。
所以Immutability模式就是利用變量只讀的方式。對象一創建之后,就不會在修改。
實現不可變性的類
如何實現呢,其實很簡單,就是針對的類和屬性 添加final 關鍵詞進行修飾。并且只提供只讀的方法。
Java中String、Integer 、Double等基礎類都是具備不可變性。類和屬性都是final,所有方法都是只讀的。但是你可能使用過String的替換方法,我們看看源碼看是怎么回事。
類和屬性都被final修飾。replace 其實是通過內部構件了一個新的char數組。進行操作的。 也就是創建了一個新的不可變對象。
public final class String {private final char value[];// 字符替換String replace(char oldChar, char newChar) {// 無需替換,直接返回 this if (oldChar == newChar){return this;}int len = value.length;int i = -1;/* avoid getfield opcode */char[] val = value; // 定位到需要替換的字符位置while (++i < len) {if (val[i] == oldChar) {break;}}// 未找到 oldChar,無需替換if (i >= len) {return this;} // 創建一個 buf[],這是關鍵// 用來保存替換后的字符串char buf[] = new char[len];for (int j = 0; j < i; j++) {buf[j] = val[j];}while (i < len) {char c = val[i];buf[i] = (c == oldChar) ? newChar : c;i++;}// 創建一個新的字符串返回// 原字符串不會發生任何變化return new String(buf, true);}
}
問題: 那么如果頻繁創建過多相同的對象,會不會對內存造成影響。又如何解決呢??
享元模式避免重復創建對象
這里可能要留一個坑了,那就是什么是享元模式,后邊有時間花一篇文章在介紹。
簡單一點其實享元模式就是可以共享的單元,目的是達到對象的服用、共享,前提是不可變對象。
將相同的對象只保存一份,可以復用。實現比較簡單,就是使用list或者map存儲共享的對象。
這里簡單說下和單例模式的區別:單例模式是為了保證對象的全局唯一性,享元模式是達到對象的服用。
享元模式工作模式:享元模式其實就是一個對象池,創建的時候,先看池里有沒有,沒有的話新創建,有的話 直接復用。
我們通過分析Long 可以發現,通過一個靜態類 提前創建-128到127之間的數。使用的時候,先查看是否在這個范圍,在的話直接使用。Integer類也是大同小異。
//緩存-128到127之間的數值private static class LongCache {private LongCache(){}static final Long cache[] = new Long[-(-128) + 127 + 1];static {for(int i = 0; i < cache.length; i++)cache[i] = new Long(i - 128);}}public static Long valueOf(long l) {final int offset = 128;// 緩存內的數據直接使用if (l >= -128 && l <= 127) { // will cachereturn LongCache.cache[(int)l + offset];}return new Long(l);}
問題:那么可以使用基礎類做一把鎖嘛?
private Integer lockA = new Integer(0);private Integer lockB = new Integer(0);public static void main(String[] args) throws InterruptedException {TestBaseLock t = new TestBaseLock();new Thread(()-> { t.lockA();}).start();new Thread(()-> { t.lockB();}).start();TimeUnit.SECONDS.sleep(100);}public void lockA () {synchronized (lockA) {System.out.println("LockA before");try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("LockA after");}}public void lockB () {synchronized (lockB) {System.out.println("LockB before");try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("LockB after");}}
從執行結果來看的話,發現啊,怎么會lockA 獲取鎖的同時,lockB也可以獲取呢。因為本質就是Integer lockA 和 lockB 共享的是同一個對象。公用一把鎖。并不是兩把鎖。
LockA before
LockB before
LockA after
LockB after
注意點
在實際的編程中,可能A對象內部的對象屬性B 和 屬性值是C是不可變的,但是對象屬性B 可以被修改。 所以我們需要合理評估不可變性的邊界在哪里,是否屬性對象也需要保證。
如果需要保證就需要加上voliatie保證可見性、如果保證原子性,可以使用原子類進行構建。
class B{int age=0;int name="abc";
}
final class A {final B b;final Integer c;void setAge(int a){c=a;}
}
總結
好了,本篇主要介紹了Immutability模式,利用享元模式解決不可變性的重復對象的問題,在多線程編程的時候,我們需要首先考慮是否數據是否不可變,如果不可變,就簡單了,如果不行在使用別的設計模式來解決數據安全問題。
在分布式系統中,有無狀態服務,就是不存儲數據,這種方式可以很好的無限水平拓展,當然也就是有對象的無狀態對象,類似于函數式編程,我們只需要輸入輸出,不改變數據。