并发

作者: sizuoyi00 | 来源:发表于2022-05-06 17:11 被阅读0次

多线程

什么是上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

引起线程上下文切换的原因
对于我们经常使用的抢占式操作系统而言,引起线程上下文切换的原因大概有以下几种:
当前执行任务的时间片用完之后,系统CPU正常调度下一个任务
当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一任务
多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务
用户代码挂起当前任务,让出CPU时间
硬件中断

线程的实现-了解

1.内核线程:直接使用操作系统内核支持的线程,由内核操作
程序一般通过内核线程的接口-轻量级进程,每个轻量级进程对应一个线程。
缺点:1.基于内核实现,系统调用代价高,需要在用户态和内核态切换。2.耗费内核资源。
2.用户线程:非内核线程则为用户线程。用户线程的建立、同步、销毁等在用户态处理,内核无感知。
缺点:要考虑线程的创建,切换,调度
3.用户线程加轻量级进程混合实现

java线程调度

协同式:线程时间自己控制,处理好了通知系统切换线程。
好处:简单,无同步问题。
坏处:线程执行时间不可控制,代码有问题则会一直阻塞住。
抢占式:线程时间系统控制,线程切换系统控制(Thread.yield)
好处:线程时间可控,不会出现一个线程阻塞程序的情况

可通过设置线程优先级建议给某些线程多分配时间
优先级并不完全靠谱,因为其基于系统原生线程实现,取决于操作系统。
1.操作系统优先级与java优先级不一定能一一匹配。
2.如windows有"优先级推进器",活跃线程会多分配时间。

java线程状态
1.新建(New):创建后尚未启动的线程处于这种状态。
2.运行(Runnable):包括操作系统线程状态中的Running和Ready ,也就是处于此状态的线程有可能正在执行,也有可能正在等待着操作系统为它分配执行时间。
3.无限期等待(Waiting):处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线程显式唤醒。

以下方法会让线程陷入无限期的等待状态:
没有设置Timeout 参数的Object::wait ()方法;
没有设置Timeout 参数的Thread::join()方法;
LockSupport::park()方法。

4.限期等待(Timed Waiting):处于这种状态的线程也不会被分配处理器执行时间,不过无须等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒。

以下方法会让线程进入限期等待状 态:
Thread::sleep()方法;
设置了Timeout 参数的Object::wait ()方法;
设置了Timeout 参数的Thread::join()方法;
LockSupport::parkNanos()方法;
LockSupport::parkUntil()方法。

5.阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;
而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种阻塞状态。
6.结束(Terminated):已终止线程的线程状态,线程已经结束执行。

java中有几种方法可以实现一个线程?
继承Thread,实现Runnable,实现Callable+FutureTask实现有返回的线程

如何停止一个正在运行的线程?
interrupt()并不能马上停止,其相当于为该线程打一个停止的标识,如T1正在运行,其他线程main调用了t1.interrupt(),但是T1是否能够停止,依赖于其当前运行状态,只有当其运行到某个可以暂定的地方才有可能停止
suspend()暂停,resume()继续,stop()停止方法,已被废弃
废弃原因:suspend不会释放锁,容易引发死锁问题,stop没有给线程完成释放工作的机会,导致工作状态不确定。由等待wait/通知notify机制实现

如何安全的停止一个线程
通过中断来取消或停止任务(简单的通过sleep1ms来使线程感知到中断),也可以利用一个变量标志位来控制是否需要停止任务并终止线程。通过标识符或中断的方式能够使线程在终止时有机会取清理资源,比较安全。

线程间通信
1.volatile共享/sync同步(Monitor监视器)
2.等待/通知机制

wait()与wait(timeout)方法详解
对象加锁synchronized关键字底层会转换为两条指令monitorenter,monitorexit,T1线程加锁,就是monitorenter成功,此时T2线程想要加锁,发现对象锁已经被占用了monitorenter失败,此时会进入一个同步队列SynchronizedQueue,T2线程状态变为阻塞状态;当T1执行完同步代码块的代码时,会走到monitorexit指令,走到这里后会发一个通知,也就是会唤醒同步队列中的线程出队列进行抢占锁。

