当前位置:科技动态 > 【第123期】一篇文章解决那些高难度并发面试题

【第123期】一篇文章解决那些高难度并发面试题

  • 发布:2023-10-05 20:09

点击上方“Java面试题精选”关注公众号 面试时画图,查漏补缺 >>番外:往期面试题,10个为单位放在这个公众号菜单栏->面试题,有需要的欢迎阅读 阶段总结合集:++小旗实现、百道面试题总结++ 1.对象的wait()和notify()方法 下图显示了线程状态: Object对象中的wait()和notify()用于实现等待/通知模式。等待状态和阻塞状态是不同的。处于等待状态的线程可以通过notify()方法被唤醒并继续执行,而处于阻塞状态的线程则等待获取新的锁。 调用wait()方法后,当前线程将进入等待状态,直到其他线程调用notify()或notifyAll()唤醒。 调用notify()方法后,可以唤醒正在等待的单个线程。 相关文章参考 我们来说一下notify和notifyAll的区别和相似之处 2.并发特性——原子性、有序性、可见性 原子性:即一个操作或多个操作要么完全执行且执行过程不被任何因素打断,要么根本不执行。 可见性:当多个线程访问同一个变量时,如果一个线程修改了该变量的值,其他线程可以立即看到修改后的值。 有序性:即程序执行的顺序是按照代码的顺序执行的,没有指令重新排列。 3.synchronized的实现原理是什么? Synchronized可以保证运行时只有一个进程可以同时访问某个方法或代码块,同时还可以保证共享变量的内存可见性。 Java中的每个对象都可以作为一个锁,这是synchronized实现同步的基础: 普通同步方法,锁是当前实例对象 静态同步方法,锁是当前类的类对象 同步方法块,锁是括号内的对象 同步代码块:monitorenter指令插入在同步代码块的开头,monitorexit指令插入在同步代码块的末尾。 JVM需要保证每个monitorenter都有一个与之对应的monitorexit。任何对象都有一个与其关联的监视器。当监视器被握住时,它将处于锁定状态。当线程执行monitorenter指令时,会尝试获取该对象对应的Monitor的所有权,即会尝试获取该对象的锁。同步方法:同步方法会被翻译成普通的方法调用和返回指令如:invokevirtual和areturn指令。 VM字节码级别没有专门的指令来实现synchronized修饰的方法,而是在Class文件中。在方法表中,将方法的access_flags字段中的synchronized标志位置设置为1,表示该方法是synchronized方法,并使用调用该方法的对象或者该方法所属的Class来表示Klass JVM内部对象中的锁对象。 synchronized是重量级锁,在JDK1.6中进行了优化,有自旋锁、自适应自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术,降低锁操作的成本。 相关文章参考 陷入Synchronized的底层实现——简介 卡在Synchronized的底层实现上——偏向锁 卡在Synchronized底层实现——轻量级锁 卡在Synchronized的底层实现——重量级锁 4、Volatile的实现原理是什么? Volatile是一种轻量级锁,不会引起线程上下文切换和调度。 易失性可见性:读取易失性变量总是可以看到对该变量的最终写入。 易失性原子性:易失性对于单个读/写(32位Long,Double)来说是原子的,除了复合操作,例如i++。 JVM底层使用“内存屏障”来实现易失性语义并防止指令重排序。 volatile常用于两种场景:状态标记变量和Double Check。 相关文章参考 Java并发编程:Volatile关键字分析 www.sychzs.cn内存模型(JMM) JMM指定了线程工作内存和主内存之间的交互,以及线程之间的可见性和程序的执行顺序。 一方面,需要为程序员提供足够强的内存可见性保证。 另一方面,编译器和处理器的限制应该尽可能地放宽。 JMM使程序员免受CPU和操作系统内存使用问题的影响,使程序能够在不同的CPU和操作系统内存上达到预期的结果。 Java使用内存共享模型来实现线程之间的通信。编译器和处理器可以对程序进行重新排序和优化,但需要遵循一些规则,不能随意重新排序。 在并发编程模型中,你必然会遇到以上三个概念:原子性:一个或多个操作要么全部执行,要么不执行。 可见性:当多个线程同时访问共享变量时,如果其中一个线程更改了共享变量,其他线程应该能够立即看到更改。 顺序性:程序的执行必须按照代码的顺序执行。 通过 volatile、synchronized、final、concurrent 包等实现。 相关文章参考 深入理解Java虚拟机[1]JVM内存模型 6.关于队列AQS队列同步器 AQS是构建锁或其他同步组件(如ReentrantLock、ReentrantReadWriteLock、Semaphore等)的基本框架,包括实现同步器的细节(获取同步状态、FIFO同步队列)。使用AQS的主要方式是继承。子类继承同步器并实现其抽象方法来管理同步状态。 保持同步状态状态。当state > 0时,表示已获取锁;当state = 0时,表示锁已被释放。 AQS通过内置的FIFO同步队列完成资源获取线程的排队工作: 如果当前线程未能获取同步状态(锁),AQS会将当前线程和等待状态信息构造成一个节点(Node)并添加到同步队列中,同时阻塞当前线程。 当同步状态释放后,节点中的线程会被唤醒,尝试再次获取同步状态。 AQS内部维护**CLH双向同步队列** 相关文章参考 AbstractQueuedSynchronizer源码分析条件队列 七、锁的特点 可重入锁:指同一个锁在一个线程中可以多次获取。 ReentrantLock和synchronized都是可重入锁。 可中断锁:顾名思义,就是可以相应中断的锁。 Synchronized 不是可中断锁,但 Lock 是可中断锁。 公平锁:即按照请求锁的顺序尝试获取锁。 synchronized是非公平锁,ReentrantLock和ReentrantReadWriteLock默认是非公平锁,但是可以设置为公平锁。 相关文章参考 Java多线程编程-锁优化 并发编程的死锁分析 Java读写锁实现原理 8.ReentrantLock锁ReentrantLock,即可重入锁,是一种递归非阻塞同步机制。可以相当于使用synchronized,但是ReentrantLock提供了比synchronized更强大、更灵活的加锁机制,可以降低死锁的概率。 ReentrantLock 基于内部 Sync 实现来实现 Lock 接口。 Sync 实现了 AQS,并提供了两种实现:FairSync 和 NonFairSync。 健康)状况 Condition和Lock一起使用来实现等待/通知模式,通过await()和singnal()阻塞和唤醒线程。 Condition是广义上的条件队列。它为线程提供了更灵活的等待/通知模式。线程在调用await方法后执行挂起操作,直到线程等待的某个条件成立时才会被唤醒。 Condition 必须与 Lock 一起使用,因为对共享状态变量的访问发生在多线程环境中。 Condition实例必须绑定Lock,所以Condition一般作为Lock的内部实现。 相关文章参考 ReentrantLock源码分析 9. 可重入读写锁 读写锁维护一对锁,一个读锁和一个写锁。通过分离读锁和写锁,并发性相比普通排它锁有很大的提高: 同时可以允许多个读线程同时访问。 但是,当写入线程访问时,所有读写线程都会被阻塞。 读写锁的主要特点: 公平:支持公平和不公平。 可重入:支持重入。读写锁最多支持65535个递归写锁和65535个递归读锁。 锁降级:按照先获取写锁,再获取读锁,最后释放写锁的顺序,即可将写锁降级为读锁。 ReentrantReadWriteLock实现了ReadWriteLock接口,是一个可重入读写锁实现类。 在同步状态下,为了表示两个锁,将一个32位整数分为高16位和低16位,分别表示读和写状态。 10.Synchronized和Lock的区别 Lock是一个接口,synchronized是Java中的关键字,synchronized是内置的语言实现;当异常发生时,synchronized会自动释放线程占用的锁,因此不会造成死锁;而当发生异常时,如果Lock不主动通过unLock()释放锁,很可能会造成死锁。因此,使用Lock时,需要在finally块中释放锁; Lock允许等待锁的线程响应中断,但synchronized则不允许。当使用synchronized时, - 等待线程将永远等待,无法响应中断; 可以使用Lock来知道锁是否已经成功获取,但是synchronized不能做到这一点。 锁可以提高多线程读操作的效率。 更深: 与synchronized相比,ReentrantLock提供了更多、更全面的功能,并且具有更强的扩展性。例如:时间锁等待、可中断锁等待、锁投票。 ReentrantLock还提供了Condition,对于线程等待和唤醒操作更加细致和灵活。因此,ReentrantLock更适合条件变量多、锁竞争性强的场合(Condition会在后面解释)。 ReentrantLock 提供可轮询的锁定请求。它会尝试获取锁,如果成功则继续,否则可以等到下一次运行,而synchronized一旦进入锁请求就会要么成功,要么阻塞,所以相比synchronized,ReentrantLock会更不容易出现死锁。 ReentrantLock支持更灵活的synchronized代码块,但是使用synchronized时只能在同一个synchronized块结构中获取和释放。注意ReentrantLock的锁释放必须在finally中处理,否则可能会产生严重的后果。 ReentrantLock支持中断处理,性能比synchronized要好。 11、Java中的线程同步方法 synchronized 同步方法或代码块 易挥发的 锁 线程局部 阻塞队列(LinkedBlockingQueue) 使用原子变量(java.util.concurrent.atomic) 变量的不变性 相关文章参考 Java 并发编程:同步容器 java中实现同步的几种方式(总结)12、CAS是一种什么样的同步机制?多线程下为什么不使用int而是使用AtomicInteger? 比较和交换,比较和交换。可见synchronized可以保证代码块的原子性,而这往往会带来性能问题。易失性也是一个不错的选择,但是易失性不能保证原子性,只能在某些情况下使用。因此可以通过CAS进行同步,保证原子性。 当我们阅读Concurrent包下的类的源码时,我们发现无论是ReentrantLock内部的AQS,还是Atomic开头的各种原子类,内部都应用了CAS。 CAS中有三个参数:内存值V、旧期望值A、待更新值B。当且仅当内存值V的值等于旧期望值A时,内存值V的值为修改为B,否则什么都不做。伪代码如下: if (this.value == A) { this.value = B return true;} else { return false;} CAS可以保证读-修改-写操作是原子操作。 在多线程环境下,int类型的自增操作不是原子的,也是线程不安全的。您可以使用 AtomicInteger 代替。 // AtomicInteger.javaprivate static final Unsafe unsafe = Unsafe.getUnsafe();private static final long valueOffset;static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } } catch (Exception ex) { throw new Error(ex); } }}私有易失性整型值; Unsafe是CAS的核心类。 Java无法直接访问底层操作系统,而是通过本地的native方法来访问。但尽管如此,JVM仍然打开了一个后门:Unsafe,它提供了硬件级的原子操作。valueOffset是变量值在内存中的偏移地址。 Unsafe通过偏移地址获取数据的原始值。 value的当前值用volatile修饰,保证在多线程环境下看到相同的值。 // AtomicInteger.javapublic final int addAndGet(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta) + delta;} // www.sychzs.cn//compareAndSwapInt(var1, var2, var5, var5 + var4) 换成compareAndSwapInt(obj, offset,expect, update)其实更清楚,也就是说如果obj中的值等于expect,证明没有其他线程改变了这个变量,那么就更新它来更新。如果这一步CAS不成功,则使用spin方法继续CAS操作。乍一看,这似乎也是两步。事实上,在JNI中它是借助一条CPU指令来完成的。所以它仍然是一个原子操作。公共最终 int getAndAddInt(Object var1, long var2, int var4) { int var5;执行 { var5 = this.getIntVolatile(var1, var2); while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)) ; return var5;}//该方法是一个本地方法,有四个参数,分别代表:对象、对象地址、期望值、修改值 public Final Native boolean CompareAndSwapInt(Object var1, long var2, int var4, int var5) ; 13. HashMap是线程安全的吗?如何体现呢?如何变得安全? 当向map添加元素时,大量的数据会引起扩容操作,多线程会导致HashMap的节点链表形成环形数据结构,形成死循环。所以HashMap是线程不安全的。如何变得安全: Hashtable:通过synchronized、排他锁、悲观策略保证线程安全。吞吐量低且性能差 SynchronizedHashMap:使用Collections.synchronizedMap()方法包装HashMap并返回一个SynchronizedHashMap对象。在源码中,SynchronizedHashMap也使用了synchronized来保证线程安全。但实现方法与Hashtable略有不同(前者是synchronized方法,后者是通过互斥变量的synchronized加锁实现) ConcurrentHashMap:JUC中线程安全的容器,高效并发。 ConcurrentHashMap的key和value不允许为null。 相关文章参考 浅析HashMap的实现原理 Java中HashMap的底层数据结构 集合系列—HashMap源码分析 14.ConcurrentHashMap如何实现? ConcurrentHashMap的实现与Hashtable不同。它不使用排他锁,效率更高。 jdk1.7和jdk1.8中的实现方法也略有不同。 Jdk1.7中使用了分段锁和HashEntry,使锁更加细致。 ConcurrentHashMap使用段锁技术,其中Segment继承自ReentrantLock。与 HashTable 不同,put 和 get 操作都需要同步。理论上,ConcurrentHashMap支持CurrencyLevel(Segment数组的数量)线程并发。 Jdk1.8使用CAS+Synchronized来保证并发更新的安全性。当然底层采用的是数组+链表+红黑树的存储结构。 节点表中存储节点数据。默认 Node 数据大小为 16,扩展大小始终为 2^N。 为了保证可见性,Node节点中的val和next节点都用volatile修饰。 当链表长度大于8时,会转为红黑树,并将节点打包成TreeNode,放入TreeBin中。put():1、计算key对应的哈希值; 2、如果哈希表还没有初始化,则调用initTable()进行初始化,否则找到表中的索引位置,通过CAS添加节点。如果链表节点数超过8,则将链表转换为红黑树。如果总节点数超过,则进行扩容。 get():不需要加锁,直接根据key的哈希值遍历节点。 相关文章参考 Java并发系列| ConcurrentHashMap源码分析 15.CountDownLatch和CyclicBarrier有什么区别?并发工具 CyclicBarrier 允许一组线程相互等待,直到到达公共屏障点。 CyclicBarrier 在涉及一组必须不时相互等待的固定大小线程的程序中非常有用。由于Barrier在释放等待线程后可以重复使用,因此被称为循环Barrier。 每个线程调用#await()方法告诉CyclicBarrier我已经到达barrier了,然后当前线程被阻塞。当所有线程到达屏障时,阻塞结束,所有线程可以继续执行后续逻辑。 CountDownLatch 使一个线程能够等待其他线程完成其工作,然后再继续。使用计数器来实现这一点。计数器的初始值是线程数。当每个线程完成其任务时,计数器值减一。当计数器值为 0 时,表示所有线程都已完成任务,然后等待 CountDownLatch 的线程可以继续执行任务。 两者的区别: CountDownLatch的作用是允许1个或N个线程等待其他线程完成执行;而CyclicBarrier则允许N个线程互相等待。 CountDownLatch 的计数器无法重置; CyclicBarrier的计数器可以重置并使用,因此被称为循环屏障。 信号量是一个控制对多个共享资源的访问的计数器。与CountDownLatch一样,它本质上是一个“共享锁”。计数信号量。从概念上讲,信号量维护一个权限集。 如果有必要,每个获取都会阻塞,直到权限可用,然后获取权限。每个版本都会添加一个权限,可能会释放一个阻塞的获取者。 相关文章参考 Java并发系列| CountDownLatch源码分析 16、如何控制线程,尽可能减少上下文切换? 减少上下文切换的方法包括无锁并发编程、CAS算法、使用最少线程和使用协程。 无锁并发编程。当多个线程竞争锁时,就会发生上下文切换。因此,当多个线程处理数据时,可以通过一些方法来避免使用锁。例如,根据Hash算法的模数将数据的ID分为段,不同的线程处理不同的段数据。 CAS算法。 Java的Atomic包使用CAS算法来更新数据,无需锁定。 使用最少的线程。避免创建不必要的线程。例如,如果任务很少但创建了很多线程来处理它们,这将导致大量线程处于等待状态。 协程。在单线程中实现多任务调度,并在单线程中维护多个任务之间的切换。 17.什么是乐观锁和悲观锁? 像synchronized这样的独占锁是悲观锁。它假设肯定会发生冲突,所以锁定才有用。此外,还有乐观锁。乐观锁的意义在于,假设不存在冲突,那么我就可以在执行某个操作的时候,如果发生冲突,那么我就再去尝试,直到成功为止。最常见的乐观锁是CAS。 18. 阻塞队列 阻塞队列实现了BlockingQueue接口,有多组处理方法。 抛出异常:add(e)、remove()、element() 返回特殊值:offer(e)、pool()、peek() 块:put(e)、take() JDK 8 中有七个可用的阻塞队列: ArrayBlockingQueue:由数组结构组成的有界阻塞队列。 LinkedBlockingQueue:由链表结构组成的无界阻塞队列。 PriorityBlockingQueue:支持优先级排序的无界阻塞队列。 DelayQueue:使用优先级队列实现的无界阻塞队列。 SynchronousQueue:不存储元素的阻塞队列。 LinkedTransferQueue:由链表结构组成的无界阻塞队列。 LinkedBlockingDeque:由链表结构组成的双向阻塞队列。ArrayBlockingQueue,一个由数组实现的有界阻塞队列。该队列采用先进先出原则来排序和添加元素。内部使用ReentrantLock + Condition来完成多线程环境下的并发操作。 相关文章参考 阻塞队列阻塞队列 Java并发编程:阻塞队列 19. 线程池 线程池有五种状态:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。 RUNNING:接收并处理任务。 SHUTDOWN:不接受但处理现有任务。 STOP:不接收、不处理任务,终端正在处理任务。 TIDYING:所有任务终止,线程池进入TIDYING状态。当线程池变为TIDYING状态时,钩子函数termination()就会被执行。 TERMINATED:线程池完全终止。 内部变量**ctl**定义为AtomicInteger,记录了两条信息:“线程池中的任务数量”和“线程池的状态”。总共32位,其中高3位代表“线程池状态”,低29位代表“线程池中的任务数量”。 线程池创建参数 核心池大小 线程池中核心线程的数量。当任务提交时,线程池会创建一个新的线程来执行该任务,直到当前线程数等于 corePoolSize。如果调用线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。 最大池大小 线程池中允许的最大线程数。线程池的阻塞队列满后,如果还有任务提交,且当前线程数小于maximumPoolSize,则会创建一个新的线程来执行该任务。请注意,如果使用无界队列,则该参数将不起作用。 保持活动时间 线程空闲的时间。线程的创建和销毁是有代价的。线程完成任务后并不会立即销毁,而是会继续存活一段时间:keepAliveTime。默认情况下,该参数仅在线程数大于 corePoolSize 时生效。 单元 keepAliveTime的单位。时间单位 工作队列 阻塞队列,用于保存等待执行的任务。等待任务必须实现Runnable接口。我们可以从以下选项中进行选择:ArrayBlockingQueue:基于数组结构的有界阻塞队列,先进先出。 LinkedBlockingQueue:基于链表结构的有界阻塞队列,先进先出。 SynchronousQueue:不存储元素的阻塞队列。每个插入操作都必须等待删除操作,反之亦然。 PriorityBlockingQueue:具有优先级边界的阻塞队列。 线程工厂 工厂用于设置线程创建。可以通过 Executors.defaultThreadFactory() 访问该对象。它提供了通过newThread()方法创建线程的功能。 newThread()方法创建的线程都是“非守护线程”,并且“线程优先级是Thread.NORM_PRIORITY”。 处理程序 RejectedExecutionHandler,线程池的拒绝策略。所谓拒绝策略是指当有任务加入线程池时,线程池拒绝该任务所采取的相应策略。当向线程池提交任务时,如果线程池中的线程饱和并且阻塞队列已满,线程池会选择拒绝策略来处理该任务。 线程池提供了四种拒绝策略: AbortPolicy:直接抛出异常,默认策略; CallerRunsPolicy:使用调用者的线程执行任务; DiscardOldestPolicy:丢弃阻塞队列中最前面的任务,执行当前任务; DiscardPolicy:直接丢弃任务; 当然,我们也可以通过实现RejectedExecutionHandler接口来实现自己的拒绝策略,比如日志记录等。 当向线程池添加新任务时: 如果线程数量没有达到corePoolSize,就会创建一个新的线程(核心线程)来执行任务。 当线程数量达到corePoolSize时,任务将被移至队列等待。 队列已满,创建新线程(非核心线程)执行任务 如果队列已满并且线程总数达到maximumPoolSize,则会按handler的拒绝策略进行处理。 可以通过Executor框架创建线程池: 固定线程池Public Static ExecutorService NewFixedThreadPool (Int NTHREADS) {返回新ThreadPoolexecutor (NTHREADS, NTHREADS, 0L, TimeUnit.milliseConds, NEW LinkedBlockingQueue ());} corePoolSize和maximumPoolSize都设置为创建FixedThreadPool时指定的参数nThreads,这意味着当线程池已满并且阻塞队列也已满时,如果继续提交任务,将直接使用拒绝策略,线程池不会创建新线程。执行任务,但直接遵循拒绝策略。 FixThreadPool使用默认的拒绝策略AbortPolicy,直接抛出异常。 但是workQueue使用无界的LinkedBlockingQueue,因此当任务数量超过corePoolSize时,所有任务都会被添加到队列中,而不执行拒绝策略。 单线程执行器 公共静态ExecutorService newSingleThreadExecutor(){返回新的FinalizedDeleatedExecutorService(新的ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,新的LinkedBlockingQueue()));} 作为单个工作线程的线程池,SingleThreadExecutor将corePool和maximumPoolSize都设置为1。与FixedThreadPool一样,它使用无界队列LinkedBlockingQueue,因此影响与FixedThreadPool相同。缓存线程池 CachedThreadPool是一个线程池,它根据需要创建新线程。它的定义如下: public static ExecutorService newCachedThreadPool() {   return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                 60L, TimeUnit.SECONDS,                                 new SynchronousQueue());} 这个线程池,当任务提交是就会创建线程去执行,执行完成后线程会空闲60s,之后就会销毁。但是如果主线程提交任务的速度远远大于 CachedThreadPool 的处理速度,则 CachedThreadPool 会不断地创建新线程来执行任务,这样有可能会导致系统耗尽 CPU 和内存资源,所以在使用该线程池是,一定要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题。 相关文章参考 JAVA线程池原理详解(1) JAVA线程池原理详解(2) Java多线程和线程池 Java线程池总结 20、为什么要使用线程池? 创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率。线程池缓存线程,可用已有的闲置线程来执行新任务(keepAliveTime) 线程并发数量过多,抢占系统资源从而导致阻塞。运用线程池能有效的控制线程最大并发数,避免以上的问题。 对线程进行一些简单的管理(延时执行、定时循环执行的策略等) 21、生产者消费者问题 实例代码用 Object 的 wait()和notify() 实现,也可用 ReentrantLock 和 Condition 来完成。或者直接使用阻塞队列。 public class ProducerConsumer {    public static void main(String[] args) {        ProducerConsumer main = new ProducerConsumer();        Queue buffer = new LinkedList<>();        int maxSize = 5;        new Thread(www.sychzs.cn Producer(buffer, maxSize), "Producer1").start();        new Thread(www.sychzs.cn Consumer(buffer, maxSize), "Comsumer1").start();        new Thread(www.sychzs.cn Consumer(buffer, maxSize), "Comsumer2").start();    }     class Producer implements Runnable {        private Queue queue;        private int maxSize;         Producer(Queue queue, int maxSize) {            this.queue = queue;            this.maxSize = maxSize;        }         @Override        public void run() {            while (true) {                synchronized (queue) {                    while (queue.size() == maxSize) {                        try {                            System.out.println("Queue is full");                            queue.wait();                        } catch (InterruptedException e) {                            e.printStackTrace();                        }                    }                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    Random random = new Random();                    int i = random.nextInt();                    System.out.println(Thread.currentThread().getName() + " Producing value : " + i);                    queue.add(i);                    queue.notifyAll();                }            }        }    }     class Consumer implements Runnable {        private Queue queue;        private int maxSize;         public Consumer(Queue queue, int maxSize) {            super();            this.queue = queue;            this.maxSize = maxSize;        }         @Override        public void run() {            while (true) {                synchronized (queue) {                    while (queue.isEmpty()) {                        try {                            System.out.println("Queue is empty");                            queue.wait();                        } catch (Exception ex) {                            ex.printStackTrace();                        }                    }                    try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    System.out.println(Thread.currentThread().getName() + " Consuming value : " + queue.remove());                    queue.notifyAll();                }            }        }    }} 相关文章参考 Java并发系列 | ConcurrentHashMap源码分析 Java并发系列 | CyclicBarrier源码分析 Java并发系列 | CountDownLatch源码分析 Java并发系列 | Semaphore源码分析 Java并发系列 | ReentrantLock源码分析 Java并发系列 | AbstractQueuedSynchronizer源码分析之条件队列 Java并发系列 | AbstractQueuedSynchronizer源码分析之共享模式 Java并发系列 | AbstractQueuedSynchronizer源码分析之概要分析 Java并发系列 | AbstractQueuedSynchronizer源码分析之独占模式 作者:Fururur www.sychzs.cn/Sinte-Beuve Java知音整理,答案仅供参考,欢迎指正! 与其在网上拼命找题? 不如马上关注我们~

相关文章

最新资讯