并发编程一直是Java基础中的高地,但很多只要有两三年Java基础的工程师,会在简历中很明确的标明“熟悉多线程编程、有高并发编程经验”,来凸显自己编程能力有多厉害,但实际上可能只是看了《Java Concurrency in Practice》的几个章节而已。其实对很多业务研发工程师来说,高并发编程经验并不是必备的核心竞争力之一,很多需要加锁或者统计的场景,大都可以交给外部系统如Redis来做,即多线程并发场景的转移。
那么作为面试官,如何简单快速考察面试者的多线程编程能力?可能方法有很多,笔者喜欢用到的一个题目如下:“对于单个Java应用,我们如何限制其中某个方法methodA()被调用的并发数不能超过100,如果超过100,超出的请求就直接返回null或抛异常”。笔者会要求面试者在白板上写出相应代码片段(当然并不会要求面试者一定要完整写出用到的类名或方法名)。
这个题看起来并不难,但仔细想想也不那么简单,大约30%的面试者会给出Semaphore的解决方案,例如:
private static Semaphore semaphore = new Semaphore(100); public static Integer methodA() { if(!semaphore.tryAcquire()) { return null; } try { // TODO 方法中的业务逻辑 } finally { semaphore.release(); } }
Semaphore信号量是一种比较完美的解决方案,代码简单而高效,一旦面试者给出这个方案,我们可以顺便考察下信号量相关的知识点。
但还有没有其他的思路或解决方案呢?笔者接触到的面试者,还给出过线程池的方案,因为最高并发数是100,那么表明同时最多只能有100个线程访问该方法,面试者一般会这么写:
private final static ExecutorService pool = new ThreadPoolExecutor(100, 100, 1, TimeUnit.MINUTES, new SynchronousQueue<>()); public static Integer methodAWrapper() { try { Future<Integer> future = pool.submit(() -> methodA()); return future.get(); } catch (Exception e) { return null; } } public static Integer methodA() { // TODO 方法中的业务逻辑 }
其实严格意义来讲,线程池的这种方案无法完美做到“如果超过100,超出的请求就直接返回null或抛异常”,哪怕是使用SynchronousQueue队列。但没关系,最关键的考察点并不在超限如何返回。当面试者写出这种方案,也可以顺便考察下线程池相关的知识点。
大多数面试者想到的是写个计数器,例如:
private static AtomicInteger counter = new AtomicInteger(0); public static Integer methodA() { int value = counter.incrementAndGet(); if(value > 100) { return null; } try { // TODO 方法中的业务逻辑 } finally { counter.decrementAndGet(); } }
于是我一般会问,为啥是选择incrementAndGet方法,而不是选择getAndIncrement?仔细看看会不会有其他问题?大多数面试者经过提示都能发现这里get、incr和比较不是原子操作,会产生“竞态条件”(race condition)。我们先把这种计数器方案叫做方案A,像想到计数器方案的面试者,也有小部分会这样写:
private static AtomicInteger counter = new AtomicInteger(0); public static Integer methodA() { int value = counter.get(); if(value > 100) { return null; } counter.incrementAndGet(); try { // TODO 方法中的业务逻辑 } finally { counter.decrementAndGet(); } }
我们把这种计数器的实现方案叫做方案B,这两种方案都会有“竞态条件”的问题,但产生的现象不一样。
对于方案A,在极端高并发的情况下,每个调用methodA的请求,都会对计数器进行+1,即使我们在finally对计数器进行了-1,也阻止value的值继续上涨,导致远大于100,得到的结果是所有请求没机会执行业务逻辑,即“饿死”现象。
对于方案B,由于是活的执行业务逻辑的许可后再进行的+1操作,很显然在高并发情况下会导致执行业务逻辑的线程数超过100。
很多Java老司机觉得自己不会犯这种错误,但实际上,最近阿里的开源项目Sentinel就有类似方案A的问题,Sentinel中使用责任链的模式,来对每笔调用进行统计、拦截,这里给出构建责任链DefaultSlotsChainBuilder类的代码片段:
public class DefaultSlotsChainBuilder implements SlotsChainBuilder { @Override public ProcessorSlotChain build() { ProcessorSlotChain chain = new DefaultProcessorSlotChain(); chain.addLast(new NodeSelectorSlot()); chain.addLast(new ClusterBuilderSlot()); chain.addLast(new LogSlot()); chain.addLast(new StatisticSlot()); // 统计数据,类似于上文提到的incrementAndGet chain.addLast(new SystemSlot()); // 检查是否超过阈值,类似于上文提到的value > 100 chain.addLast(new AuthoritySlot()); chain.addLast(new FlowSlot()); chain.addLast(new DegradeSlot()); return chain; } }
而在分布式服务框架Dubbo的早期版本(例如2.5.3),在对Provider提供线程限制保护的executes
(例如:<dubbo:service interface="com.manzhizhen.dubbo.server.service.Dubbo2Service"
ref="dubbo2Service" version="1.0.0" timeout="3000" executes="10" />)的实现方案,就踩了上述方案B的坑。
回归正题,也有面试者给出了阻塞队列的方案,即:
private static BlockingQueue<Integer> reqQueue = new ArrayBlockingQueue<>(100); public static Integer methodA() { if(!reqQueue.offer()) { return null; } try { // TODO 方法中的业务逻辑 } finally { reqQueue.poll(); } }
阻塞队列的方案也是不错的,代码简单,效率也还行。
当然,也有面试者说用Hystrix,嗯嗯,这也是可以的。
做个总结吧,最优方案当然是使用Semaphore,但Semaphore无法统计实际高峰期时的并发量有多少(很多场景需要通过实际最高的并发值来优化我们系统),文章很短,希望对大家能有所帮助。
相关推荐
并发控制是高并发环境中的关键问题,Spring提供了多种并发工具类,如`Semaphore`、`ReentrantLock`等,可以用来限制并发访问的数量,防止过多的并发导致数据库压力过大。此外,还可以使用乐观锁或者悲观锁策略来处理...
在Java编程中,HTTP并发访问是一项重要的技术,用于提高应用程序的性能和响应速度。当我们需要从服务器获取大量数据或者执行批量操作时,并发访问能够显著减少整体的等待时间。本项目提供了一个用Java语言实现的HTTP...
在网站分析中,PV(Page...在实际应用中,可能还需要考虑性能优化、数据持久化以及并发处理等问题,以应对大规模的访问日志。同时,为了提供更精确的分析,还可以进一步统计其他指标,如访问深度、停留时间、跳出率等。
Java并发编程是Java开发中的重要领域,特别是在多核处理器和分布式系统中,高效地利用并发可以极大地提升程序的性能和响应速度。《java并发编程的核心方法和框架》这本书旨在深入探讨这一主题,帮助开发者掌握Java...
6. **Semaphore**:信号量是一个控制同时访问特定资源的线程数量的工具,可以用来限制并发线程数。`TestSemaphore.java`可能包含了如何使用`Semaphore`来限制系统资源(如数据库连接)的并发访问。 7. **FutureTask...
并发编程是Java语言的一个重要特性,对于面试中涉及Java并发编程的问题,需要有深入的理解和掌握。本文档详细列举了Java并发领域面试中常见的问题,以下是对这些问题及答案的解析。 1.1 多线程和并发问题。并发是指...
java.util.concurrent 在并发编程中很常用的实用工具类。 java.util.concurrent.atomic 类的小工具包,支持在单个变量上解除锁的线程安全编程。 java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类...
Java并发编程是Java开发中的重要领域,它涉及到多线程、同步机制、线程池以及并发集合等核心概念。在Java中,并发编程是提高系统性能和资源利用率的关键技术,尤其是在处理大量I/O操作或者计算密集型任务时。本文将...
Java并发编程是Java开发中的重要领域,涉及到多线程、同步机制、线程池等多个核心概念,对于构建高效、稳定的应用至关重要。这份资料包含了关于Java并发编程的博客和网页,可以提供深入的理解和实践指导。 在Java...
Java并发编程是Java语言中最为复杂且重要的部分之一,它涉及了多线程编程、内存模型、同步机制等多个领域。为了深入理解Java并发编程,有必要了解其核心技术点和相关实现原理,以下将详细介绍文件中提及的关键知识点...
Java并发编程是Java开发中的重要领域,它涉及如何在多处理器或多核心环境下高效地执行程序。本套源码集合全面涵盖了Java并发编程的核心知识点,包括但不限于对象锁、Executors多任务线程框架以及线程池的实现。下面...
常用于限制系统中的并发线程数,例如限制服务器同时处理的连接数。 #### 1.4. Executor `Executor`框架是Java并发编程的核心,它提供了线程池的创建与管理。通过`ExecutorService`接口,我们可以提交任务...
3. 限制并发数:控制同时运行的任务数量,避免过多线程导致系统资源耗尽。 线程池有多种拒绝策略,例如: - AbortPolicy:默认策略,丢弃任务并抛出RejectedExecutionException异常。 - CallerRunsPolicy:由调用...
在Java编程领域,多线程和高并发是面试中经常被问到的重要知识点,也是大型系统设计的关键技术。本文将围绕“Java面试多线程高并发相关回家技巧”这一主题,深入探讨相关概念、原理以及面试中可能遇到的问题,帮助你...
线程安全是指在多线程环境下,一个类或方法能够正确地处理并发访问,不会因为线程间的交互而导致数据的不一致。在Java中,实现线程安全的方式有多种,如使用`synchronized`关键字、`volatile`关键字以及`Atomic`类。...
### Java并发框架整理 #### 多线程从1.2到1.7各种接口使用及场景介绍 在Java中,多线程技术是一项非常重要的功能,它可以极大地提高应用程序的性能和响应能力。从JDK 1.2版本开始,Java就提供了支持多线程的基础...
在Java开发中,尤其是在多核处理器和高并发场景下,理解和掌握并发编程是至关重要的。以下是对书中的主要知识点的详细阐述: 1. **Java并发基础** - **线程**:线程是程序执行的最小单位,Java通过`Thread`类来...
Java并发编程是Java开发中的重要领域,它涉及到多线程、同步、锁机制、线程池等关键概念,是提高程序性能和效率的关键技术。在Java中,并发编程的运用可以充分利用多核处理器的能力,实现高效的多任务处理。以下是对...
在Java编程中,多线程是并发执行任务的关键技术,它可以提高程序的效率和响应性。本文将深入探讨Java中的多线程操作方法,包括线程控制的基本方法、中断和睡眠以及相关示例。 首先,了解线程的基本状态至关重要。...