前言
总结了一些碎知识点在系列文章:【并发编程】知识脉络
在此总结一篇【并发编程】模块的面试题与答案,纯文字版,不过多讲解,更侧重面试中被问到该如何简洁明了的回答。
欢迎补充问答!
1、进程和线程的关系?
操作系统以进程为单位分配独立的系统资源(CPU、内存...),CPU以线程为单位共享进程得到的系统资源。
进程间并行,线程间并发。
进程间通信可以使用socket、pipe、信号量...线程间通信可以使用共享内存。
2、那并行和并发是什么关系?
- 并行:不同实体(比如CPU)同时进行。
- 并发:同一实体(比如CPU)间隔交替进行。
3、Java的线程模型了解吗?
- 按空间划分为内核线程模型(klt)和用户线程模型(ult)。区别在于线程的创建与销毁由谁管理,由内核管理的为KLT,Java属于这种。
4、Java创建线程有哪些方式,哪些区别?
- Java创建线程根本上都是new Thread().start()。
- 但是只创建线程没意义,因为创建线程是为了执行某项任务,因此,Java有多种方式将任务和线程搭配组合:
- 第一种组合方式(扩展Thread类):
- 创建任务类继承Thread类,重载run方法定义好自己的任务。
- 实例化任务类.start()方法。即可执行重载的run方法里的任务。
- 此方法也可以优化为lambda表达式方式:new Thread( () - > { //执行任务方法 } , "给线程取个名字").start();
- 优点:简单明了。缺点:任务和线程耦合。
- 第二种组合方式(实现Runnable接口):
- 创建任务类实现Runnable接口,重写run方法定义自己的任务。
- 创建线程对象,将任务类作为参数传入,线程对象启动.start()。
- 优点:任务和线程解耦,缺点:任务没返回值任务没异常处理。
- 第三种组合方式(实现Callable接口):
- 创建任务类实现Callable接口(定义好返回值类型),重写run方法定义自己的任务。
- 将任务类作为参数创建FutureTask类实例,再将该实例作为参数创建线程对象并.start()。
- 优点:可获取任务的异常,和返回值,与线程解耦。缺点:任务没异常处理。
- springboot中多线程的使用
- 考虑到bean是有spring容器管理的,bean里的变量等是需要初始化的:
- 定义一个工具类实现ApplicationContextAware,把applicationContext作为静态变量,定义getBean的方法,从applicationContext中获取bean(将来创建任务时不必new,而是用此方式获取)
- 从上述三种方式中选一种定义线程类和任务类,@Component("myTask") @Scope("prototype")注意不能是默认的单例bean
- 创建启动线程类,可以在启动类里创建多线程并执行,也可以单独定义组件创建执行:组建加@Component注解,创建执行方法上加 @PostConstruct注解(在构造方法完毕后,执行这个方法)
5、线程调用start()和run()方法的区别是什么呢?
- 执行run()方法,等同于,当前线程执行了一个方法函数,必须等run()方法执行完才继续向下执行,执行路径为一条,并非多线程。
- 执行start()方法,等同于,Thread类调用本地方法(public native void start0() ),start0为JVM层面c++写的函数,此函数根据不同操作系统定义了不同的代码但都是去调用操作系统线程库,例如linux中调用的操作系统函数pThread_create创建出内核态的线程,并进入就绪状态,与此同时JVM把创建出的线程与Java的new Thread()对象绑定,绑定后设置状态为Runnable,JVM唤醒操作系统中此线程的并取指到run方法那里执行代码。
6、线程的状态有哪些?
- 操作系统中线程状态分为五种(Linux x86):初始--就绪--运行--休眠--终止。
- 初始为刚创建出来,就绪状态是等待cpu分配时间片等,得到时间片执行时为运行状态,没有执行任务时为休眠,之行结束终止状态。
- Java中枚举类把线程分为六种状态:new--runnable-blocked--waiting--timed_waiting--terminated。新建--可运行状态--阻塞--等待--计时等待--终止。
- NEW:新创建的线程,只是Java中对象。
- RUNNABLE:包含了操作系统中线程状态的就绪状态和运行状态。
- BLOCKED:synchronized专属状态,等待监视器锁的状态,即未获取到锁而等待执行synchronized代码块或用synchronized标记的方法时会出现此状态。
- WAITING:Thread.join() \ Object.wait() \ LockSupport.park() 后进入等待状态。
- TIMED_WAITING:Thread.join(time) \ Object.wait(time) 有超时时间的等待状态。
7、sleep()\wait()\join()\park()\yield()什么关系与区别?
Thread()类中提供的方法有:sleep() \ join() \ yield() : 他们是服务于线程状态的。
- join() :非静态的方法,例如在main线程中调用myTaskThread的join()方法,即main线程等待myTaskThread线程执行结束后main线程再向下执行。
- sleep() :静态方法,需要指定时间。当前线程sleep后暂停执行,让出执行机会,如果有synchronized锁,不会让出访问共享资源的权利。
- yield() :静态方法,不需要指定时间。当前线程yield后暂停执行,重新获取执行机会,如果有synchronized锁,不会让出。
Object()类中提供的 wait() : 用于协调多个线程对共享数据的存取
- 配合notify() 和 notifyAll() 方法一起使用 。仅在synchronized代码块中使用。
- 调用这些方法前必须拥有对象的锁,wait()后暂停执行,让出执行机会,让出锁,将当前线程放入对象等待池中。
- 调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中。
LockSupport.park() : 更灵活多用的暂停线程方法。
- 底层是调用的Unsafe的native方法。
- sleep只能自己到时间醒,而park可以由别人唤醒,
- wait只能notify随机唤醒或全部唤醒,而park可以指定唤醒谁。
- 可以先unpark 后 park 相当于通行证,而现notify后wait会抛异常。
- 用在synchronized中和其他地方都可以,也不释放锁。
- sleep、wait 都要捕获中断异常,而park不需要捕获异常。
8、如何优雅的停止线程?
- stop() : 不优雅,直接杀线程,释放锁对象,无论有没有执行完自己的任务。
- interrupt() : 改变标志位,由中断者发出中断命令,由被中断者自己决定到哪里该中断。
9、Java的线程池了解吗?
JUC(java util concurrent)包下提供了线程池工具,为什么推荐自定义线程池呢?:
- java线程的创建和销毁是重量级的(会由用户态切换内核态),所以尽量控制创建的数量,线程池替我们实现了根据任务创建或销毁线程。还有许多其他的功能。
- 使用线程池的时候尽量自定义。而不是使用默认的线程池,因为默认的线程池是全局的,也就是你用了默认的线程池,别人也用了磨人的线程池执行别的任务,可能造成内存的溢出,因为默认线程池是无限接受任务的。还有当生产环境线程池出问题时,日志中分辨不出是谁的任务出问题因为公用的嘛。
- 自定义线程池尽量自定义属性,比如threadFactory,自己控制创建的线程一些属性,比如起与任务相关的线程名字,很好分辨,比如拒绝策略可以依据任务自定义,线程数量也是。
线程池:
- new ThreadPoolExecutor(int corePoolSize,int maxmumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
- int corePoolSize:自定义核心线程数
- 线程池会创建出核心线程数来执行任务
- int maxmumPoolSize:最大线程数
- 当队列已满,核心线程数都在工作,会增加线程至最大核心线程数量
- long keepAliveTime:线程存活时间
- 当一个线程无事可做,超过 keepAliveTime时,线程池会判断当前运行的线程数大 于corePoolSize,那么这个线程就被停掉。
- TimeUnit unit:时间单位
- BlockingQueue<Runnable> workQueue:工作队列
- 当核心线程都已经在执行任务了,再来新的任务会放入队列中等候,等待核心线程释放再来队列中取任务,若队列足够大,就不会增加线程至最大核心数。
- ThreadFactory threadFactory:创建线程的工厂
- 线程池创建线程的来源。
- RejectedExecutionHandler handler:拒绝策略
- 当核心线程都在工作,队列已满,线程数量增至最大线程数量,此时再增加任务,该任务就会按绝策略的处理。
JUC中有定义好的具有功能的,使用方法:ExecutorService es = Executor....; es.excute(任务),他们的构造方法都是返回new ThreadPoolExecutor
- Executors.newFixedThreadPool(int nThread):
- 入参是最大线程数
- 默认使用的LinkedBlockingQueue容量为最大值2的31次方-1,容易造成OOM内存占满
- Executors.newCacheThreadPool() :
- 核心线程为0
- 最大线程数取最大值,因需创建线程
- 60秒内未使用的线程将被终止并从缓存中删除
- SynchronousQueue<Runnable>()队列
- Executors.newSingleThreadExecutor():
- 核心线程数为1,最大线程数为1,保证串行执行。
- Executors.newScheduledThreadPoolExecutor(int nThread):
- 可指定核心线程数
- 最大线程数为 2的31次方-1.
- 队列使用的DelayedWorkQueue ()
ThreadPoolExecutor源码重点:
- execute() 方法:先提交后执行
- 提交 addWorker() :自旋添加worker,优先级为:核心线程数、队列、最大线程数
- 执行runWorker() :执行优先级:空闲线程执行任务、取队列里任务执行
- 线程池执行任务不是调用.start()方法,而是线程池维护的线程去执行任务的run方法。
31、这几个队列的区别(见31)
10、线程池有哪些状态?
五种:running、shutdown、stop、tidying、terminated
11、Java多线程执行是安全的吗?
Java的内存模型、计算机内存模型都是共享内存模型是不安全的,操作系统有MESI协议保证线程安全,Java有JMM保证线程安全,也有volatile、synchronized、锁机制、内存屏障等保证线程安全。
12、计算机内存模型介绍一下?
- 计算机的线程模型是共享内存模型,为了减少cpu等待时间,提高cpu的利用率,计算机设计了缓存。
- 最常见的是三级缓存:即并发线程到主存取cache line 64字节数据到本地缓存中执行,为保证并发线程安全,计算机设计了缓存一致性协议常见的为MESI。
- MESI为四种状态的首写字母:
- M:Modify修改。E:Exclusive独占。S:Share共享。I:Invalid无效。
- 同时主线监听各线程的缓存状态,进行协调保证安全。
13、Java内存模型是什么?
JMM(Java Memory Model 、Java内存模型):
- 是一种规范,和计算机内存模型一致,Java的内存模型也是共享内存模型,且JMM是一种逻辑描述可用在多处真实物理空间。
- JMM定义了数据同步的八大原子操作:read \ load \ use \ assign \ store \ write \ lock \unlock
- 每个线程都有自己的本地缓存区,线程间共享主存资源。也具有并发安全问题,主要分为三类:
- 可见性:即同一个变量在各个线程本地会有一个副本,那一个线程修改时,其他线程是否能知道?
- 有序性:为高效利用cpu,Java编译器会进行指令重排序(依据一定规范),一段代码由单线程执行重排的指令没问题,但是同一段代码,多线程并发执行时,其中某线程的指令重排序会影响到整体。
- 原子性:上述八大原子操作的组合如何保证原子性呢?例如:把i++,分为从主存read i ,再 load i 进线程本地缓存 , 寄存器 use i 加一 ,将结果 assign i, 从本地缓存中store i,再 write i 进主存。这一系列动作的完整性也决定了数据的准确性。而计算机又是中断切换线程工作的。
14、volatile关键字
- Java层面的volatile关键字修饰变量的。
- JVM层面识别到该关键字会调用汇编语言的_LOCK_指令。
- 而_LOCK_指令在计算机层面会触发缓存一致性协议并将结果立即刷回主存。同时也禁止了指令重排序。当一个线程修改volatile修饰的变量时,会将变量状态改为I失效状态,根据缓存一致性协议,其他线程缓存中此变量为失效状态需回主存重新读取。这样保证了可见性,禁止指令重排序,保证了线程安全。
15、synchronized介绍
synchronized是Java的内置锁,也叫监视器锁:每个对象都可以被当作同步锁来使用。功能也是用于解决多线程访问临界区的共享变量的安全问题,解决执行指令的原子性问题。
Java应用层面加锁方式:
- 用synchronized修饰方法,锁对象为类的实例对象。
- 用synchronized修饰静态方法,锁对象为类对象。
- synchronized代码块参数是类的实例对象this:锁为类的实例对象。
- synchronized代码块参数是class对象,锁为类对象。
- synchronized代码块参数是任意类的实例对象:锁为那个类的实例对象。
Java字节码指令层面:
- synchronized修饰方法是通过方法的access_flag中设置ACC_SYNCHRONIZED标志
- synchronized代码块,是通过monitorenter和2个monitorexit实现
JVM内存层面,是对象的内存分布中记录着锁标志:
- 对象创建是在内存堆区分配一块空间包含:
- 对象头(Header)
- markword:8字节(8byte) = 在ARM体系结构(32位机)和8位/16位处理器体系结构中,字节的长度均为8位(8bit)
- 1bit:0 / 1
- 2bit:01 / 00 / 10 / 11
- 001为无锁锁
- 101为偏向锁
- 00为轻量级锁
- 10为重量级锁
- 11为GC可回收状态
- Klass pointer(元数据区指针:指向class文件):jdk8之后默认开启压缩模式大小为4byte
- (数组才有的)数组长度:4byte
- 实例数据区(Instance Data):对象中的变量
- 对齐填充区(Padding):以上部分字节大小如果不能被8整除,需要补位补到能被8整除
在JDK1.5版本之前没优化之前是直接到操作系统级别调用互斥原语Mutex(重量级锁):
- 向内核态申请锁,(耗时)
- 被阻塞的线程会被挂起,进入等待队列,等待重新调度。
- 导致用户态和内核态的切换,开销比较大。
在hotspot的c++源码中:
- 基于monitor机制实现管程:synchronized的wait()\notify()\notifyAll()方法基于monitor机制实现管程:monitor机制种类上分为Hasen模型、Hoare模型和MSEA模型,广泛使用的是MESA模型,Java的MSEA
- 入口等待队列:多线程进入执行前需排队,只允许一个线程进入。
- 条件变量等待队列:每个条件变量有一个队列,目的为解决线程同步问题。
- Java的monitor机制实现,是Object类中定义的wait()\notify()\notifyAll()方法的具体实现,依赖于ObjectMonitor实现,是JVM的C++实现(hotspot源码ObjectMonitor.hpp):
- ObjectMonitor中定义了_recursions:记录锁的重入次数,所以synchronized是可重入锁。
- 定义了_owner:标识拥有该monitor的线程。
- 定义了_cxq:多线程竞争锁会先存到这个单向链表构成的栈结构中:FILO。
- 定义了_EntryList:存放进入或重新进入时被阻塞的线程。
在JDK1.5版本后,进行了优化,所优化的部分就是在调用原语之前增加很多过程,比如锁升级的过程,使得尽可能的不进行内核态和用户态的切换,这部分优化:
- 自旋优化:在是否挂起(阻塞)线程前会进行自旋,自旋成功多,就扩大自旋次数,自旋失败多,就减少自旋次数。
- 锁的粗化:例如buffer.append("a").append("b").append("c")每个append都会有synchronized,此时不能加锁解锁三次,而是加锁,三个执行完最后解锁。
- 线程的逃逸分析:线程方法只在线程栈内调用,而在别处无调用时,可以无锁。
- 偏向锁延迟:虚拟机启动4秒之内的锁为轻量级锁,4秒之后才会有偏向锁。因为启动过程的许多初始化会使用synchronized,避免从偏向锁到撤销的性能问题。
- 偏向锁撤销:如果在一定时间超过20个撤销偏向锁撤销则所有偏向锁重偏向,如果第40个批量撤销偏向锁,则取消偏向锁机制,直接为轻量级锁。
16、锁的升级过程
JDK8默认开启偏向锁
- 服务启动后4秒后创建对象,默认为匿名偏向状态,即锁标志为101,但是所属线程为空。有线程执行锁代码块,则为偏向锁,所属线程为当前线程。
- 线程竞争后,升级为轻量级锁,轻量级锁在膨胀过程会初始化对象ObjectMonitor,CAS获取锁。
- 服务启动后4秒之内创建对象,默认为轻量级锁,即锁标志位为00。
- 轻量级锁CAS无法获取到锁,膨胀为重量级锁,创建monitor对象CAS+自旋获取锁,获取不到则调用操作系统pthread_cond_park/pthread_cond_timepark挂起线程释放时间片。
17、为什么要有偏向锁?
统计表示,常用sync锁过程中70%-80%情况下只有一个线程去使用锁,。没有过多竞争,没有必要升级到轻量级锁。偏向的意义在于:第一个线程拿到锁将自己的线程信息标记到锁上,下次再来就不用拿锁验证了。如果超过一个线程去抢锁,就升级为轻量级锁。
18、为什么延迟开启偏向锁?
jdk15以后默认已经禁用了偏向锁。
jdk8要在4s后开启偏向锁,因为程序在启动过程中会有很多代码执行锁操作,如果是偏向锁,就会偏向锁撤销、再升级为轻量级锁,导致效率会降低。
19、简单介绍一下AQS
AQS = abstract queue synchronizer 同步器的抽象类,juc工具包内很多线程安全工具都实现了这个抽象类,用于管理线程(管程),遵循管程模型MESA,此类定义了:
- state : 获取锁的标识
- 同步等待队列:用有界数组,构建双向链表,实现先进先出的队列结构。每个节点为一个Node,Node包含
- 线程:用于记录当前线程
- prev:前趋指针,指向前一个节点
- next:后继指针,指向后一个节点
- nextWaiter:后面的线程是哪个
- waitStatus:
- canceled = 1 代表线程已被取消
- signal = -1 代表后继的线程需要unpark被唤醒
- condition = -2 代表线程正在等待条件
- propagate= -3 代表下一个被获取的人应该无条件传播
- 条件等待队列:Condition类:Condition代替了Object监视器(synchronized使用中的wait() / notify() / notifyAll() )方法的使用,定义了:await() / signal() / signalAll() / park() / unpark()
- 加锁 : Lock.lock();
- 解锁:Lock.unlock();
20、AQS有哪些特性
- 阻塞等待队列
- 共享锁/独占锁:计数器定义有几个线程可以同时进入执行,可用作限流等功能。
- 公平/非公平锁:先进先出队列,竞争线程排队唤醒执行 / 不优先在队列中唤醒线程执行,而是是否有竞争者来,来则执行。
- 可重入:某线程进入锁内代码执行,再次遇到锁,具有通行权而无需获取/释放。
- 允许中断:某个任务只需一个线程执行完即可,无需重复执行,此时对于其他来竞争执行的线程可以采取中断。
21、简单介绍一下ReentrantLock
ReentrantLock是AQS的一种具体实现,独占锁。
- 独占锁,可重入。
- 默认是非公平的锁。创建锁对象的时候参数为true时就是公平锁的实现方式。区别在于获取锁tryAcquire时:
- 公平:先hasQueuedPredecessors()优先获取队列里的头节点唤醒执行。
- 非公平:不考虑队列,直接cas尝试修改状态。
- 可以设置超时时间,超过指定时间会抛异常。
- 支持多个条件等待队列。
- 可中断。
- 使用场景:例如扣减库存时只允许一个线程执行(如果是分布式场景需要使用分布式锁)。
22、ReentrantLock如何实现可重入?
ReentrantLock内部自定义了同步器Sync,加锁的时候,通过cas算法将线程对象放入到双向链表中,每次获取锁的时候,看下那个线程id和当前的请求线程id是否一致,一样就可以重入。
23、ReentrantLock与synchronized的异同?
ReentrantLock
- 实现的Lock,互斥同步锁
- ReentrantLock比synchronized功能更多:
- 可中断,可响应中断请求
- 带超时的获取锁
- 可判断是否有线程等待
- 可以实现公平锁
- 释放锁是显示编码释放:异常时不会自动释放锁,所以放入finally
- 竞争十分激烈场景使用ReentrantLock更好,有一些竞争时synchronized效率更高
24、什么是死锁?
指两个以上线程/进程执行过程中,竞争资源或彼此通信异常造成的阻塞,若无外力作用,都将无法继续。例如Lock.lock();后出现异常,而没有执行unlock,此时本线程异常阻塞,其他线程也无法获取到锁导致无法进行。
25、如何防止死锁?
死锁的四个必要条件,缺一不可:
- 互斥条件:(锁)进程对分配到的资源,只允许一个线程执行,其他线程只能等待,占有资源的执行完才能释放资源。
- 请求和保持:本线程获得本资源后又对其他资源发出请求,此时其他资源可能被占有,本线程被阻塞又不释放本资源。
- 不可剥夺:获取资源的只有完成使用自己释放。
- 环路等待:发生死锁后若干线程循环等待。
避免条件成立即可预防死锁,核心是打破循环等待,可以设置拉长时间来排期锁的释放等。
26、什么是悲观锁、乐观锁?
- 总是假设坏的情况-悲观锁:
- 认为访问者每次去访问公共资源都会有别的访问者会修改数据,必须拿到资源(锁)才能去访问共享资源,如果没拿到就需要等待,直到资源释放。
- 关系型数据库常用此锁,Java中的synchronized和ReentrantLock都是这种悲观锁的实现。独占锁。
- 写多的情况更适合使用悲观锁。
- 总是假设好的情况-乐观锁:
- 每次去拿都认为不会有别人修改数据,所以不会上锁,但是在更新的时候会判断一下此期间是否有别人修改数据。可以用版本号+CAS算法实现。
- 乐观锁适用于读多的操作,这样有利于提高吞吐量。
- 数据库的read_condition机制是乐观锁CAS的实现。
- 乐观锁缺点
- ABA问题是乐观锁的常见问题,解决办法使用版本号机制:JDK1.5之后Atomic StampedReference类中conpareAndSet就使用了版本号
- 循环时间长,如果长时间自旋不成功,cpu开销过大
- 只对单个共享变量有效:当操作跨多个共享变量CAS操作时无效:JDK1.5之后AtomicReference类来保证引用对象之间的原子性,即可以把多个变量放一个对象里来进行CAS操作,(相当于把多个共享变量变成一个共享变量)。
27、什么是CAS?简单介绍一下
compare and swap 比较与交换,是无锁算法,即无锁的情况下实现多线程之间的变量同步,也就是没有线程被阻塞的情况下实现变量同步,涉及到三个操作数:
- 内存值V
- 进行比较的值A
- 拟写入的值B
当且仅当V=A时才用B替换V,否则不会执行任何操作,一般会有自旋不断尝试重试。
28、CAS会有ABA问题如何解决?
ABA问题就是:内存值V第一次读到的值是A,当赋值时检查也是A,并不代表始终是A值,有可能1线程将其改为Z,2线程将其改为A。而CAS是无法感知这一切的。
解决办法:就是使用版本号,版本号可以为自增的函数等,当预期值为A,版本号也是预期的版本号,再修改,可保证无ABA问题:AtomicStampedReference,其中的compareAndSet就是使用了版本号。
29、说说JUC工具包中的Semaphore。
semaphore是juc中的共享锁的实现,即使用信号量控制执行线程的数量。(操作系统中也有此概念:P表示通过,V表示释放,信号量又可以理解为资源数)。
与独占锁(reentrantLock)的主要不同点在于:state 和 唤醒方法。
- 独占锁的state只有0和1,标识占有或释放;而共享锁state可以是4,可以是8,作为一种许可证数量的标识,来一个线程就可以减一,走一个线程释放资源就可以加一。
- 没有获取到资源的线程,同样会进入到同步阻塞队列。而唤醒时:
- 独占锁是唤醒头节点(waitStatus为-1)的下一个节点里的线程。
- 共享锁在此基础上,会传播,也就是继续向下唤醒。
30、说说JUC工具包中的CyclicBarrier。
CyclicBarrier可以叫它循环栅栏,几个线程同时到达时同时执行。实现此功能是利用的AQS中的条件等待队列Condition。(可是想要使用条件等待队列,前提必须是在锁内,即lock,unlock之间使用await() )。
主要构成:
- count 和 parties 都是记录需要控制几个线程,count在执行中变化,而parties作为副本一样的存在,是一个栅栏用完之后(count清零)再来一个栅栏时用到的计数副本。
- 条件等待队列:
- 创建节点时,waitStatus=-2;是个单链表。
- 没获取到执行资格的线程入队阻塞后state为0,即释放锁。
- 线程唤醒出队后需从条件等待队列转移到同步等待队列,去再次获取锁,继续执行完任务后释放锁。
- 条件队列转同步队列
- 条件等待队列为单链表:头出 :waitStatus从-2 改为0 (signal)
- 同步等待队列双链表:尾插 :waitStatus从0 改为-1 (unpark)
- 等待被唤醒
31、说说JUC中的线程安全队列
JUC包中提供的队列是线程安全的,因为底层已使用加锁等机制,不用自己加锁保证线程安全了,它们的顶层接口是Queue(java自带的),根据不同的数据结构和功能,分为以下几种:
- ArrayBlockingQueue(数组队列):
- 数据结构是array数组,也就是有界,在内存中固定的一块区域。
- 先进先出,即公平的。
- 保证安全是使用了reentrantlock,当满了再有put() / 空了再有take()会await阻塞等待,但是put() / take() 共用的一把reentrantlock锁。
- 有个特点就是环形数组:即下标取到数组的末尾之后下一个取的是数组的首位。取出的指针也如此。
- LinkedBlockingQueue(单向链表队列):
- 数据结构是单向链表,默认大小是Integer.MAX_VALUE:容易导致OOM
- 未插头出,先进先出,即公平。
- 线程安全也是使用了reentrantlock,满了再有put() / 空了再有take()会await阻塞等待,但是put() / take() 各用一把reentrantlock锁。性能因此提升,存取互不干扰,提高了吞吐量。
- LinkedBlockingDeque(双端队列):
- 数据结构是双向链表,做到了双端链表。
- 即可头插或尾插,可头取或尾取,可公平可非公平。
- 保证安全是使用了reentrantlock,当满了再有put() / 空了再有take()会await阻塞等待,但是put() / take() 共用的一把reentrantlock锁。
- SynchronousQueue(同步队列):
- 数据结构有两种选择(但都是单向链表):
- 采用的栈结构,即先进后出,非公平。
- 采用队列,即先进先出,公平。
- 保证线程安全使用的是CAS+自旋。
- 特点是数据交换:即构建Node节点,生产者将任务赋值给Node中的item变量,消费者取item的值消费任务。
- PriorityBlockingQueue(优先级队列):
- 数据结构采用数组+二叉堆(默认是小顶堆):可指定容量,会自动扩容。
- 特点:随机put(),但是take()按优先级出队
- 入队时会将小元素上浮到堆顶(即数组下标最小的位置),出队时大元素下沉取出的始终为相对最小的那个元素。
- 可自己实现比较器。
- DelayQueue
- 底层使用的PriorityBlockingQueue,此时的优先级为时间过期与否。
- reentrantlock保证线程安全
- 应用于超时关闭、异步通信、延时清空缓存等。
ThreadPoolExecutor中对线程安全队列的选取与使用如下:
- newFixedThreadPool(int nThread):
- 使用的LinkedBlockingQueue
- newCacheThreadPool() :
- 使用的SynchronousQueue<Runnable>()队列
- newSingleThreadExecutor():
- 使用的LinkedBlockingQueue
- newScheduledThreadPoolExecutor(int nThread):
- 使用的DelayedWorkQueue ()
32、ForkJoin了解吗?
forkjoin框架是对传统线程池的补充,主要思想为分而治之,适合于cpu密集型任务,它解决了传统线程池两大问题:
- 无法对大任务的拆分,执行任务时是单线程执行。
- 工作线程从队列中获取任务时存在竞争。
工作原理:
- 工作队列workQueue:数组实现的双端队列,先进后出,因为要实现递归。
- 工作线程:
- 每个线程维护一个工作队列workQueue
- 窃取任务:CAS当前线程的工作队列为空时就到隔壁线程的队列中窃取任务执行。
总结
java并发编程是很基础庞大的模块,主要都是围绕着并发安全问题展开。未来学到相关新知识再来补充,也欢迎小伙伴补充👏🏼