Java多線程 – 線程同步
一、線程安全問題
在多線程編程中,極容易出現一個問題,即線程安全。舉個栗子~,如果此時火車站有十張餘票,四個售票口,此時四個售票口相當於四個線程,他們一同運轉,賣票,那麼就會出現一個很經典的安全問題。
示例代碼:
class demo{
public static void main(String[] args) throws InterruptedException {
Train train = new Train();
Train train1 = new Train();
Train train2 = new Train();
Train train3 = new Train();
train.start();
train1.start();
train2.start();
train3.start();
}
}
class Train extends Thread{
private static int count = 10;
@Override
public void run(){
while (true){
try{
Thread.sleep(500);
boolean flag = sale();
if(!flag)
break;
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public boolean sale(){
if(count > 0){
--count;
System.out.println( Thread.currentThread().getName() + "售出一張,剩餘:"+count);
return true;
}else{
return false;
}
}
}
大家可以運行一下以上的程序,會發現有的時候售出的票不只是10張,這正是線程調度的不確定性,
二、同步代碼塊
之所以出現以上的情況,是因爲run()
方法的方法體不具有同步安全性,當多個線程同步修改同一變量的時候容易出現以上情況,爲了解決這個尷尬的問題,Java的多線程支持引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步代碼塊。
synchronized(obj){
//此處代碼就是同步代碼塊
}
注意:任何時刻都只能擁有一個線程獲得對同步監視器的鎖定,當同步代碼塊執行完成之後,該線程就會釋放對同步監視器的鎖定。
class demo{
public static void main(String[] args) throws InterruptedException {
Train train = new Train();
Train train1 = new Train();
Train train2 = new Train();
Train train3 = new Train();
train.start();
train1.start();
train2.start();
train3.start();
}
}
class Train extends Thread{
private static int count = 10;
@Override
public void run(){
//獲取類鎖
synchronized(Train.class){
while (true){
try{
Thread.sleep(500);
boolean flag = sale();
if(!flag)
break;
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public boolean sale(){
if(count > 0){
--count;
System.out.println( Thread.currentThread().getName() + "售出一張,剩餘:"+count);
return true;
}else{
return false;
}
}
}
三、同步方法
與同步代碼塊相對應,Java的多線程安全支持,還提供了同步方法,同步方法就是使用synchronized
修飾的實例方法(非static
方法),則該方法稱爲同步方法,對於synchronized
修飾的實例方法,無需顯式指定同步監視器,同步方法的同步監視器是this
,也就是調用該方法的對象。
通過同步方法可以非常方便地實現線程安全的類。
線程安全的類有以下的特徵:
1. 該類的對象可以被多個線程安全的訪問。
2. 每個線程調用該對象的任意方法之後都將得到正確結果。
3. 每個線程調用該對象的任意方法之後,該對象狀態依然保持合理狀態。
注意:synchronized
可以修飾方法和代碼塊,但不能修飾構造器和成員變量等
四、釋放同步監視器的鎖定
任何線程進入同步代碼塊或同步方法前,都必須先獲得對同步監視器的鎖定,那麼何時會釋放對同步監視器的鎖定呢?程序無法顯示釋放對同步監視器的鎖定,而是在以下這些情況釋放對同步監視器的鎖定:
1. 當前線程的同步方法、同步代碼執行結束,當前線程即釋放同步監視器。
2. 當前線程在同步代碼塊、同步方法中遇到了break
,return
終止了該代碼塊、該方法的繼續執行,當前線程將會釋放同步監視器。
3. 當前線程在同步代碼塊、同步方法之中出現了未處理的error
或Exception
,導致了該代碼塊,該方法異常結束的時候,當前線程將會釋放同步監視器。
4. 當前線程執行同步代碼塊或同步方法時,程序執行了同步監視器對象的wait()
方法,則當前線程暫停,並且釋放同步監視器。
以下情況,線程不會釋放同步監視器:
1. 線程執行同步代碼塊或者同步方法的時候,程序調用Thread.sleep()
、Thread.yield()
方法來暫停當前線程的執行,但是當前線程並不會釋放同步監視器。
2. 線程執行同步代碼塊的時候,其他的線程調用該線程的suspend()
方法將其掛起,該線程不會釋放同步監視器,當然,需要注意的是:程序中儘量少使用suspend()
和resume()
方法來控制線程。
五、同步鎖
從Java 5開始,Java提供了一種功能更強大的線程同步機制 – 通過顯示定義同步鎖對象來實現同步。
Lock
提供了比synchronized
方法和代碼塊更廣泛的鎖定操作,Lock
允許實現更加靈活的結構,可以具有差別很大的屬性,並且支持多個相關的Condition
對象。
Lock
是控制多個線程對共享資源進行訪問的工具。通常,所提供了對共享資源的獨佔訪問,每次都只能有一個線程對Lock
對象枷鎖,線程開始訪問共享資源之前,應該先獲得Lock
對象。
某些所可能允許對共享資源併發操作,例如說:ReadWriteLock
(讀寫鎖),Lock
和ReadWriteLock
是Java 5提供的兩個根接口,且爲Lock
提供了ReentrantLock
(可重入鎖)實現類,爲ReadWreiteLock
提供了ReentrantReadWriteLock
實現類。
Java 8新增了新型的StampedLock
類,在大多數場景中他可以替代傳統的ReentrantreadWriteLock
。ReentrantreadWriteLock
爲讀寫操作提供了三種鎖模式:Writing
、ReadingOptimisitic
、Reading
。
在實現線程安全的控制之中,較爲常用的是ReentrantLock
(可重入鎖)。使用該Lock
對象可以顯式地添加所和釋放鎖。
import java.util.concurrent.locks.ReentrantLock;
class demo{
//定義鎖對象
private final ReentrantLock lock = new ReentrantLock();
//定義需要保證線程安全的方法
public void m(){
//加鎖
lock.lock();
try{
//需要保證線程安全的代碼
}
//使用finally塊來釋放鎖
finally {
//解鎖
lock.unlock();
}
}
}
使用ReentrantLock
對象進行同步,加鎖和釋放鎖均出現在不同的額作用範圍之內,通常建議使用finally
塊來確保在必要的時候釋放鎖,通常使用ReentrantLock
對象。
六、避免死鎖
當兩個線程相互等待對方釋放同步監視器就會發生死鎖,其中Java虛擬機中沒有監測,也沒有采取措施處理死鎖情況,所以多線程編程中應當精良避免死鎖出現,一旦出現死鎖,整個程序既不會發生任何異常,也不會有任何提示,只是所有線程處於阻塞狀態,無法繼續。
死鎖不應該在程序中出現,編寫時應該儘量避免死鎖。
1. 避免多次鎖定,儘量避免同一個線程對多個同步監視器進行鎖定
2. 具有相同的加鎖順序,如果多個線程對多個同步監視器進行鎖定,則應該報這個它們以相同的順序請求加鎖。
3. 採用定時所,程序調用Lock
對象的truLock()
方法加鎖時,可以指定time
和unit
參數,當超過指定時間就會自動釋放對Lock
的鎖定。
4. 死鎖檢測,這是依靠算法進行實現的死鎖預防措施,主要針對那些不可能實現按序加鎖,也不可能使用定時鎖的場景。
//會產生死鎖的代碼:
class A{
public synchronized void foo(B b){
System.out.println("線程名:" + Thread.currentThread().getName() + "進入A實例的foo方法");
try{
Thread.sleep(200);
}
catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("線程名:" + Thread.currentThread().getName() + "企圖調用B實例的Last方法");
b.last();
}
public synchronized void last(){
System.out.println("進入A類的last方法內部");
}
}
class B{
public synchronized void foo(A a){
System.out.println("線程名:" + Thread.currentThread().getName() + "進入A實例的foo方法");
try{
Thread.sleep(200);
}
catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("線程名:" + Thread.currentThread().getName() + "企圖調用B實例的Last方法");
a.last();
}
public synchronized void last(){
System.out.println("進入B類的last方法內部");
}
}
public class demo implements Runnable{
A a = new A();
B b = new B();
public void init(){
Thread.currentThread().setName("主線程");
a.foo(b);
System.out.println("進入主線程之後");
}
@Override
public void run(){
Thread.currentThread().setName("副線程");
b.foo(a);
System.out.println("進入副線程");
}
public static void main(String[] args) {
demo d = new demo();
new Thread(d).start();
d.init();
}
}