java安全编码指南之:可见性和原子性
文章目錄
- 簡介
- 不可變對象的可見性
- 保證共享變量的復合操作的原子性
- 保證多個Atomic原子類操作的原子性
- 保證方法調用鏈的原子性
- 讀寫64bits的值
簡介
java類中會定義很多變量,有類變量也有實例變量,這些變量在訪問的過程中,會遇到一些可見性和原子性的問題。這里我們來詳細了解一下怎么避免這些問題。
不可變對象的可見性
不可變對象就是初始化之后不能夠被修改的對象,那么是不是類中引入了不可變對象,所有對不可變對象的修改都立馬對所有線程可見呢?
實際上,不可變對象只能保證在多線程環境中,對象使用的安全性,并不能夠保證對象的可見性。
先來討論一下可變性,我們考慮下面的一個例子:
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方法。
再來討論一下可見性。
上面的例子中,在多線程環境中,是不是每次setImmutableObject都會導致getImmutableObject返回一個新的值呢?
答案是否定的。
當把源碼編譯之后,在編譯器中生成的指令的順序跟源碼的順序并不是完全一致的。處理器可能采用亂序或者并行的方式來執行指令(在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();}保證多個Atomic原子類操作的原子性
如果一個方法使用了多個原子類的操作,雖然單個原子操作是原子性的,但是組合起來就不一定了。
我們看一個例子:
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的數組。
讀寫64bits的值
在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/
最通俗的解讀,最深刻的干貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!
總結
以上是生活随笔為你收集整理的java安全编码指南之:可见性和原子性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ECMAScript 6新特性简介
- 下一篇: java安全编码指南之:敏感类的拷贝