Java 编程问题:十、并发-线程池、可调用对象和同步器1

简介: Java 编程问题:十、并发-线程池、可调用对象和同步器

本章包括涉及 Java 并发的 14 个问题。我们将从线程生命周期以及对象级和类级锁定的几个基本问题开始。然后我们继续讨论 Java 中线程池的一系列问题,包括 JDK8 工作线程池。在那之后,我们有关于CallableFuture的问题。然后,我们将几个问题专门讨论 Java 同步器(例如,屏障、信号量和交换器)。在本章结束时,您应该熟悉 Java 并发的主要坐标,并准备好继续处理一组高级问题。

问题

使用以下问题来测试您的并发编程能力。我强烈建议您在使用解决方案和下载示例程序之前,先尝试一下每个问题:

  1. 线程生命周期状态:编写多个程序,捕捉线程的每个生命周期状态。
  2. 对象级与类级的锁定:写几个例子来举例说明通过线程同步实现对象级与类级的锁定。
  3. Java 中的线程池:简要概述 Java 中的线程池。
  4. 单线程线程池:编写一个程序,模拟一条装配线,用两个工作器来检查和打包灯泡。
  5. 固定线程数的线程池:编写一个程序,模拟一条装配线,使用多个工作器检查和打包灯泡。
  6. 缓存和调度线程池:编写一个程序,模拟装配线,根据需要使用工作器检查和打包灯泡(例如,调整打包机的数量(增加或减少)以吸收检查器产生的传入流量)。
  7. 偷工线程池:编写依赖偷工线程池的程序。更准确地说,编写一个程序,模拟一条装配线来检查和打包灯泡,如下所示:检查在白天进行,打包在晚上进行。检查过程导致每天有 1500 万只灯泡排队。
  8. CallableFuture:用CallableFuture编写模拟灯泡检查打包流水线的程序。
  9. 调用多个Callable任务:编写一个模拟装配线的程序,对灯泡进行检查和打包,如下所示:检查在白天进行,打包在晚上进行。检查过程导致每天有 100 个灯泡排队。包装过程应一次包装并归还所有灯泡。也就是说,我们应该提交所有的Callable任务,等待它们全部完成。
  10. 锁存器:编写一个依赖CountDownLatch的程序来模拟服务器的启动过程。服务器在其内部服务启动后被视为已启动。服务可以同时启动并且相互独立。
  11. 屏障:编写一个依赖CyclicBarrier来模拟服务器启动过程的程序。服务器在其内部服务启动后被视为已启动。服务可以同时启动(这很费时),但它们是相互依赖的—因此,一旦准备好启动,就必须一次启动所有服务。
  12. 交换器:编写一个程序,模拟使用Exchanger,一条由两名工作器组成的灯泡检查打包流水线。一个工作器(检查人员)正在检查灯泡,并把它们放进篮子里。当篮子装满时,工作器将篮子交给另一个工作器(包装工),他们从另一个工作器那里得到一个空篮子。这个过程不断重复,直到装配线停止。
  13. 信号量:编写一个程序,模拟每天在理发店使用一个Semaphore。我们的理发店一次最多只能接待三个人(只有三个座位)。当一个人到达理发店时,他们试着坐下。理发师为他们服务后,这个人就把座位打开。如果一个人在三个座位都坐满的时候到达理发店,他们必须等待一定的时间。如果这段时间过去了,没有座位被释放,他们将离开理发店。
  14. 移相器:编写一个依赖Phaser的程序,分三个阶段模拟服务器的启动过程。服务器在其五个内部服务启动后被视为已启动。在第一阶段,我们需要同时启动三个服务。在第二阶段,我们需要同时启动另外两个服务(只有在前三个服务已经运行的情况下才能启动)。在第三阶段,服务器执行最后一次签入,并被视为已启动。

解决方案

以下各节介绍上述问题的解决方案。记住,解决一个特定问题通常不是只有一种正确的方法。另外,请记住,这里显示的解释仅包括解决问题所需的最有趣和最重要的细节。下载示例解决方案以查看更多详细信息,并在这个页面中试用程序。

199 线程生命周期状态

Java 线程的状态通过Thread.State枚举表示。Java 线程的可能状态如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SM3wWnYZ-1657345732714)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/49d6745e-1417-4768-aad8-ea8ab6832ed6.png)]

不同的生命周期状态如下:

  • 新建状态
  • 可运行状态
  • 阻塞状态
  • 等待状态
  • 定时等待状态
  • 终止状态

