JAVA 多線程(3)
再講線程安全:
一、臟讀
臟讀:在於讀字,意在在讀取實例變量時,實例變量有可能被另外一個線程更改了,導致獲取到的數據出現異常。
在非線程安全的情況下,如果線程A與線程B 共同使用對象實例C中的方法method,如果實例C存在實例變量,同時在method中會操作這個實例變量a,則有可能出現臟讀的情況。
也就是期望值不同,讀取的數據不同,因為線程A與B會同時使用實例C的方法。
例如:
private String name;
public static void main(String[] args){
Test2 test2 = new Test2();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// System.out.println(Thread.currentThread().getName());
test2.testUnsafe("a","我是A");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// System.out.println(Thread.currentThread().getName());
test2.testUnsafe("b","我是B");
}
});
t.start();
t2.start();
}
public void testUnsafe(String name,String param){
this.name = name;
try {
Thread.sleep(1000);
System.out.println(this.name+":"+param);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
執行結果如下:
代碼中加上了sleep 為了模擬運算所需的時間。
可以看出來,大概是這樣的過程:線程B在run時首先搶到了資源,並運行了實例方法,並把實例變量name修改為b,
然後繼續執行,這時候線程A搶回了資源(因為不是同步的),它把實例變量那麽又修改為b,這個是時候開始執行其他邏輯操作(這裏用sleep模擬),
然後2個線程輪番執行最後的操作-打印。
因為這個時候實例變量name 已經變為a了, 所以線程B 出現臟讀,和期望輸出的 b:我是B 結果出現差異。
如果操作的是不同的對象實例(在synchronized 同步裏 jvm 會創建多個鎖,下面的例子控制2個實例,也就是創建了2個鎖),就不會出現這個問題了,還有如果name不是實例變量,只是私有變量的話也不會出現這種情況。
修改一下看看:
public static void main(String[] args){ Thread t = new Thread(new Runnable() { @Overridepublic void run() { Test2 test2 = new Test2(); test2.testUnsafe("a","我是A"); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { Test2 test2 = new Test2(); test2.testUnsafe("b","我是B"); } }); t.start(); t2.start(); }
輸出結果:
如果想要保證使用實例變量而又不出現這種問題,怎麽辦,同步~ 可以使用 synchronized 方法或 synchronized代碼塊。
例如:
public synchronized void testUnsafe(String name,String param){
this.name = name;
try {
Thread.sleep(1000);
System.out.println(this.name+":"+param);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
輸出結果:
關於synchronized 想看的可以下看之前的隨筆(《JAVA 多線程(1)》)。
這裏我想記錄一下類與對象和實例的個人理解():
Class 是類,編寫時候我們稱為類,編譯好的class我們可以稱為class對象,或者說類對象,由類對象new出來的是實例或者說叫對象實例也就是instance。
它們分時期,分關系。類是抽象的概念,對象為具體的事物,人是類,張三是人,張三是人這個類具象,是一個對象實例的存在,它的嘴巴是實例變量,吃飯是實例方法,米飯和菜是吃飯這個對象方法中的方法私有變量,
打比方張三、李四在一起去吃飯(2個線程),都調用了吃飯這個方法(吃飯這個方法是人類共有的),分布是土豆A與土豆B,如果不做同步,有可能2個人會夾到同一條土豆絲。
好吧,上面的比方看看就好。
二、可重入鎖
如果一個線程獲取了或者說搶到了cpu資源,拿到了實例對象鎖,那麽在實例方法中在調用其他同步方法時,依然會獲取到同一個鎖,因為已經搶到了,
比方說,一個屋子有3個房間(方法),張三(線程),搶到了房間1的鎖,由於李四和王五壓根就沒進入這個屋子,所有他們倆必須等張三出來才行,這樣的話
張三進過房間1後,還能獲得房間2、3的鎖。
這個鎖指的是對象鎖,或者說一個實例對象只有一把鎖會更好理解。因為李四和王五想進房間2,雖然不是房間1,但是只有一把鎖,這個鎖在張三手上。
可重入鎖個人感覺主要貢獻在於可繼承,如果B基礎了A,那麽如果操作B,獲取到了實例對象B的鎖,那麽也可以繼續獲得A的鎖。
三、異常釋放鎖
當線程出現異常時,鎖會自動釋放。
四、同步不具有繼承
如果類A 有同步方法 methodA,類B繼承了類A並重寫了methodA 方法,但是沒有加上同步關鍵字,那麽實際上,B類中重寫的methodA 並不是同步方法。
五、同步方法與同步代碼塊
之前在1中寫過,這裏再提及,同步代碼塊同樣時獲得的是對象鎖,當不同的實例方法各自有各自的同步代碼塊,當線程A訪問methodA 時獲得實例對象鎖,如果線程B訪問的實例對象與線程A相同,那麽線程B如想
訪問methodB中的同步代碼塊時同樣需要等線程A使用完methodA,釋放對象鎖,才能執行。
六、非當前實例對象鎖(任意對象監視器)
優點:同一個類中的多個方法或者同一個方法中有多個代碼塊,為了提高性能,使用多個對象鎖,來加快運行速度,或者說減少阻塞。
例如:
private Object object = new Object();
public static void main(String[] args){
Test2 test2 = new Test2();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
test2.test();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
test2.test2();
}
});
t.start();
t2.start();
}
public void test2(){
synchronized (object){
System.out.println("我是第二個塊"+Thread.currentThread().getName());
}
}
public void test(){
synchronized (this){
try {
System.out.println("我是第一個塊 開始:"+Thread.currentThread().getName());
Thread.sleep(100);
System.out.println("我是第一個塊 結束:"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
輸出:
通過控制臺打印結果可以看出,在A線程訪問test方法執行的同時,B線程同樣訪問可test2方法。
所以,他們互不幹擾,因為不是一把鎖。
但是問題又出來,就因為這樣提高了性能,但是又有可能會導致臟讀,如果methodA與methodB 中有對同一個實例變量的寫操作,那麽及有可能出現臟讀。
如下:
private Object object = new Object();
private static List<String> list = new ArrayList<>();
public static void main(String[] args){
Test2 test2 = new Test2();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
test2.test();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
test2.test2();
}
});
t.start();
t2.start();
try {
Thread.sleep(6000);
System.out.println(list.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void test2(){
synchronized (object){
judge("test2");
}
}
public void test(){
synchronized (this){
judge("test");
}
}
public void judge(String what){
try {
if(list.size() < 1){
Thread.sleep(2000);
list.add(what);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
輸出結果:
出現了,以為是異步的,所以線程A和線程B都可以同時訪問到judge方法,又同時進入到了if語句中,導致最終結果輸出為2,而不是我們期望的1。
所以,方法就是,給judge加上同步鎖:
synchronized (list){ if(list.size() < 1){ Thread.sleep(2000); list.add(what); } }
這樣拿到list對象鎖的操作就變成同步的了~
明兒個繼續~
JAVA 多線程(3)