1. 程式人生 > >Java多執行緒01_可重入函式、可重入鎖

Java多執行緒01_可重入函式、可重入鎖

測試環境

OS:windows7_X64
JDK:jdk1.8.0_20
IDE: eclipse_neon

一、可重入函式

相信很多人都聽說過可重入函式,可重入函式最重要的兩條法則就是:

  1. 只使用非靜態區域性變數;
  2. 不呼叫不可重入的函式。
public class Reentrant1 {
    private int count = 0;
    /**
     * 不可重入
     * 當有多個執行緒呼叫同一個Reentrant物件的increament(),輸出數值是不可預測的。count可能是1,也可能是任何正整數。
     */
    public void increament
()
{ count ++; System.out.println(count); } /** * 可重入 * 無論多少執行緒呼叫同一個Reentrant對的象decreament方法,輸出數值都是傳入引數length的值減1。 */ public void decreament(int length){ length--; System.out.println(length); } }
如上程式碼所述,increament是不可重入的函式,decreament是可重入的函式。區別在於一個引用了例項變數,一個未引用例項變數。
不可重入函式被視為不安全的函式,無狀態的函式則是執行緒安全的。在spring 的IOC容器中,預設為單例模式,所以在程式設計時要儘量使用無狀態的類和方法。如像incerement
()
這樣類似的方法,為了得到期望的值,則應該每次訪問都新建一個例項物件,也就是要在類上標記@Scope(prototype)
二、可重入鎖
說完了可重入函式,那麼再來看看可重入鎖。
可重入鎖的意思是當同一個執行緒獲取鎖後,在未釋放鎖的情況下,多次獲取同一個鎖物件並不會發生死鎖現象。
public class Reentrant2 {

    public static void main(String[] args){
        Reentrant2 rt2 = new Reentrant2();
        rt2.getCount("a", 1);
    }
	
    public
void getCount(String str, int count){ // 第一次獲取鎖 synchronized (str.intern()) { System.out.println(count); count++; // 第二次獲取鎖 synchronized (str.intern()) { System.out.println(count); count++; } } } }
執行Reentrant2的輸出結果為:
1
2
getCount方法中第一次獲取鎖之後在沒有釋放鎖的情況下,第二次獲取同一個字串物件的鎖,並沒有發生死鎖現象,因此使用synchronized修飾的是可重入鎖。

遞迴呼叫進行同步必須使用可重入的鎖,如下程式碼所示:
public class Reentrant3 {
    public static void main(String[] args){
        Reentrant3 rt3 = new Reentrant3();
        new Thread(rt3.new ReentrantInner()).start();
        new Thread(rt3.new ReentrantInner()).start();
    }
    
    class ReentrantInner implements Runnable{
        public int getCount(String str, int count) throws InterruptedException{
            synchronized (str.intern()) {
                if(count>5){
                    return count;
                }
                System.out.println(Thread.currentThread().getId() + "==" + count);
                count++;
                Thread.sleep(100);
                //遞迴呼叫
                return getCount(str, count);
            }
        }
        
        @Override
        public void run() {
            try {
                this.getCount(new String("a"), 1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
執行Reentrant3的輸出結果為:
10==1
10==2
10==3
10==4
10==5
9==1
9==2
9==3
9==4
9==5
可以看到與預期結果一致,並沒有發生死鎖。另外,我想有些人一定注意到了我使用的是new String("a"),同步時又呼叫了str.intern()方法。因為在實際生產環境中每一個String字串物件都可能是由不同的執行緒產生,即使字串的值完全相同,也可能不是同一個物件。因此需要呼叫intern()方法,保證相同值的字串均引用的是同一物件。
synchronized是根據物件進行加鎖,如果不是同一個物件,那麼鎖就會失效。各位可自行去掉intern()方法試試,輸出順序肯定會發生變化。
至於為什麼呼叫intern()返回的會是同一個物件?請參閱String類的api。
三、其它可重入鎖
除了使用synchronized以外,ReentrantLock和ReentrantReadWriteLock也是可重入鎖,具體的api請自行查閱,這裡不再詳談。ReentrantLock的遞迴實現如下程式碼所示:
import java.util.concurrent.locks.ReentrantLock;

public class Reentrant4 {
	
	private final ReentrantLock lock = new ReentrantLock();
	
	public static void main(String[] args){
		Reentrant4 rt4 = new Reentrant4();
		new Thread(rt4.new ReentrantInner()).start();
		new Thread(rt4.new ReentrantInner()).start();
	}
	
	class ReentrantInner implements Runnable{
		
		public int getCount(String str, int count){
			lock.lock();
			try{
				if(count>5){
					return count;
				}
				System.out.println(Thread.currentThread().getId() + "==" + count);
				count++;
				Thread.sleep(100);
				return getCount(str, count);
			}catch(Exception e){
				e.printStackTrace();
				return count;
			}finally{
				lock.unlock();
			}
		}

		@Override
		public void run() {
			this.getCount("a", 1);
		}
	}
	
}
執行Reentrant4的輸出結果為:
9==1
9==2
9==3
9==4
9==5
10==1
10==2
10==3
10==4
10==5
可以看到與預期結果一致,並沒有發生死鎖。

四、實現自己的可重入鎖

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class MyReentrantLock implements Lock {

    private AtomicReference<Thread> owner = new AtomicReference<Thread>();
	
    private int count = 0;

    @Override
    public void lock() {
        Thread current = Thread.currentThread();	        //獲取當前執行緒
        if (current == owner.get()) {				            //如果當前執行緒是持有鎖的執行緒,count計數+1
            count++;
            System.out.println("lock重入次數:"+count);
            return;
        }
        /*
         * 1.如果持有鎖的執行緒為空,將當前執行緒設定為持有鎖
         * 2.如果持有鎖的執行緒不為空,且並非當前執行緒,全部進入休眠狀態100毫秒
         */
        while (!owner.compareAndSet(null, current)) {	
        	try {
        		Thread.sleep(100);
        	} catch (InterruptedException e) {
        		e.printStackTrace();
        	}
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
    }
    
    @Override
    public Condition newCondition() {
    	return null;
    }
    
    @Override
    public boolean tryLock() {
    	return false;
    }

    @Override
    public boolean tryLock(long arg0, TimeUnit arg1) throws InterruptedException {
    	return false;
    }
    
    @Override
    public void unlock() {
    	Thread current = Thread.currentThread();
    	/*
    	 * 1.如果當前執行緒為持有鎖的執行緒:
    	 * 如果count不為0,說明當前執行緒至少獲得過一次鎖,count--;
    	 * 如果count為0,說明當前執行緒為最後一次釋放鎖,將持有鎖的執行緒設定為空
    	 */
    	if (current == owner.get()) {
    		if(count > 0){
    			count--;
    		}else{
    			owner.compareAndSet(current, null);
    		}
    	}
    	System.out.println("unlock重入次數:"+count+ "ThreadId:"+Thread.currentThread().getId());
    }

}
實際開發時還要考慮鎖超時,鎖的效率,中斷異常等等因素,此程式碼僅供參考。

五、後記

本文源於最近在玩的一個專案,發現對可重入函式和可重入鎖的概念並不是特別清晰,於是看api看書看部落格,有些心得,所以發出來分享。
如果您有更好的理解或者文中有不對的地方,歡迎留言探討補充指正。謝謝。