《Java线程与并发编程实践》—— 2.3 谨防活跃性问题

简介: 活跃性这个词代表着某件正确的事情最终会发生。活跃性失败发生在应用程序触及一种无法继续执行的状态。在单线程的应用程序中,无限循环就是一个例子。多线程应用程序面临着诸如死锁、活锁和饿死的额外挑战。

本节书摘来异步社区《Java线程与并发编程实践》一书中的第2章,第2.3节,作者: 【美】Jeff Friesen,更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.3 谨防活跃性问题

活跃性这个词代表着某件正确的事情最终会发生。活跃性失败发生在应用程序触及一种无法继续执行的状态。在单线程的应用程序中,无限循环就是一个例子。多线程应用程序面临着诸如死锁、活锁和饿死的额外挑战。

死锁:线程1等待线程2互斥持有的资源,而线程2也在等待线程1互斥持有的资源。两条线程都无法继续执行。
活锁:线程x持续重试一个总是失败的操作,以致于无法继续执行。
饿死:线程x一直被(调度器)延迟访问其赖以执行的资源。或许是调度器先于低优先级的线程执行高优先级的线程,而总是有一个高优先级的线程可以执行。饿死通常也称为无限延迟。
死锁会发生在synchronized关键字带来的过多同步上。如果不小心,你可能就会遭遇锁同时被多条线程竞争的情形;即线程自身缺失继续执行的锁,却持有其他线程需要的锁,同时由于其他线程持有临界区的锁,导致没有一条线程能够通过临界区,进而释放自己所持有的锁。清单2-1就是描述该场景的一个典型例子。

清单2-1 一个死锁的问题

public class DeadlockDemo
{
   private final Object lock1 = new Object();
   private final Object lock2 = new Object();

   public void instanceMethod1()
   {
      synchronized(lock1)
      {
         synchronized(lock2)
         {
            System.out.println("first thread in instanceMethod1");
            // critical section guarded first by
            // lock1 and then by lock2
         } 
      }
   }
   public void instanceMethod2()
   {
      synchronized(lock2)
      {
         synchronized(lock1)
         {
            System.out.println("second thread in instanceMethod2");
            // critical section guarded first by
            // lock2 and then by lock1
         }
      }
   }

   public static void main(String[] args)
   {
      final DeadlockDemo dld = new DeadlockDemo();
      Runnable r1 = new Runnable()
                    {
                       @Override
                       public void run()
                       {
                          while(true)
                          {
                             dld.instanceMethod1();
                          try
                          {
                             Thread.sleep(50);
                          }
                          catch (InterruptedException ie)
                          {
                          }
                       } 
                    }
                 };
   Thread thdA = new Thread(r1);
   Runnable r2 = new Runnable()
                 {
                    @Override
                    public void run()
                    {
                       while(true)
                       {
                          dld.instanceMethod2();
                          try
                          {
                             Thread.sleep(50);
                          }
                             catch (InterruptedException ie)
                             {
                             }
                          }
                       }
                    };
      Thread thdB = new Thread(r2);
      thdA.start();
      thdB.start();
   }
}```
清单2-1中线程A和线程B在不同的时间分别调用了instanceMethod1()和instanceMethod2()方法。参考下面的执行序列:

(1)线程A调用instanceMethod1(),获取到lock1引用对象的锁,然后进入它外部的临界区(但是还没有获取lock2引用对象的锁)。

(2)线程B调用instanceMethod2(),获取到lock2引用对象的锁,然后进入它外部的临界区(但是还没有获取lock1引用对象的锁)。

(3)线程A尝试去获取和lock2相关联的锁。JVM强制线程在内部临界区之外等待,由于线程B持有那个锁。

(4)线程B尝试去获取和lock1相关联的锁。JVM强制线程在内部临界区之外等待,由于线程A持有那个锁。

(5)由于其他线程持有了必要的锁,没有一条线程能继续执行。遭遇死锁,程序(至少在这两条线程的上下文中)就冻结住了。

照下面那样编译清单2-1:

javac DeadlockDemo.java
运行程序:

java DeadlockDemo`
你应该能在标准输出流中观测到交替打印的first thread in instance Method1和second thread in instanceMethod2信息,直到程序因死锁而冻结。