那么wait是什么时间发生的呢,wait发生在T1加锁成功,也就是monitorenter成功时,如果调用wait方法,这时候会放弃/释放锁,并将线程T1转移到对象的等待队列WaitQueue中,线程T1状态变为等待状态。当其他线程T2抢占到锁并释放时,并不会唤醒其线程T1,只有持有锁的其他线程T2主动调用notify/notifyAll才会唤醒该线程T1,唤醒后该线程T1后由waitqueue转移到syncqueue中,状态变为阻塞状态,注:notify不会释放锁,执行完线程代码才会释放锁。

除了wait()外,还有wait(timeout)方法,其与wait方法的区别在于,wait()状态为等待WAITTING,wait(timeout)状态为超时等待状态TIMED_WAITTING;另外wait(timeout)无需其他线程显示唤醒,而是在一定时间后由系统自动唤醒,当然该方法也会释放锁,也需要进入waitqueue,唤醒后转移到syncqueue中进行新一轮锁的抢占。

waitqueue与syncqueue区别
syncqueue发生在抢占锁时,抢占失败进入monitor监视器的syncqueue中,对应线程阻塞状态,当锁被释放时,会唤醒syncqueue中的线程进行竞争锁 waitqueue发生在抢占锁成功时,调用wait/park方法,释放锁进入对象的waitqueue中,对应线程等待状态;其他线程抢占到锁后释放时,并不会唤醒该线程,只有主动调用notify/notifyAll才会唤醒该线程,唤醒后该线程后由waitqueue转移到syncqueue中,状态变为阻塞状态。
LockSupport.park也是进入waitqueue中。--ReentrantLock使用

sleep方法与wait方法区别
sleep方法是线程的方法,wait是对象的方法
sleep方法可以在任何地方使用,不需要占用锁,也不会释放锁;

wait需要在同步块中使用,会释放锁。 为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里? 因为线程加锁实际上锁的是对象,每个java对象都有一个锁(Monitor监视器),而wait(), notify()和notifyAll()都是锁级别的操作,所以定在在Object类中,作为线程通信的基本方法。如果定义在线程里,线程在等待哪个锁就不知道了。

为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用?

介绍上边的流程,且wait(),notify()和notifyAll()都是锁级别的操作,并且发生时机是在加锁成功后触发,所以要在同步代码块执行,否则会抛出IllegalMonitorStateException异常。

notify()和notifyAll()有什么区别?
notifyAll会唤醒waitqueue中所有的线程,notify会随机唤醒一个线程。

join方法
线程T1启动,T1中执行了thread2.join(),表示线程T1会等待T2执行完以后继续执行,底层join是调用了wait()方法。

什么是Daemon线程?它有什么意义?
后台提供服务的线程,jvm不可缺少的一部分,当所有非后台线程结束时,程序就终止了,同时会杀死所有的后台线程。我们可以通过setDaemon方法来设置一个线程为后台线程,要在启动之前设置。在构建后台线程时,不能依靠finally块内容确保执行关闭或清理资源的逻辑。

java如何实现多线程之间的通讯和协作?
volatile共享数据
sync同步线程--wait/notify线程协作
blockingqueue阻塞队列/Conditon--
LockSupport.park/LockSupport.unpark

当一个线程进入某个对象的一个synchronized的实例方法后,其它线程是否可进入此对象的其它方法?
一个线程在访问一个对象的同步方法时,另一个线程能同时访问这个对象的另一个非同步方法。
一个线程在访问一个对象的同步方法时,另一个线程不能同时访问这个对象的另一个同步方法。
一个线程在访问一个对象的同步方法时,另一个线程不能同时访问这个对象的同一个同步方法。

可以直接调用Thread类的run()方法么?
可以,只是一个普通方法,会在当前线程执行,只有调用start方法才会在相当于在新的线程执行我们的代码

你对线程优先级的理解是什么?
线程优先级只是具有优先权,但结果并不完全绝对。
1.如操作系统有关,java的优先级级别与不同操作系统的优先级不一定可以一一对应。
2,windows就有优先级推进器,会识别积极的线程,会越过优先级为其分配执行时间

在多线程中,什么是上下文切换
上下文切换时存储和恢复cpu状态的过程,使得程序执行能够从中断点恢复执行。

