Java多執行緒01_可重入函式、可重入鎖
阿新 • • 發佈:2019-01-07
測試環境
OS:windows7_X64
JDK:jdk1.8.0_20
IDE: eclipse_neon
一、可重入函式
相信很多人都聽說過可重入函式,可重入函式最重要的兩條法則就是:
- 只使用非靜態區域性變數;
- 不呼叫不可重入的函式。
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看書看部落格,有些心得,所以發出來分享。
如果您有更好的理解或者文中有不對的地方,歡迎留言探討補充指正。謝謝。