尽管前面的例子很清晰地识别了死锁的状态,但侦测死锁还是不容易。举个例子,你的代码可能在多个类中(在多个源文件里)包含如下的环形关系:

类A的同步方法调用了类B的同步方法。
类B的同步方法调用了类C的同步方法。
类C的同步方法调用了类A的同步方法。
如果线程A调用了类A的同步方法,线程B调用了类C的同步方法,因为线程A还在那个方法当中,当线程B尝试调用类A的同步方法时,会被阻塞住。线程A会继续执行直至其调用类C的同步方法,然后阻塞住。死锁发生了。

注意:
  >Java语言和JVM都没有提供一种方式来避免死锁,所以这一任务就落到你的身上。避免死锁最简单的方式,就是阻止同步方法或者同步块调用其他的同步方法和同步块。尽管这个建议能避免死锁发生,但还是不现实,因为,在你的某一个同步方法和同步块中,很可能需要调用Java API中的同步方法,而且这个建议有点因噎废食,因为被调用的同步方法和同步块很可能不会调用其他的同步方法和同步块,从而不会导致死锁发生。

相关文章
|
1天前
|
安全 Java
java多线程(一)(火车售票)
java多线程(一)(火车售票)
|
2天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
2天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。
|
2天前
|
缓存 分布式计算 监控
Java并发编程:深入理解线程池
【4月更文挑战第17天】在Java并发编程中,线程池是一种非常重要的技术,它可以有效地管理和控制线程的执行,提高系统的性能和稳定性。本文将深入探讨Java线程池的工作原理,使用方法以及在实际开发中的应用场景,帮助读者更好地理解和使用Java线程池。
|
3天前
|
负载均衡 Java 开发者
细解微服务架构实践:如何使用Spring Cloud进行Java微服务治理
【4月更文挑战第17天】Spring Cloud是Java微服务治理的首选框架,整合了Eureka(服务发现)、Ribbon(客户端负载均衡)、Hystrix(熔断器)、Zuul(API网关)和Config Server(配置中心)。通过Eureka实现服务注册与发现,Ribbon提供负载均衡,Hystrix实现熔断保护,Zuul作为API网关,Config Server集中管理配置。理解并运用Spring Cloud进行微服务治理是现代Java开发者的关键技能。
|
3天前
|
Java API 数据库
深研Java异步编程:CompletableFuture与反应式编程范式的融合实践
【4月更文挑战第17天】本文探讨了Java中的CompletableFuture和反应式编程在提升异步编程体验上的作用。CompletableFuture作为Java 8引入的Future扩展,提供了一套流畅的链式API,简化异步操作,如示例所示的非阻塞数据库查询。反应式编程则关注数据流和变化传播,通过Reactor等框架实现高度响应的异步处理。两者结合,如将CompletableFuture转换为Mono或Flux,可以兼顾灵活性和资源管理,适应现代高并发环境的需求。开发者可按需选择和整合这两种技术,优化系统性能和响应能力。
|
1月前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【2月更文挑战第22天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个主题,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。
15 0
|
17天前
|
安全 Java 容器
Java并发编程:实现高效、线程安全的多线程应用
综上所述,Java并发编程需要注意线程安全、可见性、性能等方面的问题。合理使用线程池、同步机制、并发容器等工具,可以实现高效且线程安全的多线程应用。
14 1
|
27天前
|
安全 Java 开发者
Java并发编程中的线程安全性探究
在Java编程中,线程安全性是一个至关重要的问题,涉及到多线程并发访问共享资源时可能出现的数据竞争和不一致性问题。本文将深入探讨Java并发编程中的线程安全性,介绍常见的线程安全性问题以及解决方法,帮助开发者更好地理解和应对在多线程环境下的挑战。
|
28天前
|
安全 Java 开发者
Java并发编程中的线程安全性探究
在Java开发中,多线程编程是一项常见且重要的技术。本文将深入探讨Java并发编程中的线程安全性问题,从数据竞争到同步机制的选择,帮助开发者更好地理解和应对多线程环境下的挑战。
13 2