什么是死锁(Deadlock)?如何分析和避免死锁?
两个线程永远阻塞的情况。
jstack可查看死锁。 通过避免嵌套锁避免

如何在两个线程间共享数据?
共享对象,阻塞队列 如何检测一个线程是否拥有锁 Thread::holdsLock方法,返回boolean,一个native方法

Thread类中的yield方法有什么作用?
将当前线程从执行状态变为就绪状态,接下来会与其他线程一起竞争锁从就绪状态进入执行状态。

线程池
https://www.jianshu.com/p/4087922d88f1
1.如果任务队列为无界队列
线程数<核心线程数:创建核心线程 核心线程数<线程数<最大线程数:加入队列 线程数>最大线程数:加入队列
无界队列,创建核心线程->队列
2.如果任务队列为有界队列
线程数<核心线程数:创建核心线程 核心线程数<线程数<最大线程数:加入队列,队列满后,创建非核心线程 线程数>最大线程数:队列未满,加入队列;队列满后,拒绝策略
有界队列,创建核心线程->队列->创建非核心线程 任务完成后,闲置时间达到超时时间后,非核心线程会被清除
3.如果任务队列为无缓存队列SynchronousQueue
线程数<核心线程数:创建核心线程 核心线程数<线程数<最大线程数:创建非核心线程 线程数>最大线程数:拒绝策略
无缓存队列,创建核心线程->创建非核心线程 任务完成后,闲置时间达到超时时间后,非核心线程会被清除


线程安全 什么叫线程安全?servlet是线程安全吗?
当多个线程访问一个对象时,不需要考虑线程的交替执行,不需要同步,不需要任何协调操作,该对象都可以得到一个正确的结果,那么该对象就是线程安全的。
判断是否线程安全,主要使判断是否存在共享数据。

同步有几种实现方法?
阻塞同步:共享数据在同一时段只能有一个线程使用,synchronized同步,Lock同步。坏处:线程唤醒阻塞性能问题
非阻塞同步:默认多数场景不会有太高并发-CAS
无同步方案:不涉及共享数据,线程本地存储ThreadLocal

**请说明下java的内存模型及其工作流程。 **
jmm定义了程序中各个变量(共享数据如实例对象,静态字段,不包含局部变量)的访问规则,即虚拟机中将变量存储到内存和从内存取出数据的底层细节。
jmm规定所有的变量存储在主内存中,对应硬件的主内存;每条线程对变量的所有操作(读取,赋值等)必须在工作内存中进行,而不能直接读写主内存中的变量;线程间的变量不可直接访问,线程间值的传递需要通过主内存类完成。

什么是缓存一致性协议?
cpu与内存的交互-高速缓存:由于cpu处理速度远远大于内存读写速度,解决方案就是在cpu和内存之间增加高速缓存,高速缓存速度快内存小。这样下计算机cpu与内存的交互就变为了程序运行时会先查找高速缓存数据,找不到再去内存中查找并复制到cpu的高速缓存中,后边使用时直接从高速缓存中读取,运算结束将高速缓存的数据刷新到主存中。高速缓存也慢慢发展为了一级缓存到二级缓存到三级缓存最后到主存依次查找的形式。L1缓存容量最小速度最快,L2缓存容量升级些速度慢些,L3容量再大些速度更慢些。 高速缓存带来的问题-数据不一致:通过高速缓存解决了cpu处理速度与内存读写速度不一致的问题,也带来了问题,当多线程多核处理时,其中一个核修改了主存后,其他核不知道缓存的数据已经失效,导致数据不一致的问题。

缓存一致性就是为了解决由于cpu处理速度远远大于内存读写速度而导致的误差 如何解决数据不一致问题:也就是缓存一致性协议。缓存一致性协议以MESI最出名,mesi分为四个状态,m修改e独占s共享i失效。
流程是这样的:
1.t1获取主存变量x,标为e独占状态,同时监听bus其他线程是否有对变量x的操作;
2.如果t2读取了变量x,监听机制会起作用,t1与t2会将变量x修改为s共享状态;
3.如果t1修改了变量x,先锁住变量缓存行,将变量x修改为m修改状态并向bus发送消息,此时t2会监听到bus消息中其他线程要修改变量x,t2会将变量x修改为i无效,需要重新读取主存变量x;
4线程t1修改完变量x后,将状态修改为e,并写回主存变量x新值。 volatile可见性就是依赖于缓存一致性

