java類中會定義不少變量,有類變量也有實例變量,這些變量在訪問的過程當中,會遇到一些可見性和原子性的問題。這裏咱們來詳細瞭解一下怎麼避免這些問題。java
不可變對象就是初始化以後不可以被修改的對象,那麼是否是類中引入了不可變對象,全部對不可變對象的修改都立馬對全部線程可見呢?git
實際上,不可變對象只能保證在多線程環境中,對象使用的安全性,並不可以保證對象的可見性。github
先來討論一下可變性,咱們考慮下面的一個例子:數組
public final class ImmutableObject { private final int age; public ImmutableObject(int age){ this.age=age; } }
咱們定義了一個ImmutableObject對象,class是final的,而且裏面的惟一字段也是final的。因此這個ImmutableObject初始化以後就不可以改變。緩存
而後咱們定義一個類來get和set這個ImmutableObject:安全
public class ObjectWithNothing { private ImmutableObject refObject; public ImmutableObject getImmutableObject(){ return refObject; } public void setImmutableObject(int age){ this.refObject=new ImmutableObject(age); } }
上面的例子中,咱們定義了一個對不可變對象的引用refObject,而後定義了get和set方法。多線程
注意,雖然ImmutableObject這個類自己是不可變的,可是咱們對該對象的引用refObject是可變的。這就意味着咱們能夠調用屢次setImmutableObject方法。
再來討論一下可見性。app
上面的例子中,在多線程環境中,是否是每次setImmutableObject都會致使getImmutableObject返回一個新的值呢?ui
答案是否認的。this
當把源碼編譯以後,在編譯器中生成的指令的順序跟源碼的順序並非徹底一致的。處理器可能採用亂序或者並行的方式來執行指令(在JVM中只要程序的最終執行結果和在嚴格串行環境中執行結果一致,這種重排序是容許的)。而且處理器還有本地緩存,當將結果存儲在本地緩存中,其餘線程是沒法看到結果的。除此以外緩存提交到主內存的順序也肯能會變化。
怎麼解決呢?
最簡單的解決可見性的辦法就是加上volatile關鍵字,volatile關鍵字可使用java內存模型的happens-before規則,從而保證volatile的變量修改對全部線程可見。
public class ObjectWithVolatile { private volatile ImmutableObject refObject; public ImmutableObject getImmutableObject(){ return refObject; } public void setImmutableObject(int age){ this.refObject=new ImmutableObject(age); } }
另外,使用鎖機制,也能夠達到一樣的效果:
public class ObjectWithSync { private ImmutableObject refObject; public synchronized ImmutableObject getImmutableObject(){ return refObject; } public synchronized void setImmutableObject(int age){ this.refObject=new ImmutableObject(age); } }
最後,咱們還可使用原子類來達到一樣的效果:
public class ObjectWithAtomic { private final AtomicReference<ImmutableObject> refObject= new AtomicReference<>(); public ImmutableObject getImmutableObject(){ return refObject.get(); } public void setImmutableObject(int age){ refObject.set(new ImmutableObject(age)); } }
若是是共享對象,那麼咱們就須要考慮在多線程環境中的原子性。若是是對共享變量的複合操做,好比:++, -- *=, /=, %=, +=, -=, <<=, >>=, >>>=, ^= 等,看起來是一個語句,但其實是多個語句的集合。
咱們須要考慮多線程下面的安全性。
考慮下面的例子:
public class CompoundOper1 { private int i=0; public int increase(){ i++; return i; } }
例子中咱們對int i進行累加操做。可是++其實是由三個操做組成的:
若是在單線程環境中,是沒有問題的,可是在多線程環境中,由於不是原子操做,就可能會發生問題。
解決辦法有不少種,第一種就是使用synchronized關鍵字
public synchronized int increaseSync(){ i++; return i; }
第二種就是使用lock:
private final ReentrantLock reentrantLock=new ReentrantLock(); public int increaseWithLock(){ try{ reentrantLock.lock(); i++; return i; }finally { reentrantLock.unlock(); } }
第三種就是使用Atomic原子類:
private AtomicInteger atomicInteger=new AtomicInteger(0); public int increaseWithAtomic(){ return atomicInteger.incrementAndGet(); }
若是一個方法使用了多個原子類的操做,雖然單個原子操做是原子性的,可是組合起來就不必定了。
咱們看一個例子:
public class CompoundAtomic { private AtomicInteger atomicInteger1=new AtomicInteger(0); private AtomicInteger atomicInteger2=new AtomicInteger(0); public void update(){ atomicInteger1.set(20); atomicInteger2.set(10); } public int get() { return atomicInteger1.get()+atomicInteger2.get(); } }
上面的例子中,咱們定義了兩個AtomicInteger,而且分別在update和get操做中對兩個AtomicInteger進行操做。
雖然AtomicInteger是原子性的,可是兩個不一樣的AtomicInteger合併起來就不是了。在多線程操做的過程當中可能會遇到問題。
一樣的,咱們可使用同步機制或者鎖來保證數據的一致性。
若是咱們要建立一個對象的實例,而這個對象的實例是經過鏈式調用來建立的。那麼咱們須要保證鏈式調用的原子性。
考慮下面的一個例子:
public class ChainedMethod { private int age=0; private String name=""; private String adress=""; public ChainedMethod setAdress(String adress) { this.adress = adress; return this; } public ChainedMethod setAge(int age) { this.age = age; return this; } public ChainedMethod setName(String name) { this.name = name; return this; } }
很簡單的一個對象,咱們定義了三個屬性,每次set都會返回對this的引用。
咱們看下在多線程環境下面怎麼調用:
ChainedMethod chainedMethod= new ChainedMethod(); Thread t1 = new Thread(() -> chainedMethod.setAge(1).setAdress("www.flydean.com1").setName("name1")); t1.start(); Thread t2 = new Thread(() -> chainedMethod.setAge(2).setAdress("www.flydean.com2").setName("name2")); t2.start();
由於在多線程環境下,上面的set方法可能會出現混亂的狀況。
怎麼解決呢?咱們能夠先建立一個本地的副本,這個副本由於是本地訪問的,因此是線程安全的,最後將副本拷貝給新建立的實例對象。
主要的代碼是下面樣子的:
public class ChainedMethodWithBuilder { private int age=0; private String name=""; private String adress=""; public ChainedMethodWithBuilder(Builder builder){ this.adress=builder.adress; this.age=builder.age; this.name=builder.name; } public static class Builder{ private int age=0; private String name=""; private String adress=""; public static Builder newInstance(){ return new Builder(); } private Builder() {} public Builder setName(String name) { this.name = name; return this; } public Builder setAge(int age) { this.age = age; return this; } public Builder setAdress(String adress) { this.adress = adress; return this; } public ChainedMethodWithBuilder build(){ return new ChainedMethodWithBuilder(this); } }
咱們看下怎麼調用:
final ChainedMethodWithBuilder[] builder = new ChainedMethodWithBuilder[1]; Thread t1 = new Thread(() -> { builder[0] =ChainedMethodWithBuilder.Builder.newInstance() .setAge(1).setAdress("www.flydean.com1").setName("name1") .build();}); t1.start(); Thread t2 = new Thread(() ->{ builder[0] =ChainedMethodWithBuilder.Builder.newInstance() .setAge(1).setAdress("www.flydean.com1").setName("name1") .build();}); t2.start();
由於lambda表達式中使用的變量必須是final或者final等效的,因此咱們須要構建一個final的數組。
在java中,64bits的long和double是被當成兩個32bits來對待的。
因此一個64bits的操做被分紅了兩個32bits的操做。從而致使了原子性問題。
考慮下面的代碼:
public class LongUsage { private long i =0; public void setLong(long i){ this.i=i; } public void printLong(){ System.out.println("i="+i); } }
由於long的讀寫是分紅兩部分進行的,若是在多線程的環境中屢次調用setLong和printLong的方法,就有可能會出現問題。
解決辦法本簡單,將long或者double變量定義爲volatile便可。
private volatile long i = 0;
本文的代碼:
learn-java-base-9-to-20/tree/master/security
本文已收錄於 http://www.flydean.com/java-security-code-line-visibility-atomicity/最通俗的解讀,最深入的乾貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!
歡迎關注個人公衆號:「程序那些事」,懂技術,更懂你!