JAVA線程死鎖
Java線程死鎖發(fā)生的常見行為是雙方互相持有對方需要的鎖:即兩個(gè)或多個(gè)線程在等待彼此持有的鎖,導(dǎo)致所有線程都無法繼續(xù)執(zhí)行下去。這種情況通常會產(chǎn)生一個(gè)循環(huán)等待的場景。
舉例代碼:
public class DeadlockExample {
private static Object lockA = new Object();
private static Object lockB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockA) {
System.out.println("Thread 1 acquired lock A");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println("Thread 1 acquired lock B");
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockB) {
System.out.println("Thread 2 acquired lock B");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockA) {
System.out.println("Thread 2 acquired lock A");
}
}
}
});
thread1.start();
thread2.start();
}
}
為避免死鎖,可以采取以下措施:
- 避免嵌套鎖
- 使用同步塊代替同步方法
- 按照固定順序獲取鎖
- 可以使用Lock對象代替synchronized關(guān)鍵字
- 使用Semaphore避免死鎖
- 使用ThreadLocal為每個(gè)線程提供獨(dú)立的變量副本,避免多個(gè)線程之間共享變量導(dǎo)致的死鎖問題
- 避免長時(shí)間持有鎖,盡可能減少持有鎖的時(shí)間
- 使用非阻塞算法或無鎖算法
修改上述代碼
使用ReentrantLock
使用ReentrantLock時(shí),需要將所有涉及到共享資源的代碼放在lock()和unlock()方法之間,才能保證線程安全。在之前的代碼示例中,if代碼塊沒有被包括在lock()和unlock()方法之間,因此不具備線程安全性。
以下是修改后的示例代碼:
public class DeadlockExample {
private static final ReentrantLock lockA = new ReentrantLock();
private static final ReentrantLock lockB = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
lockA.lock();
System.out.println("Thread 1 acquired lock A");
Thread.sleep(100);
if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) {
System.out.println("Thread 1 acquired lock B");
Thread.sleep(100);
} else {
System.out.println("Thread 1 failed to acquire lock B, aborting");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lockB.isHeldByCurrentThread()) {
lockB.unlock();
System.out.println("Thread 1 released lock B");
}
lockA.unlock();
System.out.println("Thread 1 released lock A");
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
lockB.lock();
System.out.println("Thread 2 acquired lock B");
Thread.sleep(100);
if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) {
System.out.println("Thread 2 acquired lock A");
Thread.sleep(100);
} else {
System.out.println("Thread 2 failed to acquire lock A, aborting");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lockA.isHeldByCurrentThread()) {
lockA.unlock();
System.out.println("Thread 2 released lock A");
}
lockB.unlock();
System.out.println("Thread 2 released lock B");
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
以上代碼中,在每個(gè)線程的run方法中,我們依次獲取兩把鎖,并在獲取到鎖之后進(jìn)行相應(yīng)的操作。在釋放鎖時(shí),需要先判斷當(dāng)前線程是否持有該鎖,如果是,則釋放該鎖,并打印相應(yīng)的信息。
這樣修改后,就能保證線程安全和避免死鎖的問題了。
使用Semaphore
以下是修改后的示例代碼:
public class DeadlockExample {
private static final Semaphore semaphoreA = new Semaphore(1);
private static final Semaphore semaphoreB = new Semaphore(1);
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
semaphoreA.acquire();
System.out.println("Thread 1 acquired semaphore A");
Thread.sleep(100);
while (!semaphoreB.tryAcquire()) {
semaphoreA.release();
System.out.println("Thread 1 released semaphore A and waiting for semaphore B");
semaphoreA.acquire();
System.out.println("Thread 1 acquired semaphore A again");
}
System.out.println("Thread 1 acquired semaphore B");
Thread.sleep(100);
semaphoreB.release();
System.out.println("Thread 1 released semaphore B");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphoreA.release();
System.out.println("Thread 1 released semaphore A");
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
semaphoreB.acquire();
System.out.println("Thread 2 acquired semaphore B");
Thread.sleep(100);
while (!semaphoreA.tryAcquire()) {
semaphoreB.release();
System.out.println("Thread 2 released semaphore B and waiting for semaphore A");
semaphoreB.acquire();
System.out.println("Thread 2 acquired semaphore B again");
}
System.out.println("Thread 2 acquired semaphore A");
Thread.sleep(100);
semaphoreA.release();
System.out.println("Thread 2 released semaphore A");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphoreB.release();
System.out.println("Thread 2 released semaphore B");
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
以上代碼中,如果全部使用acquire(),那么在當(dāng)前線程獲取到第一個(gè)資源后,如果無法獲取第二個(gè)資源,就會釋放第一個(gè)資源,并進(jìn)入等待狀態(tài)。然而,由于此時(shí)其他線程可能已經(jīng)獲取了第一個(gè)資源并在等待第二個(gè)資源,因此當(dāng)前線程可能永遠(yuǎn)無法獲取到第二個(gè)資源,從而導(dǎo)致死鎖的發(fā)生。
為了避免這種情況,可以使用tryAcquire()方法來嘗試獲取第二個(gè)資源,如果無法獲取,則先釋放第一個(gè)資源再進(jìn)行等待。
這樣修改后,就能保證線程安全和避免死鎖的問題了。
Java線程饑餓
Java中線程饑餓(Thread Starvation)是指某個(gè)或某些線程無法獲得所需的資源,從而無法繼續(xù)執(zhí)行下去,導(dǎo)致程序出現(xiàn)假死等異常情況。
舉個(gè)例子,如果一個(gè)高優(yōu)先級的線程一直占用某個(gè)共享資源,并且低優(yōu)先級的線程總是無法獲取該資源,那么這些低優(yōu)先級的線程就會一直處于等待狀態(tài),無法執(zhí)行下去,從而導(dǎo)致線程饑餓的發(fā)生。
另外,線程饑餓也可能發(fā)生在多個(gè)線程競爭同一資源時(shí),如果某些線程總是能夠比其他線程更快地獲取到該資源,那么其他線程就會一直處于等待狀態(tài),無法及時(shí)完成任務(wù),從而導(dǎo)致線程饑餓的發(fā)生。
為避免線程饑餓的發(fā)生,可以采取以下措施:
- 合理設(shè)計(jì)線程優(yōu)先級,盡量保證每個(gè)線程都能有機(jī)會獲取到所需的資源。
- 使用公平鎖來保證資源的公平分配,避免某個(gè)線程長期占用某個(gè)資源。
- 盡量減少線程等待的時(shí)間,例如使用超時(shí)機(jī)制、避免死鎖等方式來盡早釋放資源,讓其他線程有機(jī)會獲取到資源。
- 對于一些不必要占用大量資源的線程,可以將其設(shè)置為低優(yōu)先級或者使用線程池等方式來限制其數(shù)量,從而避免線程饑餓的發(fā)生。
總之,在編寫多線程程序時(shí)需要注意線程饑餓問題,并采取相應(yīng)措施來保證程序的正常執(zhí)行。
以下是一個(gè)簡單的代碼示例,模擬了線程饑餓的情況。
public class ThreadStarvationExample {
private static final Object lock = new Object();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
final int index = i;
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("Thread " + index + " acquired lock");
try {
// 模擬某些線程需要占用鎖較長時(shí)間
if (index == 2 || index == 3) {
Thread.sleep(5000);
} else {
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread " + index + " released lock");
}
}
}).start();
}
}
}
以上代碼中,我們創(chuàng)建了10個(gè)線程,并對它們共享的一個(gè)鎖進(jìn)行競爭。其中,第2個(gè)線程和第3個(gè)線程分別需要占用鎖5秒鐘和其他線程相比更久的時(shí)間。
如果運(yùn)行以上代碼,我們可能會發(fā)現(xiàn)第2個(gè)線程和第3個(gè)線程總是優(yōu)先獲得鎖,而其他線程則會等待較長時(shí)間才能獲取到鎖,從而導(dǎo)致這些線程在整個(gè)程序執(zhí)行期間都無法正常執(zhí)行下去,出現(xiàn)線程饑餓的情況。
為避免線程饑餓的發(fā)生,我們可以對占用鎖時(shí)間較長的線程做出調(diào)整,例如將它們設(shè)置為低優(yōu)先級或者減少其持有鎖的時(shí)間等措施。
另外,我們還可以使用公平鎖來保證資源的公平分配,避免某個(gè)線程長期占用某個(gè)資源。以下是修改后的代碼示例:
public class ThreadStarvationExample {
private static final ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
final int index = i;
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("Thread " + index + " acquired lock");
try {
if (index == 2 || index == 3) {
// 將占用鎖時(shí)間較長的線程設(shè)置為低優(yōu)先級
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
Thread.sleep(5000);
} else {
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 減少持有鎖的時(shí)間,增加其他線程獲取鎖的機(jī)會
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread " + index + " released lock");
}
}
}
}).start();
}
}
}