**volatile有什么用?能否用一句话说明下volatile的应用场景?
轻量级的同步机制,所有线程可见性,禁止指令重排。
多线程开关标识。

volatile特殊规则
轻量级的同步机制
1.所有线程可见性
一条线程修改了该值,其他线程立即可知,普通变量线程间传递需要通过主内存完成。
运算结果并不依赖变量的当前值如正例a=1 反例a++,场景如volatile修饰变量做并发开关
2.禁止指令重排
指令重排:只要程序的最终结果与其顺序执行的结果一致,那么指令的执行顺序可以和代码不一致。
为什么要指令重排:jvm根据处理器性能,通过指令重排,最大限度的发挥机器性能
as-if-serial:不管怎么重排序,线程的执行结果不能变。基于此原则,编译器和处理器都不会对存在数据依赖关系的操作进行重排序,因为会改变结果

通过内存屏障实现禁止指令重排,内存屏障有两个作用
1.保证特定操作的执行顺序,在指令间插入一条内存屏障,则其后边的指令不能重排序到内存屏障前边
2.保证某些变量的内存可见性,内存屏障底层指令lock开头,会将本处理器的缓存写进内存(store+write),引起其他处理器无效化(I)其缓存,即各处理器各线程都会读取到最新的数据。(volatile可见性缘由)

禁止指令重排应用:双重锁单例模式,字节码指令(1分配对象内存,2构造器初始化,3将对象引用赋值给变量)。
23不存在数据依赖,改变顺序对结果无影响,多线程可能会指令重排,如果顺序变为1分配对象内存,3将对象引用赋值给变量,2构造器初始化,当t1执行到3时,t2刚好进来,发现对象不为null已经有值了,访问该对象,然而该对象还没初始化,则t2的该数据会有问题。通过volatile禁止指令重排解决该问题
双重锁单例模式禁止指令重排参考:https://www.cnblogs.com/goodAndyxublog/p/11356402.html

内存间交互操作

原子操作-lock锁定 unlock解锁 read读取 load加载 user使用 assign赋值 store存储 write写入
主内存->工作内存 read-load
工作内存->主内存 store-write
double与long非原子协定,虚拟机将64位数据当作原子操作对待,因此编写代码不需要将long和double变量专门声明为volatile

规则
read-load、store-write成对,表示主内存-工作内存读写变量
assign赋值后必须写会主内存,无assign则不可以将数据同步到主内存
一个变量只能从主内存产生,不允许再工作内存使用一个未被初始化(load或assign)的变量
一个变量同一时间只允许一条线程进行lock操作,lock-unlock成对出现
对一个变量进行lock操作,lock会清空工作内存中此变量的值,执行引擎会重新执行load或assign操作初始化变量的值
对一个变量执行unlock执行,需要先把该变量同步到主内存中



线程安全:当多个线程访问一个对象时,不用考虑线程的调度交替执行,不需要同步,不需要任何协调操作,调用该对象都会返回一个正确的结果,那么该对象就是线程安全的。
多线程执行一个方法时,方法局部变量并不是临界资源,因为局部变量存在于每个线程的私有栈中,不具备共享性,不会导致线程安全问题。
多个线程访问一个共享数据才会有线程安全问题。

线程安全实现:同步和锁机制
互斥/阻塞同步:共享数据在同一时段只能有一个线程使用
坏处:线程间唤醒和阻塞带来的性能问题
实现:synchronized,ReentrantLock
选择:关于sync与Lock的选择,jdk1.5以前,sync性能差很多,1.6之后已经优化了很多,性能差别不大。
sync是一个关键字,可自动释放锁,作为关键字没法设置超时时间,所以可能会导致线程为了加锁一直在阻塞,也不知道加锁的结果,不可中断;lock是一个接口,需要手动释放锁,作为接口有方法可以设置超时时间,也可以确定是否有成功加锁,interrupt可中断。

