為了加快程式碼的運行速度,我們採用了多執行緒的方法。並行的執行確實讓程式碼變得更有效率,但隨之而來的問題是,有很多個執行緒在程式中同時運行,如果它們同時的去修改一個對象,很可能會造成謳誤的情況,這個時候我們需要用一種同步的機制來管理這些執行緒。
記得作業系統中,讓我印像很深的有一張圖。上面畫的是一塊塊進程,在這些進程裡面分了幾個線程,所有這些線程都齊刷刷統一的指向進程的資源。 java中也是如此,資源會在執行緒間共享而不是每個執行緒都有一份獨立的資源。在這種共享的情況下,很有可能有多個執行緒同時在存取一個資源,這種現像我們叫做競爭條件。
在一個銀行系統中,每個執行緒分別管理一個帳戶,這些執行緒可能會進行轉帳的操作。
在一個執行緒進行操作的時候,他首先,會把帳戶餘額存放到暫存器中,第二步,它將暫存器中的數字減少要轉出的錢數,第三步,它將結果寫回餘額中。
問題在於,這個執行緒在執行完1、2步驟時,另外一個執行緒被喚醒並且修改了第一個執行緒的帳戶餘額值,但這個時候第一個執行緒並不知情。第一個線程等待第二個線程執行完畢後,繼續他的第三步:將結果寫回餘額中。這時候,它把第二個執行緒的操作刷掉了,所以整個的系統的總錢數一定會發成錯誤。
這就是java競爭條件發生的不良狀況。
上面的範例告訴我們,如果我們的操作不是原子運算,被打斷是肯定會發生的,即使有的時候機率真的非常小,但是也並不能排除這種情況。我們不能把我們的程式碼變成像作業系統中的原子操作,我們能做的是為我們的程式碼上鎖來保證安全性。在並發程序中,如果我們想要存取數據,在這之前我們先給我們的程式碼套一個鎖,在我們使用鎖的期間,我們的程式碼中涉及的資源就像是被」鎖上了「一樣,不能被其他的線程訪問,知道我們打開這個鎖。
在java中,synchronized關鍵字和ReentrantLock類別都有這種鎖定的功能。我們在這裡先一起來討論一下ReentrantLcok的功能。
在這個類別中,提供了兩個建構器,一個是預設建構器,沒什麼好說的,一個是帶有公平策略的構造器。這個公平策略首先他比正常的鎖慢很多,其次在有的情況下他並不是真正公平的。而且如果我們沒有特別的理由真的需要公平策略的時候,盡量不要去研究這個策略。
ReentrantLock myLock = new ReentrantLock(); //创建对象 myLock.lock(); //获取锁try{...} finally{ myLock.unlock(); //释放锁 }
一定要記得在finally中釋放鎖定! !我們之前說過,未檢查的錯誤會導致執行緒的終止。莫名其妙的終止會讓程式停止往下運行,如果不把釋放放在finally中,這個鎖將一直無法釋放。這種道理和我們在平時框架中用包後.close()
是一個道理。說到close,值得一提的,當我們使用鎖的時候,我們不能使用“帶有資源的try語句”,因為這個鎖並不是用close來關閉的。如果你不知道有資源的try語句是什麼,那就當我沒說這句話吧。
如果你要在遞迴或循環程式中使用鎖,那麼就放心的用吧。 ReentrantLock鎖定具有可重入性,他會在每次呼叫lock()的時候維護一個計數記錄著被呼叫的次數,在每一次的lock呼叫都必須要用unlock來釋放。
通常,執行緒在上了鎖進入臨界區之後發現了一個問題,他們所需要的資源,在別的物件中被使用或不滿足他們能執行的條件,這個時候我們需要用一個條件物件來管理這些得到了一個鎖,但是不能做有用工作的執行緒。
if(a>b){ a.set(b-1);}
上面是一個很簡單的條件判斷,但是我們在並發程式中不能這樣寫。存在的問題是,如果在這個執行緒剛剛做完判斷之後,另外一個執行緒被喚醒,並且另外一個執行緒在操作之後使得a小於b(if語句中的條件已經不再正確)。
那么这个时候我们可能想到,我们把整个if语句直接放在锁里面,确保自己的代码不会被打断。但是这样又存在一个问题,如果if判断是false,那么if中的语句不会被执行。但是如果我们需要去执行if中的语句,甚至我们要一直等待if判断变的正确之后去执行if中的语句,这时,我们突然发现,if语句再也不会变得正确了,因为我们的锁把这个线程锁死,其他的线程没办法访问临界区并修改a和b的值让if判断变得正确,这真的是非常尴尬,我们自己的锁把我们自己困住了,我们出不去,别人进不来。
为了解决这种情况,我们用ReentrantLock类中的newCondition方法来获取一个条件对象。
Condition cd = myLock.newCondition();
获取了Condition对象之后,我们就应该来研究这个对象有什么方法和作用了。先不急于看API,我们回到主题发现现在亟待解决的就是if条件判断的问题,我们如何才能:在已经上锁的情况下,发现if判断错误时,给其他线程机会并自己一直等着if判断变回正确。
Condition类就是为了解决这个难题而生的,有了Condition类之后,我们在if语句下面直接跟上await方法,这个方法表示这个线程被阻塞,并放弃了锁,等其他的线程来操作。
注意在这里我们用的名词是阻塞,我们之前也说过阻塞和等待有很大不同:等待获得锁时,一旦锁有了空闲,他可以自动的去获得锁,而阻塞获得锁时,即使有空闲的锁,也要等待线程调度器允许他去持有锁的时候才能获得锁。
其他的线程在顺利执行if语句内容之后,要去调用signalAll方法,这个方法将会重新去激活所有的因为这个条件被阻塞的线程,让这些线程重新获得机会,这些线程被允许从被阻塞的地方继续进行。此时,线程应该再次测试该条件,如果还是不能满足条件,需要再次重复上述操作。
ReentrantLock myLock = new ReentrantLock(); //创建锁对象myLock.lock(); //给下面的临界区上锁 Condition cd = myLock.newCondition(); //创建一个Condition对象,这个cd对象表示条件对象while(!(a>b)) cd.await(); //上面的while循环和await方法调用是标准写法 //如果不能满足if的条件,那么他将进入阻塞状态,放弃锁,等待别人去激活它a.set(b-1); //一直等到从while循环出来,满足了判断的条件,我们执行自己的功能cd.signalAll(); //最后一定不能忘记调用signalAll方法去激活其他的被阻塞的线程 //如果所有的线程都在等待其他线程signalAll,则进入死锁
非常不妙的,如果所有的线程都在等待其他线程signalAll,则进入死锁的状态。死锁状态是指所有的线程需要的资源都被其他的线程形成环状结构而导致谁都不能执行的情况。最后调用signalAll方法激活其他因为cd而阻塞的“兄弟”是必须的,方便你我他,减少死锁的发生。
总结来说,Condition对象和锁有这样几个特点。
锁可以用来保护代码片段,任何时刻只能有一个线程进入被保护的区域
锁可以管理试图进入临界区的线程
锁可以拥有一个或多个条件对象
每个条件对象管理那些因为前面所描述的原因而不能被执行但已经进入被保护代码段的线程
我们上面介绍的ReentrantLock和Condition对象是一种用来保护代码片段的方法,在java中还有另外一种机制:通过使用关键字synchronized来修饰方法,从而给方法添加一个内部锁。从版本开始,java的每一个对象都有一个内部锁,每个内部锁会保护那些被synchronized修饰的方法。也就是说,如果想调用这个方法,首先要获得内部的对象锁。
我们先拿出上面的代码:
public void function(){ ReentrantLock myLock = new ReentrantLock(); myLock.lock(); Condition cd = myLock.newCondition(); while(!(a>b)) cd.await(); a.set(b-1); cd.signalAll(); }
如果我们用synchronized来实现这段代码,将会变成下面的样子:
public synchronized void function(){ while(!(a>b)) wait(); a.set(b-1); notifyAll(); }
需要我们注意的是,在使用synchronized关键词时,无需再去用ReentrantLock和Condition对象,我们用wait方法替换了await方法,notifyAll方法替换了signalAll方法。这样写确实比之前的简单了很多。
将静态方法声明为synchronized也是合法的。如果调用这种方法,将会获取相关的类对象的内部锁。比如我们调用Test类中的静态方法,这时,Test.class对象的锁将被锁住。
内部锁虽然简便,但是他存在着很多限制:
不能中断一个正在试图获得锁的线程
试图获得锁时不能设定超时
因为不能通过Condition来实例化条件。每个锁仅有单一的条件,可能是不够的
在代码中应该使用这两种锁中的哪一种呢?Lock和Condition对象还是同步方法?在core java一书中有一些建议:
最好既不使用ReentrantLock也不使用synchronized關鍵字。在許多情況下你可以使用java.util.concurrent套件
如果synchronized符合你的程式碼需要,請優先使用它
直到如果特別需要ReentrantLcok,再去使用它
為了加快程式碼的運行速度,我們採用了多執行緒的方法。並行的執行確實讓程式碼變得更有效率,但隨之而來的問題是,有很多個執行緒在程式中同時運行,如果它們同時的去修改一個對象,很可能會造成謳誤的情況,這個時候我們需要用一種同步的機制來管理這些執行緒。
以上是java線程(二)—線程同步詳解的內容,更多相關內容請關注PHP中文網(www.php.cn)!