詳解java安全編碼指南之可見性和原子性
不可變對(duì)象的可見性
不可變對(duì)象就是初始化之后不能夠被修改的對(duì)象,那么是不是類中引入了不可變對(duì)象,所有對(duì)不可變對(duì)象的修改都立馬對(duì)所有線程可見呢?
實(shí)際上,不可變對(duì)象只能保證在多線程環(huán)境中,對(duì)象使用的安全性,并不能夠保證對(duì)象的可見性。
先來討論一下可變性,我們考慮下面的一個(gè)例子:
public final class ImmutableObject {
private final int age;
public ImmutableObject(int age){
this.age=age;
}
}
我們定義了一個(gè)ImmutableObject對(duì)象,class是final的,并且里面的唯一字段也是final的。所以這個(gè)ImmutableObject初始化之后就不能夠改變。
然后我們定義一個(gè)類來get和set這個(gè)ImmutableObject:
public class ObjectWithNothing {
private ImmutableObject refObject;
public ImmutableObject getImmutableObject(){
return refObject;
}
public void setImmutableObject(int age){
this.refObject=new ImmutableObject(age);
}
}
上面的例子中,我們定義了一個(gè)對(duì)不可變對(duì)象的引用refObject,然后定義了get和set方法。
注意,雖然ImmutableObject這個(gè)類本身是不可變的,但是我們對(duì)該對(duì)象的引用refObject是可變的。這就意味著我們可以調(diào)用多次setImmutableObject方法。
再來討論一下可見性。
上面的例子中,在多線程環(huán)境中,是不是每次setImmutableObject都會(huì)導(dǎo)致getImmutableObject返回一個(gè)新的值呢?
答案是否定的。
當(dāng)把源碼編譯之后,在編譯器中生成的指令的順序跟源碼的順序并不是完全一致的。處理器可能采用亂序或者并行的方式來執(zhí)行指令(在JVM中只要程序的最終執(zhí)行結(jié)果和在嚴(yán)格串行環(huán)境中執(zhí)行結(jié)果一致,這種重排序是允許的)。并且處理器還有本地緩存,當(dāng)將結(jié)果存儲(chǔ)在本地緩存中,其他線程是無法看到結(jié)果的。除此之外緩存提交到主內(nèi)存的順序也肯能會(huì)變化。
怎么解決呢?
最簡(jiǎn)單的解決可見性的辦法就是加上volatile關(guān)鍵字,volatile關(guān)鍵字可以使用java內(nèi)存模型的happens-before規(guī)則,從而保證volatile的變量修改對(duì)所有線程可見。
public class ObjectWithVolatile {
private volatile ImmutableObject refObject;
public ImmutableObject getImmutableObject(){
return refObject;
}
public void setImmutableObject(int age){
this.refObject=new ImmutableObject(age);
}
}
另外,使用鎖機(jī)制,也可以達(dá)到同樣的效果:
public class ObjectWithSync {
private ImmutableObject refObject;
public synchronized ImmutableObject getImmutableObject(){
return refObject;
}
public synchronized void setImmutableObject(int age){
this.refObject=new ImmutableObject(age);
}
}
最后,我們還可以使用原子類來達(dá)到同樣的效果:
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));
}
}
保證共享變量的復(fù)合操作的原子性
如果是共享對(duì)象,那么我們就需要考慮在多線程環(huán)境中的原子性。如果是對(duì)共享變量的復(fù)合操作,比如:++, -- *=, /=, %=, +=, -=, <<=, >>=, >>>=, ^= 等,看起來是一個(gè)語(yǔ)句,但實(shí)際上是多個(gè)語(yǔ)句的集合。
我們需要考慮多線程下面的安全性。
考慮下面的例子:
public class CompoundOper1 {
private int i=0;
public int increase(){
i++;
return i;
}
}
例子中我們對(duì)int i進(jìn)行累加操作。但是++實(shí)際上是由三個(gè)操作組成的:
1.從內(nèi)存中讀取i的值,并寫入CPU寄存器中。
2.CPU寄存器中將i值+1
3.將值寫回內(nèi)存中的i中。
如果在單線程環(huán)境中,是沒有問題的,但是在多線程環(huán)境中,因?yàn)椴皇窃硬僮鳎涂赡軙?huì)發(fā)生問題。
解決辦法有很多種,第一種就是使用synchronized關(guān)鍵字
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();
}
保證多個(gè)Atomic原子類操作的原子性
如果一個(gè)方法使用了多個(gè)原子類的操作,雖然單個(gè)原子操作是原子性的,但是組合起來就不一定了。
我們看一個(gè)例子:
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();
}
}
上面的例子中,我們定義了兩個(gè)AtomicInteger,并且分別在update和get操作中對(duì)兩個(gè)AtomicInteger進(jìn)行操作。
雖然AtomicInteger是原子性的,但是兩個(gè)不同的AtomicInteger合并起來就不是了。在多線程操作的過程中可能會(huì)遇到問題。
同樣的,我們可以使用同步機(jī)制或者鎖來保證數(shù)據(jù)的一致性。
保證方法調(diào)用鏈的原子性
如果我們要?jiǎng)?chuàng)建一個(gè)對(duì)象的實(shí)例,而這個(gè)對(duì)象的實(shí)例是通過鏈?zhǔn)秸{(diào)用來創(chuàng)建的。那么我們需要保證鏈?zhǔn)秸{(diào)用的原子性。
考慮下面的一個(gè)例子:
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;
}
}
很簡(jiǎn)單的一個(gè)對(duì)象,我們定義了三個(gè)屬性,每次set都會(huì)返回對(duì)this的引用。
我們看下在多線程環(huán)境下面怎么調(diào)用:
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();
因?yàn)樵诙嗑€程環(huán)境下,上面的set方法可能會(huì)出現(xiàn)混亂的情況。
怎么解決呢?我們可以先創(chuàng)建一個(gè)本地的副本,這個(gè)副本因?yàn)槭潜镜卦L問的,所以是線程安全的,最后將副本拷貝給新創(chuàng)建的實(shí)例對(duì)象。
主要的代碼是下面樣子的:
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);
}
}
我們看下怎么調(diào)用:
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();
因?yàn)閘ambda表達(dá)式中使用的變量必須是final或者final等效的,所以我們需要構(gòu)建一個(gè)final的數(shù)組。
讀寫64bits的值
在java中,64bits的long和double是被當(dāng)成兩個(gè)32bits來對(duì)待的。
所以一個(gè)64bits的操作被分成了兩個(gè)32bits的操作。從而導(dǎo)致了原子性問題。
考慮下面的代碼:
public class LongUsage {
private long i =0;
public void setLong(long i){
this.i=i;
}
public void printLong(){
System.out.println("i="+i);
}
}
因?yàn)閘ong的讀寫是分成兩部分進(jìn)行的,如果在多線程的環(huán)境中多次調(diào)用setLong和printLong的方法,就有可能會(huì)出現(xiàn)問題。
解決辦法本簡(jiǎn)單,將long或者double變量定義為volatile即可。
private volatile long i = 0;
以上就是詳解java安全編碼指南之可見性和原子性的詳細(xì)內(nèi)容,更多關(guān)于java安全編碼指南之可見性和原子性的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot中如何將logback切換為log4j2
springboot默認(rèn)使用logback作為日志記錄框架,常見的日志記錄框架有l(wèi)og4j、logback、log4j2,這篇文章我們來學(xué)習(xí)怎樣將logbak替換為log4j2,需要的朋友可以參考下2023-06-06
Java導(dǎo)出excel時(shí)合并同一列中相同內(nèi)容的行思路詳解
這篇文章主要介紹了Java導(dǎo)出excel時(shí)合并同一列中相同內(nèi)容的行,需要的朋友可以參考下2018-06-06
智能 AI 代碼生成工具 Cursor 安裝和使用超詳細(xì)教程
Cursor.so 是一個(gè)集成了 GPT-4 的國(guó)內(nèi)直接可以訪問的,優(yōu)秀而強(qiáng)大的免費(fèi)代碼生成器,可以幫助你快速編寫、編輯和討論代碼,這篇文章主要介紹了智能 AI 代碼生成工具 Cursor 安裝和使用介紹,需要的朋友可以參考下2023-05-05
java圖形化界面實(shí)現(xiàn)簡(jiǎn)單混合運(yùn)算計(jì)算器的示例代碼
這篇文章主要介紹了java圖形化界面實(shí)現(xiàn)簡(jiǎn)單混合運(yùn)算計(jì)算器的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11