非阻塞同步:基于冲突检测的客观锁并发机制。
思想:默认大多数情况不会有很高的并发,所以先执行操作,如果没有线程争用共享数据,则操作成功,没有争用,则重试到成功为止。
好处:不需要进行线程的挂起
前提:操作与冲突检测保证原子操作--CAS(Compare and Swap)
无同步方案:不涉及共享数据
可重入代码:方法结果稳定,不依赖堆上数据和公共资源

线程本地存储:ThreadLocal将共享数据存入到线程本地存储中,如一个web请求对应一个服务器线程。

互斥/阻塞同步-synchronized

原理:synchronized编译为字节码文件会被jvm翻译成两条指令,monitorenter+monitorexit,分别在同步代码块的起始位置和终止位置。这两个命令都需要指定要锁定和解锁的对象。
同步实例方法:锁当前实例对象(必须单例);
同步静态方法:锁当前类对象;同步代码块:
锁是括号里的对象。
synchronized由于是给对象加锁,所以想要跨方法执行(如方法1加锁,方法2解锁),可以使用unsafe给对象跨方法加montor锁,等于sync关键字。

锁控制原理:执行monitorenter指令,首先获取对象头中的锁,如果当前对象没有被锁或者当前对象已经有锁,将锁的计数器+1,对应monitorexit指令会-1。计数器为0时,锁会释放。如果获取对象锁失败,当前线程就会阻塞,直到对象锁被其他线程释放。

特点:可重入,锁粗化,锁消除,锁优化
锁状态:无锁,轻量级锁,偏向锁,自适应自旋锁
缺点:性能差(用户态核心态转换);

锁控制原理图
_count :计数器,可重入用 _owner:占用锁的线程
_spinFreq:是等待锁期间自旋的次数 _spinclock:是自旋的周期
_entrylist:这个就是自旋次数用完了还没获取锁,只能放到_entrylist等待队列挂起了(上边说的syncqueue)
线程进入syncQueue后等到锁被释放后再次抢占锁,这里底层涉及到上下文切换,用户态和内核态的切换,所以先自旋尝试获取锁

[图片上传失败...(image-865488-1651731204247)]

锁消除
JIT编译时,将代码用到了同步块,实际检测不存在共享数据竞争的锁进行锁消除
1.如StringBuilder.append()方法为同步方法,但如果某方法内的StringBuilder对象属于局部变量,且不会被其他线程访问,即不存在竞争,jvm会进行锁消除
2.在一段代码中,堆上所有数据都不会逃逸出去被其他程序访问,即数据线程私有,该加锁方法也会被消除。

锁粗化

一般同步块作用范围会限制的尽量小,但是有些情况比较例外,如循环内加锁,StringBuilder.append()导致不必要的性能损耗。如StringBuilder.append()会将锁粗化到整个操作的外部,作用到第一个append之前到最后一个append之后,加锁一次。

锁优化

[图片上传失败...(image-3c0338-1651731204247)]

无锁
->偏向锁:偏向于第一个获得它的线程,适用于单一线程反复执行。只要有其他线程竞争锁立即升级
->轻量级锁:线程竞争不激烈,CAS抢占共享数据,其余线程自旋(默认10次)/自适应自旋(根据上次同一个锁字段时间决定自旋次数)抢占共享数据,自旋一直没有抢到,则代表占用锁线程没有执行完,此时jvm会升级为重量级锁并挂起线程,等待系统重新唤醒。自旋适用场景:占用锁时间不会很长。
->重量级锁:互斥量monitor,见上文锁控制原理。

锁膨胀升级详细过程
https://blog.csdn.net/chenzengnian123/article/details/122683264 sync中 mark word 关联 monitor
1.无锁状态,无锁对象mark word如下

对象哈希码 分代年龄 偏向模式=0 标志位=01

2.此时T1访问同步块,判断mark word是否有锁=01无锁,是否偏向=0无偏向,CAS修改mark word(标志位=01,偏向模式=1,将当前线程id记录在对象头mark word中,如图),然后执行同步块逻辑中......

线程ID=T1 分代年龄 偏向模式=1 标志位=01

