为什么线程饥饿比线程死锁更加可怕?
导语:
Java线程死锁
Java线程死锁发生的常见行为是双方互相持有对方需要的锁:即两个或多个线程在等待彼此持有的锁,导致所有线程都无法继续执行下去。这种情况通常会产生一个循环等待的场景。
举例代码:
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(&34;); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockB) { System.out.println(&34;); } } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { synchronized (lockB) { System.out.println(&34;); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockA) { System.out.println(&34;); } } } }); thread1.start(); thread2.start(); }}
为避免死锁,可以采取以下措施:
避免嵌套锁使用同步块代替同步方法按照固定顺序获取锁可以使用Lock对象代替synchronized关键字使用Semaphore避免死锁使用ThreadLocal为每个线程提供独立的变量副本,避免多个线程之间共享变量导致的死锁问题避免长时间持有锁,尽可能减少持有锁的时间使用非阻塞算法或无锁算法修改上述代码使用ReentrantLock
使用ReentrantLock时,需要将所有涉及到共享资源的代码放在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(&34;); Thread.sleep(100); if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) { System.out.println(&34;); Thread.sleep(100); } else { System.out.println(&34;); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lockB.isHeldByCurrentThread()) { lockB.unlock(); System.out.println(&34;); } lockA.unlock(); System.out.println(&34;); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { try { lockB.lock(); System.out.println(&34;); Thread.sleep(100); if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) { System.out.println(&34;); Thread.sleep(100); } else { System.out.println(&34;); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lockA.isHeldByCurrentThread()) { lockA.unlock(); System.out.println(&34;); } lockB.unlock(); System.out.println(&34;); } } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); }}
以上代码中,在每个线程的run方法中,我们依次获取两把锁,并在获取到锁之后进行相应的操作。在释放锁时,需要先判断当前线程是否持有该锁,如果是,则释放该锁,并打印相应的信息。
这样修改后,就能保证线程安全和避免死锁的问题了。
使用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(&34;); Thread.sleep(100); while (!semaphoreB.tryAcquire()) { semaphoreA.release(); System.out.println(&34;); semaphoreA.acquire(); System.out.println(&34;); } System.out.println(&34;); Thread.sleep(100); semaphoreB.release(); System.out.println(&34;); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphoreA.release(); System.out.println(&34;); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { try { semaphoreB.acquire(); System.out.println(&34;); Thread.sleep(100); while (!semaphoreA.tryAcquire()) { semaphoreB.release(); System.out.println(&34;); semaphoreB.acquire(); System.out.println(&34;); } System.out.println(&34;); Thread.sleep(100); semaphoreA.release(); System.out.println(&34;); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphoreB.release(); System.out.println(&34;); } } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); }}
以上代码中,如果全部使用acquire(),那么在当前线程获取到第一个资源后,如果无法获取第二个资源,就会释放第一个资源,并进入等待状态。然而,由于此时其他线程可能已经获取了第一个资源并在等待第二个资源,因此当前线程可能永远无法获取到第二个资源,从而导致死锁的发生。
为了避免这种情况,可以使用tryAcquire()方法来尝试获取第二个资源,如果无法获取,则先释放第一个资源再进行等待。
这样修改后,就能保证线程安全和避免死锁的问题了。
Java线程饥饿Java中线程饥饿(Thread Starvation)是指某个或某些线程无法获得所需的资源,从而无法继续执行下去,导致程序出现假死等异常情况。
举个例子,如果一个高优先级的线程一直占用某个共享资源,并且低优先级的线程总是无法获取该资源,那么这些低优先级的线程就会一直处于等待状态,无法执行下去,从而导致线程饥饿的发生。
另外,线程饥饿也可能发生在多个线程竞争同一资源时,如果某些线程总是能够比其他线程更快地获取到该资源,那么其他线程就会一直处于等待状态,无法及时完成任务,从而导致线程饥饿的发生。
为避免线程饥饿的发生,可以采取以下措施:
合理设计线程优先级,尽量保证每个线程都能有机会获取到所需的资源。使用公平锁来保证资源的公平分配,避免某个线程长期占用某个资源。尽量减少线程等待的时间,例如使用超时机制、避免死锁等方式来尽早释放资源,让其他线程有机会获取到资源。对于一些不必要占用大量资源的线程,可以将其设置为低优先级或者使用线程池等方式来限制其数量,从而避免线程饥饿的发生。总之,在编写多线程程序时需要注意线程饥饿问题,并采取相应措施来保证程序的正常执行。
以下是一个简单的代码示例,模拟了线程饥饿的情况。
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(&34; + index + &34;); try { // 模拟某些线程需要占用锁较长时间 if (index == 2 || index == 3) { Thread.sleep(5000); } else { Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(&34; + index + &34;); } } }).start(); } }}
以上代码中,我们创建了10个线程,并对它们共享的一个锁进行竞争。其中,第2个线程和第3个线程分别需要占用锁5秒钟和其他线程相比更久的时间。
如果运行以上代码,我们可能会发现第2个线程和第3个线程总是优先获得锁,而其他线程则会等待较长时间才能获取到锁,从而导致这些线程在整个程序执行期间都无法正常执行下去,出现线程饥饿的情况。
为避免线程饥饿的发生,我们可以对占用锁时间较长的线程做出调整,例如将它们设置为低优先级或者减少其持有锁的时间等措施。
另外,我们还可以使用公平锁来保证资源的公平分配,避免某个线程长期占用某个资源。以下是修改后的代码示例:
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(&34; + index + &34;); try { if (index == 2 || index == 3) { // 将占用锁时间较长的线程设置为低优先级 Thread.currentThread().setPriority(Thread.MIN_PRIORITY); Thread.sleep(5000); } else { Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } finally { // 减少持有锁的时间,增加其他线程获取锁的机会 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(&34; + index + &34;); } } } }).start(); } }}