在大多數實際應用中,常常存在兩個或兩個以上的線程共享對同一數據的存儲。如果多個線程去對同一對象的數據進行修改,則會引起線程競爭資源,導致數據被修改錯誤的問題,比如在ATM機上取款,多個用戶對同一賬戶進行操作就會很容易發生。所以下面分析如何解決多線程共享資源引起競爭導致數據破壞問題。
### **一、鎖的原理**
java中為每一個對象都提供了鎖機制,用synchronized關鍵字修飾,它可以修飾方法和代碼塊。當一個對象獲得該鎖時,只能充許一個線程對該對象進行操作,其他線程處于等待狀態,直到該線程釋放鎖。
鎖時可以重復利用的,鎖有一個持有計數器來記錄被利用的情況。線程可以根據計數器去加鎖和釋放鎖。
條件對象可以理解為臨界區,線程只有在滿足某一條件后才能使用它,一個鎖對象可以有一個或多個相關的條件對象。以下為同步實例的實現:
~~~
public class FooThread extends Thread {
private int x=100;
public FooThread(String name) {
super(name);
}
@Override
public void run() {
for(int i=0;i<4;i++){
x=this.sub(x, 30);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " : 當前的x值= " +x);
}
}
public synchronized int sub(int x,int y){
return x=x-y;
}
}
~~~
### **二、內部鎖**
內部鎖將類的相關靜態方法加上synchronized關鍵字,比如Bank類有一個靜態同步的方法,那么當該方法被調用時,Bank.class將會被調用對象鎖住,則其他線程無法再調用該類中的對象和其他同步的靜態方法。
內部鎖也存在以下局限性:
1.無法中斷一個在視圖獲得鎖的線程
2.視圖鎖得鎖事不能設定超時
3.每一個鎖僅有一個單一條件
### **三、同步阻塞**
如果線程試圖進入同步方法,而其鎖已經被占用,則線程在該對象上被阻塞。實質上,線程進入該對象的的一種池中,必須在哪里等待,直到其鎖被釋放,該線程再次變為可運行或運行為止。
當考慮阻塞時,一定要注意哪個對象正被用于鎖定:
1、調用同一個對象中非靜態同步方法的線程將彼此阻塞。如果是不同對象,則每個線程有自己的對象的鎖,線程間彼此互不干預。
2、調用同一個類中的靜態同步方法的線程將彼此阻塞,它們都是鎖定在相同的Class對象上。
3、靜態同步方法和非靜態同步方法將永遠不會彼此阻塞,因為靜態方法鎖定在Class對象上,非靜態方法鎖定在該類的對象上。
4、對于同步代碼塊,要看清楚什么對象已經用于鎖定(synchronized后面括號的內容)。在同一個對象上進行同步的線程將彼此阻塞,在不同對象上鎖定的線程將永遠不會彼此阻塞。
**volatile域**
若僅僅為了讀寫一個或兩個實例域就使用同步,這對資源的開銷過大。volatile是為實例域的訪問提供了一種免鎖機制,若聲明一個實例域為volatile,則虛擬機就知道該域可能被其他線程并發訪問。聲明方式:
private volatile boolean flag;
還有一種用于原子整數,浮點數等的包裝器類 Atomic可以應用于程序的并發訪問,保證域的安全。
總之,在以下三個條件下,域的并發訪問是安全的:
1.域是final 并且在在構造器調用完成后訪問
2.對域的訪問由公有的鎖進行保護
3.域是volatile的
### **四、線程安全**
當一個類已經很好的同步以保護它的數據時,這個類就稱為“線程安全的”。
即使是線程安全類,也應該特別小心,因為操作的線程是間仍然不一定安全。
舉個形象的例子,比如一個集合是線程安全的,有兩個線程在操作同一個集合對象,當第一個線程查詢集合非空后,刪除集合中所有元素的時候。第二個線程也來執行與第一個線程相同的操作,也許在第一個線程查詢后,第二個線程也查詢出集合非空,但是當第一個執行清除后,第二個再執行刪除顯然是不對的,因為此時集合已經為空了。程序說明如下:
~~~
public class Demo4 {
@SuppressWarnings("unchecked")
private List resultList=Collections.synchronizedList(new LinkedList());
public synchronized void add(String param){
resultList.add(param);
}
public synchronized String remove(){
if(resultList.size()>0){
return (String) resultList.remove(0);
}else{
return null;
}
}
}
~~~
**測試調用**
~~~
public static void main(String[] args) {
final Demo4 d=new Demo4();
d.add("test");
class Test extends Thread{
@Override
public void run() {
String param=d.remove();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(param);
}
}
Thread t1=new Test();
Thread t2=new Test();
t1.start();
t2.start();
}
~~~
雖然集合對象 private List nameList = Collections.synchronizedList(new LinkedList());
是同步的,但若remove()方法的synchronized去掉后,會引起線程不安全問題。
### **五、死鎖**
鎖和條件不能解決多線程中所有問題,當多個線程發生阻塞時,每個線程在等待另一線程釋放資源會發生死鎖,比如 賬戶A:200元,賬戶B:300元,A線程從賬戶A轉移300到賬戶B,線程B從賬戶B轉移400到賬戶A,因為A和B賬戶的余額都不足,無法進行轉換,兩個線程無法繼續執行,而引發死鎖狀態。下面看一個發生死鎖的實例:
~~~
public class DeadLock implements Runnable {
int flag=1;
final Object o1=new Object();
final Object o2=new Object();
@Override
public void run() {
System.out.println("flag="+flag);
if(flag==1){
synchronized (o1) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (o2) {
System.out.println("1");
}
}else if(flag==0){
synchronized (o2) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (o1) {
System.out.println("2");
}
}
}
}
~~~
**程序說明:**
◆ 一個線程(ThreadA)調用run()。
◆ ThreadA在o1上同步,但允許被搶先執行。
◆ 另一個線程(ThreadB)開始執行。
◆ ThreadB調用run()。
◆ ThreadB獲得o2,繼續執行,企圖獲得o1。但ThreadB不能獲得o1,因為ThreadA占有o1。
◆ 現在,ThreadB阻塞,因為它在等待ThreadA釋放o1。
◆ 現在輪到ThreadA繼續執行。ThreadA試圖獲得o2,但不能成功,因為o2已經被ThreadB占有了。
◆ ThreadA和ThreadB都被阻塞,程序死鎖。