让我们在下面的部分学习所有不同的状态。

新建状态

如果 Java 线程已创建但未启动,则该线程处于新建状态(线程构造器以新建状态创建线程)。这是它的状态,直到start()方法被调用。本书附带的代码包含几个代码片段,这些代码片段通过不同的构造技术(包括 Lambda)揭示了这种状态。为简洁起见,以下只是其中一种结构:

public class NewThread {
  public void newThread() {
    Thread t = new Thread(() -> {});
    System.out.println("NewThread: " + t.getState()); // NEW
  }
}
NewThread nt = new NewThread();
nt.newThread();

可运行状态

通过调用start()方法获得从新建可运行的转换。在此状态下,线程可以正在运行或准备运行。当它准备好运行时,线程正在等待 JVM 线程调度器为它分配运行所需的资源和时间。一旦处理器可用,线程调度器就会运行线程。

下面的代码片段应该打印RUNNABLE,因为我们在调用start()之后打印线程的状态。但由于线程调度器的内部机制,这一点无法保证:

public class RunnableThread {
  public void runnableThread() {
    Thread t = new Thread(() -> {});
    t.start();
    // RUNNABLE
    System.out.println("RunnableThread : " + t.getState()); 
 }
}
RunnableThread rt = new RunnableThread();
rt.runnableThread();

阻塞状态

当线程试图执行 I/O 任务或同步块时,它可能会进入阻塞状态。例如,如果一个线程t1试图进入另一个线程t2正在访问的同步代码块,那么t1将保持在阻塞状态,直到它能够获得锁为止。

此场景在以下代码片段中形成:

  1. 创建两个线程:t1t2
  2. 通过start()方法启动t1
  1. t1将执行run()方法并获取同步方法syncMethod()的锁。
  2. 因为syncMethod()有一个无限循环,所以t1将永远留在里面。
  1. 2 秒(任意时间)后,通过start()方法启动t2
  1. t2将执行run()代码,由于无法获取syncMethod()的锁,最终进入阻塞状态。

代码段如下:

public class BlockedThread {
  public void blockedThread() {
    Thread t1 = new Thread(new SyncCode());
    Thread t2 = new Thread(new SyncCode());
    t1.start();
    Thread.sleep(2000);
    t2.start();
    Thread.sleep(2000);
    System.out.println("BlockedThread t1: " 
      + t1.getState() + "(" + t1.getName() + ")");
    System.out.println("BlockedThread t2: " 
      + t2.getState() + "(" + t2.getName() + ")");
    System.exit(0);
  }
  private static class SyncCode implements Runnable {
    @Override
    public void run() {
      System.out.println("Thread " + Thread.currentThread().getName() 
        + " is in run() method");
      syncMethod();
    }
    public static synchronized void syncMethod() {
      System.out.println("Thread " + Thread.currentThread().getName() 
        + " is in syncMethod() method");
      while (true) {
        // t1 will stay here forever, therefore t2 is blocked
      }
    }
  }
}
BlockedThread bt = new BlockedThread();
bt.blockedThread();

下面是一个可能的输出(线程的名称可能与此处不同):

Thread Thread-0 is in run() method
Thread Thread-0 is in syncMethod() method
Thread Thread-1 is in run() method
BlockedThread t1: RUNNABLE(Thread-0)
BlockedThread t2: BLOCKED(Thread-1)

等待状态

等待另一个线程t2完成的线程t1处于等待状态。

此场景在以下代码片段中形成:

  1. 创建线程:t1
  2. 通过start()方法启动t1
  3. t1run()方法中:
  1. 创建另一个线程:t2
  2. 通过start()方法启动t2
  3. t2运行时,调用t2.join()——由于t2需要加入t1(也就是说t1需要等待t2死亡),t1处于等待状态。
  1. t2run()方法中t2打印t1的状态,应该是等待(打印t1状态时t2正在运行,所以t1正在等待)。

代码段如下:

public class WaitingThread {
  public void waitingThread() {
    new Thread(() -> {
      Thread t1 = Thread.currentThread();
      Thread t2 = new Thread(() -> {
        Thread.sleep(2000);
        System.out.println("WaitingThread t1: " 
          + t1.getState()); // WAITING
      });
      t2.start();
      t2.join();
    }).start();
  }
}
WaitingThread wt = new WaitingThread();
wt.waitingThread();

定时等待状态

等待另一个线程t2完成显式时间段的线程t1处于定时等待状态。