3.此时T2访问同步块,判断mark word是否偏向=1有偏向,判断线程ID是否为自己,是自己不会进行任何同步操作,如加锁、解锁、更新mark word。
不是自己则CAS尝试修改线程ID=T1,赌一波T1已经执行完毕,大概率修改失败,然后jvm会发进行撤销偏向锁;
3-1.如果T1这时已经执行完了同步块逻辑,T1将进行解锁,将mark word线程清空,偏向模式恢复为0

分代年龄 偏向模式=0 标志位=01

T1解锁后,T2则可以直接CAS进行偏向重复上述动作

线程ID=T2 分代年龄 偏向模式=1 标志位=01

3-2如果T1这时没有执行完同步块逻辑,将升级为轻量级锁。

4.偏向锁升级为轻量级锁,首先会在栈桢中开辟一份空间锁记录Lock Record(解锁用),将mark word拷贝过来

T1线程Lock Record此时复制的mark word如图

分代年龄 偏向模式=0 标志位=01

T2线程Lock Record此时复制的mark word如图

分代年龄 偏向模式=0 标志位=01

线程T1进行CAS修改mark word,使mark word指向当前线程Lock Record
此时对象本身的mark word如图

指向T1线程Lock Record的指针 标志位=00

线程T2进行CAS修改mark word,发现mark word发生了变化,标志位01变为了00,其他信息变为了指向T1Lock Record的指针,更新失败,进入自旋

4-1 T2多次自旋时,T1执行完了同步块逻辑,T1解锁,C=判断mark word是否指向自己,CAS修改成功,对象mark word恢复为当时复制的Lock Record,此时对象的mark word恢复为原无锁状态,T2下次自旋再次竞争锁。

分代年龄 偏向模式=0 标志位=01

4-2 T2多次自旋时,T1没有执行完同步块逻辑,T2自旋失败,jvm会将T2线程挂起,并触发轻量级锁升级为重量级锁即标识改为10,并将mark word指向重量级锁Monitor对象,此时对象mark word如图

指向重量级锁的指针(Monitor监视器地址) 标志位=10

过后T1执行完同步块逻辑,开始解锁,C=判断mark word是否指向自己,CAS修改失败,说明有其他线程T2在尝试获取过该锁,且被挂起中,T1会将对象锁去除(实际就是对象mark word恢复为当时复制的Lock Record),并会唤醒被挂起的线程T2,T2被唤醒后开始再次竞争锁。解锁后对象mark word如图

分代年龄 偏向模式=0 标志位=01

升级为重量级锁后,对象头-mark word-monitor关系

[图片上传失败...(image-e97f57-1651731204247)]

什么是可重入锁(ReentrantLock)?
重入锁好处 支持重复进入的锁,也就是可以支持一个线程对资源的重复加锁。通过对同步状态值state属性的控制,加锁state+1,解锁state-1,state=0释放锁。 可以解决死锁,防止自己等待自己

乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
悲观锁:认为每次都会有人修改数据,所以每次都要进行上锁,进行阻塞互斥,即同一时间只能有一个线程调用,sync,lock都是悲观锁的实现,适用于并发高的情况。
乐观锁:认为一般不会有人修改数据,所以不会上锁,通过CAS实现,适用于并发少的情况。

两个线程交替打印奇偶数
https://www.jianshu.com/p/8d40ef55e301?utm_source=oschina-app https://www.cnblogs.com/Jansens520/p/6564750.html http://ifeve.com/java-multi-threading-concurrency-interview-questions-with-answers/


互斥/阻塞同步

