多執行緒學習-02
阿新 • • 發佈:2018-11-11
執行緒安全問題與執行緒同步
1、什麼是執行緒安全問題
之前學習常用類時:
StringBuffer與StringBuilder
ArrayList與Vector
HashMap與Hashtable
和上一篇的賣票問題,出現負數票
導致執行緒安全問題:多個執行緒訪問同一份資源
問題出現總結:
1、多個執行緒
2、共享資料
3、多條語句訪問或操作共享資料
2、如何解決執行緒安全問題
解決思路: 對多條操作共享資料的語句,只能讓一個執行緒都執行完,在執行過程中,其他執行緒不可以參與執行。 解決辦法: 如果想要解決執行緒安全問題,就必須使用同步。所謂的同步就是指多個操作在同一個時間段內只能有一個執行緒進行,其他執行緒要等待 此執行緒完成之後才可以繼續執行。
3、同步的幾種方式
(1)同步程式碼塊
格式:多個執行緒需要使用唯一的一把鎖
synchronized(鎖物件){
需要同步的程式碼
}
(2)同步方法
格式:synchronized 方法其他修飾符 返回值型別 方法名(形參列表)throws 異常列表{}
1.同步程式碼塊
package com.synch; public class TestTicket { public static void main(String[] args) { Ticket t = new Ticket(); //三個執行緒共享了t物件,num就只有一份,每個執行緒都可能修改它,一旦其中一個執行緒修改它,那麼就會影響其他執行緒 new Thread(t,"視窗一").start(); new Thread(t,"視窗二").start(); new Thread(t,"視窗三").start(); } } class Ticket implements Runnable{ private int num = 10; public void run(){ while(true){ synchronized (this) { if(num<=0){ break; } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"搶到了一張票,剩餘:"+(--num)); } } } }
2.同步方法
package com.synch; public class TestTicket { public static void main(String[] args) { Ticket t = new Ticket(); //三個執行緒共享了t物件,num就只有一份,每個執行緒都可能修改它,一旦其中一個執行緒修改它,那麼就會影響其他執行緒 new Thread(t,"視窗一").start(); new Thread(t,"視窗二").start(); new Thread(t,"視窗三").start(); } } class Ticket implements Runnable{ private int num = 10; private boolean flag =true; public void run(){ while(flag){ sale(); } } synchronized public void sale(){ if(num<=0){ flag = false; return; } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"搶到了一張票,剩餘:"+(--num)); } }
4、同步需要注意的問題
(1)同步鎖
同步鎖機制:
在《Thinking in Java》中,是這麼說的:對於併發工作,你需要某種方式來防止兩個任務訪問相同的資源(其實就是共享資源競爭)。
防止這種衝突的方法就是當資源被一個任務使用時,在其上加鎖。第一個訪問某項資源的任務必須鎖定這項資源,使其他任務在其被解鎖
之前,就無法訪問它了,而在其被解鎖之時,另一個任務就可以鎖定並使用它了。
synchronized的鎖是什麼?
任意物件都可以作為同步鎖。所有物件都自動含有單一的鎖(監視器)。
同步方法的鎖:靜態方法(類名.class)、非靜態方法(this)
同步程式碼塊:自己指定,很多時候也是指定為this或類名.class
注意:
必須確保使用同一個資源的多個執行緒共用一把鎖,這個非常重要,否則就無法保證共享資源的安全
一個執行緒類中的所有靜態方法共用同一把鎖(類名.class),所有非靜態方法共用同一把鎖(this),同步程式碼塊(指定需謹慎)
(2)同步的範圍
如何找問題,即程式碼是否存線上程安全?(非常重要)
(1)明確哪些程式碼是多執行緒執行的程式碼
(2)明確多個執行緒是否有共享資料
(3)明確多執行緒執行程式碼中是否有多條語句操作共享資料的
2、如何解決呢?(非常重要)
對多條操作共享資料的語句,只能讓一個執行緒都執行完,在執行過程中,其他執行緒不可以參與執行。
即所有操作共享資料的這些語句都要放在同步範圍中
切記:
範圍太小:沒鎖住所有有安全問題的程式碼
範圍太大:沒發揮多執行緒的功能
public class TestSynchProblem {
public static void main(String[] args) {
Window t = new Window();
//三個執行緒共享了t物件,num就只有一份,每個執行緒都可能修改它,一旦其中一個執行緒修改它,那麼就會影響其他執行緒
new Thread(t,"視窗一").start();
new Thread(t,"視窗二").start();
new Thread(t,"視窗三").start();
}
}
class Window implements Runnable{
private int num = 10;
//範圍太大,導致只有一個視窗可以賣票
public void run(){
while(true){
//多個執行緒不是同一把鎖
synchronized(new Object()){//synchronized((Integer)num){[ int num = 10;
Integer i = (Integer)num;
num = 9;
Integer j = (Integer)num;
System.out.println(i==j);//false]
if(num<=0){
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"搶到了一張票,剩餘:"+(--num));
}
}
}
}
class Window implements Runnable{
private int num = 10;
public void run(){
while(num>0){//num>0的條件也屬於訪問共享資料的程式碼,沒有放入同步範圍,範圍太小,資料仍然不安全
synchronized (this) {
//if(num>0){//解決方案,可以在裡面再判斷一次條件
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"搶到了一張票,剩餘:"+(--num));
// }else{
// break;
// }
}
}
}
}
class Window implements Runnable{
private int num = 10;
//範圍太大,導致只有一個視窗可以賣票
synchronized public void run(){
while(num>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"搶到了一張票,剩餘:"+(--num));
}
}
}
(3)同步的優點與缺點
同步的好處:解決了多執行緒的安全問題
副作用:多個執行緒需要等待鎖,因此效能低
5、釋放鎖和不釋放鎖
釋放鎖的操作
當前執行緒的同步方法、同步程式碼塊執行結束。
當前執行緒在同步程式碼塊、同步方法中遇到break、return終止了該程式碼塊、該方法的繼續執行。
當前執行緒在同步程式碼塊、同步方法中出現了未處理的Error或Exception,導致當前執行緒異常結束。
當前執行緒在同步程式碼塊、同步方法中執行了鎖物件的wait()方法,當前執行緒被掛起,並釋放鎖。
不會釋放鎖的操作
1.執行緒執行同步程式碼塊或同步方法時,程式呼叫Thread.sleep()、Thread.yield()方法暫停當前執行緒的執行。
2.執行緒執行同步程式碼塊時,其他執行緒呼叫了該執行緒的suspend()方法將該該執行緒掛起,該執行緒不會
釋放鎖(同步監視器)。
1.應儘量避免使用suspend()和resume()這樣的過時來控制執行緒
6、死鎖(遏制彼此命門,死活不放)
不同的執行緒分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了執行緒的死鎖
解決方法:
專門的演算法、原則
儘量減少同步資源的定義
儘量避免巢狀同步
7、顯示鎖Lock
java從1.5版本之後,提供了Lock介面。在後期對鎖的分析過程中,發現,獲取鎖,或者釋放鎖的動作應該是鎖這個事物更清楚。
所以將這些動作定義在了鎖當中,並把鎖定義成物件。所以,同步是隱式的鎖操作,而Lock物件是顯示的鎖操作。
synchronized是JVM層面提供的鎖,而Lock是Java的語言層面jdk為我們提供的鎖,這些鎖都在 Java.util.concurrent包中。
先來看一下JVM提供的鎖和併發包中的鎖有哪些區別:
1.synchronized的加鎖和釋放都是由JVM提供,不需要我們關注,而lock的加鎖和釋放全部由我們去控制,
通常釋放鎖的動作要在finally中實現。
2.synchronized只有一個狀態條件,也就是每個物件只有一個監視器,如果需要多個Condition的組合那麼synchronized
是無法滿足的,而Lock則提供了多條件的互斥,非常靈活。
3.ReentrantLock(可重入鎖,鎖的一種) 擁有Synchronized相同的併發性和記憶體語義,
此外還多了鎖投票,定時鎖等候和中斷鎖等候。(鎖的種類有很多)
package com.lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestLock {
public static void main(String[] args) {
Ticket t = new Ticket();
//三個執行緒共享了t物件,num就只有一份,每個執行緒都可能修改它,一旦其中一個執行緒修改它,那麼就會影響其他執行緒
new Thread(t,"視窗一").start();
new Thread(t,"視窗二").start();
new Thread(t,"視窗三").start();
}
}
class Ticket implements Runnable{
private int num = 10;
private final ReentrantLock lock = new ReentrantLock();
public void run(){
while(true){
try {
lock.lock();//獲取鎖
if(num<=0){
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"搶到了一張票,剩餘:"+(--num));
} catch (Exception e) {
e.printStackTrace();
}finally{
lock.unlock();//釋放鎖
}
}
}
}
idea枷鎖方式快捷鍵:ctrl+alt+t