此场景在以下代码片段中形成:

  1. 创建线程:t1
  2. 通过start()方法启动t1
  3. t1run()方法中,增加 2 秒的睡眠时间(任意时间)。
  4. t1运行时,主线程打印t1状态,该状态应为定时等待,因为t1处于两秒后过期的sleep()中。

代码段如下:

public class TimedWaitingThread {
  public void timedWaitingThread() {
    Thread t = new Thread(() -> {
      Thread.sleep(2000);
    });
    t.start();
    Thread.sleep(500);
    System.out.println("TimedWaitingThread t: " 
      + t.getState()); // TIMED_WAITING
  }
}
TimedWaitingThread twt = new TimedWaitingThread();
twt.timedWaitingThread();

终止状态

成功完成任务或异常中断的线程处于终止状态。模拟起来非常简单,如下面的代码片段(应用的主线程打印线程的状态,t——发生这种情况时,线程t已经完成了它的工作):

public class TerminatedThread {
  public void terminatedThread() {
    Thread t = new Thread(() -> {});
    t.start();
    Thread.sleep(1000);
    System.out.println("TerminatedThread t: " 
      + t.getState()); // TERMINATED
  }
}
TerminatedThread tt = new TerminatedThread();
tt.terminatedThread();

为了编写线程安全类,我们可以考虑以下技术:

  • 没有状态(类没有实例和static变量)
  • 状态,但不共享(例如,通过RunnableThreadLocal等使用实例变量)
  • 状态,但状态不可变
  • 使用消息传递(例如,作为 Akka 框架)
  • 使用synchronized
  • 使用volatile变量
  • 使用java.util.concurrent包中的数据结构
  • 使用同步器(例如,CountDownLatchBarrier
  • 使用java.util.concurrent.locks包中的锁

200 对象级与类级锁定

在 Java 中,标记为synchronized的代码块一次可以由一个线程执行。由于 Java 是一个多线程环境(它支持并发),因此它需要一个同步机制来避免并发环境特有的问题(例如死锁和内存一致性)。

线程可以在对象级或类级实现锁。

对象级别的锁定

对象级的锁定可以通过在非static代码块或非static方法(该方法的对象的锁定对象)上标记synchronized来实现。在以下示例中,一次只允许一个线程在类的给定实例上执行synchronized方法/块:

  • 同步方法案例:
public class ClassOll {
  public synchronized void methodOll() {
    ...
  }
}
  • 同步代码块:
public class ClassOll {
  public void methodOll() {
    synchronized(this) {
      ...
    }
  }
}
  • 另一个同步代码块:
public class ClassOll {
  private final Object ollLock = new Object();
  public void methodOll() {
    synchronized(ollLock) {
      ...
    }
  }
}

类级别的锁定

为了保护static数据,可以通过标记static方法/块或用synchronized获取.class引用上的锁来实现类级锁定。在以下示例中,一次只允许运行时可用实例之一的一个线程执行synchronized块:

  • synchronized static方法:
public class ClassCll {
  public synchronized static void methodCll() {
    ...
  }
}
  • .class同步块:
public class ClassCll {
  public void method() {
    synchronized(ClassCll.class) {
      ...
    }
  }
}
  • 同步的代码块和其他static对象的锁定:
public class ClassCll {
  private final static Object aLock = new Object();
  public void method() {
    synchronized(aLock) {
      ...
    }
  }
}

很高兴知道

以下是一些暗示同步的常见情况:

  • 两个线程可以同时执行同一类的synchronized static方法和非static方法(参见P200_ObjectVsClassLevelLockingApp 的OllAndCll类)。这是因为线程在不同的对象上获取锁。
  • 两个线程不能同时执行同一类的两个不同的synchronized static方法(或同一synchronized static方法)(检查P200_ObjectVsClassLevelLocking应用的TwoCll类)。这不起作用,因为第一个线程获得了类级锁。以下组合将输出staticMethod1(): Thread-0,因此,只有一个线程执行一个static synchronized方法:
TwoCll instance1 = new TwoCll();
TwoCll instance2 = new TwoCll();
  • 两个线程,两个实例:
new Thread(() -> {
  instance1.staticMethod1();
}).start();
new Thread(() -> {
  instance2.staticMethod2();
}).start();
  • 两个线程,一个实例:
new Thread(() -> {
  instance1.staticMethod1();
}).start();
new Thread(() -> {
  instance1.staticMethod2();
}).start();
  • 两个线程可以同时执行非synchronizedsynchronized staticsynchronizedstatic方法(检查P200_ObjectVsClassLevelLocking应用的OllCllAndNoLock类)。
  • 从需要相同锁的同一类的另一个synchronized方法调用synchronized方法是安全的,因为synchronized可重入的(只要是相同的锁,第一个方法获取的锁也会用于第二个方法)。检查P200_ObjectVsClassLevelLocking应用的TwoSyncs类。

根据经验,synchronized关键字只能用于static/非static方法(不是构造器)/代码块。避免同步非final字段和String文本(通过new创建的String实例是可以的)。

Java 编程问题:十、并发-线程池、可调用对象和同步器2https://developer.aliyun.com/article/1426162

相关文章
|
20天前
|
编解码 JavaScript 前端开发
【Java进阶】详解JavaScript的BOM(浏览器对象模型)
总的来说,BOM提供了一种方式来与浏览器进行交互。通过BOM,你可以操作窗口、获取URL、操作历史、访问HTML文档、获取浏览器信息和屏幕信息等。虽然BOM并没有正式的标准,但大多数现代浏览器都实现了相似的功能,因此,你可以放心地在你的JavaScript代码中使用BOM。
64 23
|
27天前
|
Java 数据安全/隐私保护
Java 类和对象
本文介绍了Java编程中类和对象的基础知识,作为面向对象编程(OOP)的核心概念。类是对象的蓝图,定义实体类型;对象是具体实例,包含状态和行为。通过示例展示了如何创建表示汽车的类及其实例,并说明了构造函数、字段和方法的作用。同时,文章还探讨了访问修饰符的使用,强调封装的重要性,如通过getter和setter控制字段访问。最后总结了类与对象的关系及其在Java中的应用,并建议进一步学习继承等概念。
|
2月前
|
Java 数据库
【YashanDB知识库】kettle同步大表提示java内存溢出
在数据导入导出场景中,使用Kettle进行大表数据同步时出现“ERROR:could not create the java virtual machine!”问题,原因为Java内存溢出。解决方法包括:1) 编辑Spoon.bat增大JVM堆内存至2GB;2) 优化Kettle转换流程,如调整批量大小、精简步骤;3) 合理设置并行线程数(PARALLELISM参数)。此问题影响所有版本,需根据实际需求调整相关参数以避免内存不足。
|
2月前
|
设计模式 缓存 Java
重学Java基础篇—Java对象创建的7种核心方式详解
本文全面解析了Java中对象的创建方式,涵盖基础到高级技术。包括`new关键字`直接实例化、反射机制动态创建、克隆与反序列化复用对象,以及工厂方法和建造者模式等设计模式的应用。同时探讨了Spring IOC容器等框架级创建方式,并对比各类方法的适用场景与优缺点。此外,还深入分析了动态代理、Unsafe类等扩展知识及注意事项。最后总结最佳实践,建议根据业务需求选择合适方式,在灵活性与性能间取得平衡。
107 3
|
1月前
|
存储 缓存 Java
理解Java引用数据类型:它们都是对象引用
本文深入探讨了Java中引用数据类型的本质及其相关特性。引用变量存储的是对象的内存地址而非对象本身,类似房子的地址而非房子本身。文章通过实例解析了引用赋值、比较(`==`与`equals()`的区别)以及包装类缓存机制等核心概念。此外,还介绍了Java引用类型的家族,包括类、接口、数组和枚举。理解这些内容有助于开发者避免常见错误,提升对Java内存模型的掌握,为高效编程奠定基础。
71 0
|
1月前
|
Java
java中一个接口A,以及一个实现它的类B,一个A类型的引用对象作为一个方法的参数,这个参数的类型可以是B的类型吗?
本文探讨了面向对象编程中接口与实现类的关系,以及里氏替换原则(LSP)的应用。通过示例代码展示了如何利用多态性将实现类的对象传递给接口类型的参数,满足LSP的要求。LSP确保子类能无缝替换父类或接口,不改变程序行为。接口定义了行为规范,实现类遵循此规范,从而保证了多态性和代码的可维护性。总结来说,接口与实现类的关系天然符合LSP,体现了多态性的核心思想。
36 0
|
2月前
|
存储 算法 安全
Java对象创建和访问
Java对象创建过程包括类加载检查、内存分配(指针碰撞或空闲列表)、内存初始化、对象头设置及初始化方法执行。访问方式有句柄和直接指针两种,前者稳定但需额外定位,后者速度快。对象创建涉及并发安全、垃圾回收等机制。
Java对象创建和访问
|
2月前
|
Java 调度
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
182 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
|
9月前
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
117 1
|
6月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####