**Lock ReentrantLock对比synchronized **
1.可知道加锁结果,并且可设置指定时间尝试加锁
2.必须要人工释放锁,所以写法是固定的,try{lock.lock();}finally{lock.unlock();} 内部基于AQS,AQS全称AbstractQueuedSynchronizer抽象队列同步器,AQS内部定义了一套多线程访问共享资源的同步器框架,是一个依赖状态的同步器。
公平锁与非公平锁 定义了一个Sync内部抽象类继承AQS,Sync有两个子类,一个是公平锁,另一个是非公平锁,通过构造方法来选择两种锁。
AQS定义了两种队列,CLH等待队列,双向队列,是原生CLH队列的一个变种,线程由原自旋机制改为阻塞机制;Condition条件队列,单向队列,使用场景阻塞队列;
公平锁与非公平锁的区别就是,公平锁加锁的时候会判断CLH队列是否有等待的节点,然后再加锁,非公平锁则会直接加锁。
加锁与解锁方法 lock()如果没有锁被持有,直接加锁,如果被当前线程持有,AQS.state+1,如果被其他线程持有,等待直至加锁成功 trylock()尝试加锁,成功直接返回true,否则返回false;(加锁通过CAS更新state,并比较是否为当前线程) tryLock(timeout)可以通过入参指定时间内尝试加锁,也可以通过trylock的出参确定是否加锁成功,这个对比sync的一大优势。 当获取锁不成功,将线程加入CLH等待队列进行阻塞,知道占用锁的线程执行完毕(调用unlock)释放锁才会被唤醒。

AQS阻塞线程使用的是LockSupport.park()方法,底层通过Unsafe类实现,调用系统内核功能pthread_mutex_lock阻塞线程。(对照sync-wait) unlock释放一个锁,并唤醒一个阻塞线程LockSupport.unpark(s.thread)。park/unpark相当于一个许可证,unpark许可不可叠加的,park等待许可。(对照sync-notify/notifyAll)

Reentrantlock.lock详解
1.lock--cas判断aqs.state (公平锁会判断是否有等待队列,有则加入末尾)
1.1.加锁成功,设置当前线程
1.2.失败,再次尝试或者线程进入等待队列
2.acquire--尝试获取锁,失败的线程放入AQS管理的CLH等待队列进行等待,并且将线程挂起
2.1.cas判断aqs.state || 判断线程是否自己-重入(加锁成功)
2.2加锁失败--添加线程到CLH队列
3.acquireQueued--–锁竞争优化,为了减少线程挂起、唤醒次数而作的优化操作
3.1前置节点=head,当前线程为最大优先级线程,再次尝试加锁(最后一次尝试)
3.2判断是否要阻塞,阻塞方法LockSupport.park

可重入 通过底层AQS的state实现锁的重入,默认为0,加锁时通过cas state+1修改状态,释放锁时state-1

https://mp.weixin.qq.com/s?__biz=MzI3MTUyMTY2Mw%3D%3D&chksm=eac1c3acddb64abacdc1c89a49eac1d01febaded5b9742727964cd7360e3d3275fcd746620f8&idx=1&mid=2247483836&scene=21&sn=b882216f3de52e930275e7a801f58f8c#wechat_redirect


非阻塞同步CAS
CAS指令执行时,当且仅当V符合预期值A时,处理器用新值B更新V的值,否则不执行操作,该过程是一个原子操作。对应底层cmpxchg指令。 CAS对应java程序,sun.misc.Unsafe类里的compareAndSwapXxx()方法提供。 CAS一般与volatile配合使用,保证原子性+可见性。

ABA问题 如果变量V初次读取时是A值,但是中间改为了C值,然后又改为A值,此时CAS判断变量V是符合预期值A的,这个漏洞被称为ABA问题。

ABA问题解决 1.加版本概念,除了比较值,还要比较版本。juc提供了AtomicStampedReference(E,Integer)类,通过控制变量的版本来解决ABA的漏洞。 2.改用阻塞互斥同步方案 AtomicXxx类 原子操作CAS,随意点开会发现其value都是volatile修饰的,就是为了保证原子性+可见性。


线程池

https://www.jianshu.com/p/4087922d88f1
1.如果任务队列为无界队列
线程数<核心线程数:创建核心线程 核心线程数<线程数<最大线程数:加入队列 线程数>最大线程数:加入队列
无界队列,创建核心线程->队列
2.如果任务队列为有界队列
线程数<核心线程数:创建核心线程 核心线程数<线程数<最大线程数:加入队列,队列满后,创建非核心线程 线程数>最大线程数:队列未满,加入队列;队列满后,拒绝策略
有界队列,创建核心线程->队列->创建非核心线程 任务完成后,闲置时间达到超时时间后,非核心线程会被清除
3.如果任务队列为无缓存队列SynchronousQueue
线程数<核心线程数:创建核心线程 核心线程数<线程数<最大线程数:创建非核心线程 线程数>最大线程数:拒绝策略
无缓存队列,创建核心线程->创建非核心线程 任务完成后,闲置时间达到超时时间后,非核心线程会被清除

