当前位置:
科技动态 > 【第123期】一篇文章解决那些高难度并发面试题
【第123期】一篇文章解决那些高难度并发面试题
点击上方“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知音整理,答案仅供参考,欢迎指正!
与其在网上拼命找题? 不如马上关注我们~