线程池为什么需要使用(阻塞)队列
因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换,创建线程池的消耗较高。

线程池为什么要使用阻塞队列而不使用非阻塞队列?
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。--通过park/unpark
当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。
使得在线程不至于一直占用cpu资源,使得程序空跑


阻塞队列
https://www.jianshu.com/p/568220b92f03

原理
生产者生产数据
add(E e)有空间则插入,没有报错
offer(E e)有空间则插入,返回true,没有空间返回false
put(E e)有空间则插入,没空间则阻塞至有空间
offer(long timeout, TimeUnit unit)有空间则插入,没空间则等待指定时间,返回false

消费者消费数据

take()获取队首元素并删除,没有则阻塞至有数据
poll(long timeout, TimeUnit unit)获取队首元素并删除,没有则等待指定时间,返回null
retail项目BlockQueue消费频率

生产者:订单消息/商品消息->10s去京东查一次->扔到LinkedBlockingQueue里
一般订单消息我们会关注多种类型的消息,所以每次会取多条消息,至于10s查一次是根据业务的量来做的调整。

消费者:while(true)获取消息->LinkedBlockingQueue有消息则消费,没消息则会阻塞,也可以设计为多个线程、定时任务的方式。
设计为while(true)+阻塞队列是由于最开始接收商品消息过多,消费来不及,就换为了多线程+while(true)方式;为了防止生产者过低导致消费者空跑浪费服务器资源,就增加了LinkedBlockingQueue做阻塞处理。

ArrayBlockingQueue 生产者与消费者公用一个锁控制数据同步
生产->队列满了->阻塞
消费->队列空了->阻塞
生产与消费是交替进行的,因为是同一把锁。
LinkedBlockingQueue 生产者与消费者采用独立锁控制数据同步
生产->队列满了->阻塞
消费->队列空了->阻塞
生产与消费是同时进行的,有生产就可以消费。

SynchronousQueue无缓冲队列
生产->消费(未消费则阻塞生产)
生产者生产一个必须等待一个消费操作,否则不能继续添加元素

PriorityBlockingQueue优先级无界队列,由传入的Compator决定优先级
生产->消费->消费空了->阻塞
生产不阻塞,消费空了阻塞,生产者生产过快容易内存耗尽

DelayQueue基于时间优先级的无界队列,入队的对象要实现Delayed接口,该接口实现了Comparable接口
生产->延迟时间到了->消费->消费空了->阻塞
生产不阻塞,消费空了阻塞。定时任务可基于该队列处理


ThreadLocal

ThreadLocal内部维护了一个ThreadLocalMap类,map的底层结构是一个Entry数组(通过Entry.key=ThreadLocal.hashcode求模定位数组),这个Entry是ThreadLocalMap的内部类,Entry的对象构成就是kv,且k是写死的为当前ThreadLocal,由于Entry实现了WeakReference<threadlocal<< span="">?>>,所以Entry.key是一个弱引用,指向ThradLocal对象。</threadlocal<<>
ThreadLocalMap是存放在线程Thread中,作为线程的一个属性存在

内存泄漏隐患

为什么Entry.key要使用弱引用:ThreadLocal对象一般是被ThreadLocalMap.key引用的,当key对ThreadLocal为强引用,ThreadLocal由于存在强引用永远不会被回收,举例如果创建该ThreadLocal的线程一直在持续运行(如线程池),value由于当前线程还存在强引用所以不会被回收。

使用弱引用:当key对ThreadLocal为弱引用,且ThreadLocal对象不存在其他外部的引用,那么ThreadLocal随时可能被gc回收掉。

如何避免内存泄漏隐患
上述这种情况jvm也已经做了优化,在后续调用set/get的方法时,会清除key=null的value,在我们主动调用remove方法时,也会清除该value。这样即使对应value就gc不可达了。所以在使用ThreadLocal时,一定要记得调用remove方法。

https://www.jianshu.com/p/377bb840802f

相关文章

网友评论

      本文标题:并发

      本文链接:https://www.haomeiwen.com/subject/pronyrtx.html