Java并发体系知识导图笔记
2021-09-25 16:21:56 0 举报
AI智能生成
Java并发体系知识点梳理
作者其他创作
大纲/内容
线程基础
进程VS线程
进程
进程的本质是一个正在执行的程序,程序运行时系统会创建一个进程,并且**给每个进程分配独立的内存地址空间保证每个进程地址不会相互干扰**。同时,在 CPU 对进程做时间片的切换时,保证进程切换过程中仍然要从进程切换之前运行的位置出开始执行。所以进程通常还会包括程序计数器、堆栈指针。
有时被称为**轻量级进程**(Lightweight Process,LWP),是程序执行流的最小单元。线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派 CPU 的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
创建线程的方式
继承Thread
实现Runnable接口
Callable/Future
线程的生命周期
NEW
线程被创建后尚未启动
RUNNABLE
包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在运行,
也可能正在等待系统资源,如等待CPU为它分配时间片。
也可能正在等待系统资源,如等待CPU为它分配时间片。
BLOCKED
线程阻塞于锁。
WAITING
线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING
该状态不同于WAITING,它可以在指定的时间内自行返回。
TERMINATED
该线程已经执行完毕。
线程的启动与终止
线程启动
启动线程是start方法,非run方法
线程终止
正常运行结束
程序运行结束,线程自动结束。
使用退出标志退出线程
一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的
运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:
最直接的方法就是设一个boolean 类型的标志,并通过设置这个标志为 true或 false 来控制 while
循环是否退出。
定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit
时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只
能由一个线程来修改 exit 的值。
运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:
最直接的方法就是设一个boolean 类型的标志,并通过设置这个标志为 true或 false 来控制 while
循环是否退出。
定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit
时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只
能由一个线程来修改 exit 的值。
Interrupt方法结束线程
线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,
会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。
阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让
我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实
际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正
常结束 run 方法。
会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。
阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让
我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实
际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正
常结束 run 方法。
线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用
interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
stop 方法终止线程(线程不安全)
程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关
闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:
thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子
线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用
thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈
现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因
此,并不推荐使用 stop 方法来终止线程。
闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:
thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子
线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用
thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈
现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因
此,并不推荐使用 stop 方法来终止线程。
线程间通信
内存共享
线程之间共享程序的公共状态,
通过读和写内存中的公共状态进行隐式通信
通过读和写内存中的公共状态进行隐式通信
消息传递
线程之间必须通过发送消息来现实进行通信
线程池
线程池原理
提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
好处
降低资源消耗
通过重复利用已创建的线程降低线程创建和销毁造成的消耗
提高响应速度
当任务到达时,任务可以不需要等到线程创建就能立即执行
提高线程的可管理性
进行统一分配、调优和监控
Executor
Executors
静态工厂类,提供了Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 、Callable 等类的静态工厂方法
ThreadPoolExecutor
参数含义
corePoolSize
线程池中核心线程的数量
当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,
线程池会提前创建并启动所有基本线程。
线程池会提前创建并启动所有基本线程。
maximumPoolSize
线程池中允许的最大线程数
线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
keepAliveTime
线程空闲的时间
线程池的工作线程空闲后,保持存活的时间。所以,
如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
unit
keepAliveTime的单位
用于保存等待执行的任务的阻塞队列。可以选择以下几
个阻塞队列。
个阻塞队列。
workQueue
用来保存等待执行的任务的阻塞队列
使用的阻塞队列
ArrayBlockingQueue
LinkedBlockingQueue
SynchronousQueue
PriorityBlockingQueue
threadFactory
用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
DefaultThreadFactory
handler
RejectedExecutionHandler,线程池的拒绝策略
分类
AbortPolicy:直接抛出异常,默认策略
CallerRunsPolicy:用调用者所在的线程来执行任务
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务
DiscardPolicy:直接丢弃任务
线程池分类
newFixedThreadPool
可重用固定线程数的线程池
分析
corePoolSize和maximumPoolSize一致
使用“无界”队列LinkedBlockingQueue
maximumPoolSize、keepAliveTime、RejectedExecutionHandler 无效
newCachedThreadPool
使用单个worker线程的Executor
分析
corePoolSize和maximumPoolSize被设置为1
使用LinkedBlockingQueue作为workerQueue
newSingleThreadExecutor
会根据需要创建新线程的线程池
分析
corePoolSize被设置为0
maximumPoolSize被设置为Integer.MAX_VALUE
SynchronousQueue作为WorkerQueue
如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程 ,可能会耗尽CPU和内存资源
任务提交
Executor.execute()
ExecutorService.submit()
任务执行
执行流程
线程池调优
两种模型
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。
任务的性质:CPU密集型任务、IO密集型任务和混合型任务
任务的优先级:高中低
任务的执行时间:长中短
任务 的依赖性;是否依赖其他系统资源
CPU密集型任务应配置尽可能小的线程,如配置NCPU+1个线程的线程池,
IO密集型任务线程并不是一直在执行任务 ,则应配置尽可能多的线程,如2*NCPU 。
可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理,它可以让优先级高的任务先执行。
任务的性质:CPU密集型任务、IO密集型任务和混合型任务
任务的优先级:高中低
任务的执行时间:长中短
任务 的依赖性;是否依赖其他系统资源
CPU密集型任务应配置尽可能小的线程,如配置NCPU+1个线程的线程池,
IO密集型任务线程并不是一直在执行任务 ,则应配置尽可能多的线程,如2*NCPU 。
可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理,它可以让优先级高的任务先执行。
线程池监控
在监控线程池的时候可以使用以下属性:
taskCount: 线程池需要执行的任务数量
completedTaskCount: 线程池在 运行过程中已完成的任务数量,小于或等于taskCount。
getPoolSize;线程池的线程数量,如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减
getActiveCount; 获取活动的线程数
taskCount: 线程池需要执行的任务数量
completedTaskCount: 线程池在 运行过程中已完成的任务数量,小于或等于taskCount。
getPoolSize;线程池的线程数量,如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减
getActiveCount; 获取活动的线程数
ScheduledThreadPoolExecutor
继承自ThreadPoolExecutor
给定的延迟之后运行任务,或者定期执行任务
内部使用DelayQueue来实现 ,会把调度的任务放入DelayQueue中。DelayQueue内部封装PriorityQueue,这个PriorityQueue会对队列中的ScheduledFutureTask进行排序
Future
异步计算
Future
提供操作
执行任务的取消
查询任务是否完成
获取任务的执行结果
FutureTask
实现RunnableFuture接口,既可以作为Runnable被执行,也可以作为Future得到Callable的返回值
内部基于AQS实现
阻塞队列
ArrayBlockingQueue
一个由数组实现的FIFO有界阻塞队列
ArrayBlockingQueue有界且固定,在构造函数时确认大小,确认后不支持改变
在多线程环境下不保证“公平性”
实现
ReentrantLock
Condition
LinkedBlockingQueue
基于链接,无界的FIFO阻塞队列
PriorityBlockingQueue
支持优先级的无界阻塞队列
默认情况下元素采用自然顺序升序排序,可以通过指定Comparator来对元素进行排序
二叉堆
分类
最大堆
父节点的键值总是大于或等于任何一个子节点的键值
最小堆
父节点的键值总是小于或等于任何一个子节点的键值
添加操作则是不断“上冒”,而删除操作则是不断“下掉”
实现
ReentrantLock + Condition
二叉堆
DelayQueue
支持延时获取元素的无界阻塞队列
应用
缓存:清掉缓存中超时的缓存数据
任务超时处理
实现
ReentrantLock + Condition
根据Delay时间排序的优先级队列:PriorityQueue
Delayed接口
用来标记那些应该在给定延迟时间之后执行的对象
该接口要求实现它的实现类必须定义一个compareTo 方法,该方法提供与此接口的 getDelay 方法一致的排序。
SynchronousQueue
一个没有容量的阻塞队列
应用
交换工作,生产者的线程和消费者的线程同步以传递某些信息、事件或者任务
难搞懂,与Exchanger 有一拼
LinkedTransferQueue
链表组成的的无界阻塞队列
相当于ConcurrentLinkedQueue、SynchronousQueue (公平模式下)、无界的LinkedBlockingQueues等的超集
预占模式
有就直接拿走,没有就占着这个位置直到拿到或者超时或者中断
LinkedBlockingDeque
由链表组成的双向阻塞队列
容量可选,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE
运用
“工作窃取”模式
原子操作
基本类型类
用于通过原子的方式更新基本类型
AtomicBoolean
原子更新布尔类型
AtomicInteger
原子更新整型
AtomicLong
原子更新长整型
数组
通过原子的方式更新数组里的某个元素
AtomicIntegerArray
原子更新整型数组里的元素
AtomicLongArray
原子更新长整型数组里的元素
AtomicReferenceArray
原子更新引用类型数组里的元素
引用类型
如果要原子的更新多个变量,就需要使用这个原子更新引用类型提供的类
AtomicReference
原子更新引用类型
AtomicReferenceFieldUpdater
原子更新引用类型里的字段
AtomicMarkableReference
原子更新带有标记位的引用类型
字段类
如果我们只需要某个类里的某个字段,那么就需要使用原子更新字段类
AtomicIntegerFieldUpdater
原子更新整型的字段的更新器
AtomicLongFieldUpdater
原子更新长整型字段的更新器
AtomicStampedReference
原子更新带有版本号的引用类型
Java并发集合
ConcurrentHashMap
CAS + Synchronized 来保证并发更新的安全,底层采用数组+链表/红黑树的存储结构
重要内部类
Node
key-value键值对
TreeNode
红黑树节点
TreeBin
就相当于一颗红黑树,其构造方法其实就是构造红黑树的过程
ForwardingNode
辅助节点,用于ConcurrentHashMap扩容操作
sizeCtl
控制标识符,用来控制table初始化和扩容操作的
含义
负数代表正在进行初始化或扩容操作
-1代表正在初始化
-N 表示有N-1个线程正在进行扩容操作
正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小
重要操作
initTable
ConcurrentHashMap初始化方法
只能有一个线程参与初始化过程,其他线程必须挂起
构造函数不做初始化过程,初始化真正是在put操作触发
步骤
sizeCtl < 0 表示正在进行初始化,线程挂起
线程获取初始化资格(CAS(SIZECTL, sc, -1))进行初始化过程
初始化步骤完成后,设置sizeCtl = 0.75 * n(下一次扩容阈值),表示下一次扩容的大小
put
核心思想
根据hash值计算节点插入在table的位置,如果该位置为空,则直接插入,否则插入到链表或者树中
真实情况较为复杂
步骤
table为null,线程进入初始化步骤,如果有其他线程正在初始化,该线程挂起
如果插入的当前 i 位置 为null,说明该位置是第一次插入,利用CAS插入节点即可,插入成功,则调用addCount判断是否需要扩容。若插入失败,则继续匹配(自旋)
若该节点的hash ==MOVED(-1),表示有线程正在进行扩容,则进入扩容进程中
其余情况就是按照链表或者红黑树结构插入节点,但是这个过程需要加锁(synchronized)
get
步骤
table ==null ;return null
从链表/红黑树节点获取
扩容
多线程扩容
步骤
构建一个nextTable,其大小为原来大小的两倍,这个步骤是在单线程环境下完成的
将原来table里面的内容复制到nextTable中,这个步骤是允许多线程操作
链表转换为红黑树过程
所在链表的元素个数达到了阈值 8,则将链表转换为红黑树
红黑树算法
红黑树是一种自平衡二叉查找树,是计算机科学领域中的一种数据结构,典型的用途是实现关联数组,存储有序的数据。
别称"对称二叉B树",它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的。
它可以在O(logn)时间内做查找,插入和删除,这里的n是树的结点个数
别称"对称二叉B树",它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的。
它可以在O(logn)时间内做查找,插入和删除,这里的n是树的结点个数
特性
每个结点是黑色或者红色。
根结点是黑色。
每个叶子结点(NIL)是黑色。 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!]
如果一个结点是红色的,则它的子结点必须是黑色的。
每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!]
1.8 与 1.7的区别
1.7
基于ReentrantLock实现分段锁来实现的segment。jdk1.7中采用Segment + HashEntry的方式进行实现
1.8
1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现
ConcurrentLinkedQueue
基于链接节点的无边界的线程安全队列,采用FIFO原则对元素进行排序,内部采用CAS算法实现
不变性
在入队的最后一个元素的next为null
队列中所有未删除的节点的item都不能为null且都能从head节点遍历到
对于要删除的节点,不是直接将其设置为null,而是先将其item域设置为null(迭代器会跳过item为null的节点)
允许head和tail更新滞后。这是什么意思呢?意思就说是head、tail不总是指向第一个元素和最后一个元素(后面阐述)
head的不变性和可变性
tail的不变性和可变性
精妙之处:利用CAS来完成数据操作,同时允许队列的不一致性,弱一致性表现淋漓尽致
ConcurrentSkipListMap
第三种key-value数据结构:SkipList(跳表)
SkipList
平衡二叉树结构
Skip list让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过“空间来换取时间”的一个算法,在每个节点中增加了向前的指针,在插入、删除、查找时可以忽略一些不可能涉及到的结点,从而提高了效率
特性
由很多层结构组成,level是通过一定的概率随机产生的
每一层都是一个有序的链表,默认是升序,也可以根据创建映射时所提供的Comparator进行排序,具体取决于使用的构造方法
最底层(Level 1)的链表包含所有元素
如果一个元素出现在Level i 的链表中,则它在Level i 之下的链表也都会出
每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素
查找、删除、添加
ConcurrentSkipListSet
内部采用ConcurrentSkipListMap实现
Java内存模型(JMM)
线程通信机制
内存共享
Java采用
消息传递
内存模型
重排序
为了程序的性能,处理器、编译器都会对程序进行重排序处理
条件
在单线程环境下不能改变程序运行的结果
存在数据依赖关系的不允许重排序
问题
重排序在多线程环境下可能会导致数据不安全
数据依赖性
如果两个操作访问统一变量,且这两个操作有一个是写操作,此时这两个操作之间就存在数据依赖性,
写一个变量之后,再读一个变量
写一个变量之后,再写一个变量
读一个变量后,再写一个变量
顺序一致性
多线程环境下的理论参考模型
为程序提供了极强的内存可见性保证
特性
一个线程中的所有操作必须按照程序的顺序来执行
所有线程都只能看到一个单一的操作执行顺序,不管程序是否同步
每个操作都必须原子执行且立刻对所有线程可见
happens-before
理论
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法(JMM允许这种重排序)
as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结构不被改变。
JMM中最核心的理论,保证内存可见性
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
as-if-serial
所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变
为了遵守as-if-serial语义,编译器和处理器不会存在数据依赖关系的操作 做重排序,但是如果操作之前不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
双重检查锁
顾名思义,通过两次检查,并基于加锁机制,实现某个功能。
final内存语义
在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
synchronized
同步、重量级锁
原理
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性(单台JVM内)
锁对象
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
实现机制
Java对象头
synchronized的锁就是保存在Java对象头中的
包括两部分数据
Mark Word(标记字段)
Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间
包括
哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳
Klass Pointer(类型指针)
monitor
monitorenter
monitorexit
初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
锁优化
自旋锁
该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁(循环方式)
自旋字数较难控制(-XX:preBlockSpin)
存在理论:线程的频繁挂起、唤醒负担较重,可以认为每个线程占有锁的时间很短,线程挂起再唤醒得不偿失
缺点
自旋次数无法确定
适应性自旋锁
自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
自旋成功,则可以增加自旋次数,如果获取锁经常失败,那么自旋次数会减少
锁消除
若不存在数据竞争的情况,JVM会消除锁机制
判断依据
变量逃逸
锁粗化
将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。例如for循环内部获取锁
轻量级锁
在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
通过CAS来获取锁和释放锁
性能依据
对于绝大部分的锁,在整个生命周期内都是不会存在竞争的
缺点
在多线程环境下,其运行效率比重量级锁还会慢
偏向锁
为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径
主要尽可能避免不必须要的CAS操作,如果竞争锁失败,则升级为轻量级锁
volatile
特性
原子性
volatile对单个读/写具有原子性(32位Long、Double),但是类似于i++这种复合操作不具有原子性。
可见性
当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主内存中。当一个线程用到被volatile关键字修饰的值的时候,虚拟机会强制要求它从主内存中读取。
禁止重排序
指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果时正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。非常经典的例子是在单例方法中同时对字段加入voliate,就是为了防止指令重排序。
实现机制
内存屏障
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。volatile不会引起上下文的切换 和 调度。
内存语义
写操作
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中
读操作
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
volatile 的内存语义就是,在修改一个volatile变量的时候,会将本地内存的共享变量刷新到主内存,然后读取一个volatile变量的时候会将本地内存作废,然后从主内存中读取数据。所以线程1更新volatile变量的值,线程2读取volatile变量,实际上就是线程1向线程2发送消息通知线程2共享变量发生改变。
操作系统语义
主存、高速缓存(线程私有)缓存一致?
解决方案
通过在总线加LOCK#锁的方式
通过缓存一致性协议(MESI协议)
内存模型
重排序
happens-before
DCL
单例模式
DCL
重排序
happens-beofre
解决方案
volatile方案
禁止重排序
基于类初始化的解决方案
利用classloder的机制来保证初始化instance时只有一个线程。JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化
并发基础
AQS
AbstractQueuedSynchronizer,同步器,实现JUC核心基础组件
解决了子类实现同步器时涉及的大量细节问题,例如获取同步状态、FIFO同步队列
采用模板方法模式,AQS实现大量通用方法,子类通过继承方式实现其抽象方法来管理同步状态
CLH同步队列
FIFO双向队列,AQS依赖它来解决同步状态的管理问题
首节点唤醒,等待队列加入到CLH同步队列的尾部
同步状态获取与释放
独占式
获取锁
获取同步状态:acquire
响应中断:acquireInterruptibly
超时获取:tryAcquireNanos
释放锁
release
共享式
获取锁
acquireShared
释放锁
releaseShared
线程阻塞和唤醒
当有线程获取锁了,其他再次获取时需要阻塞,当线程释放锁后,AQS负责唤醒线程
LockSupport
是用来创建锁和其他同步类的基本线程阻塞原语
每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,并且可在进程中使用,则调用park()将会立即返回,否则可能阻塞。如果许可尚不可用,则可以调用 unpark 使其可用
park()、unpark()
CAS
Compare And Swap,整个JUC体系最核心、最基础理论
内存值V、旧的预期值A、要更新的值B,当且仅当内存值V的值等于旧的预期值A时才会将内存值V的值修改为B,否则什么都不干
native中存在四个参数
缺陷
循环时间太长
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
只能保证一个共享变量原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子
ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值 原来是A,变成了B,又变成了B,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
解决方案
版本号
AtomicStampedReference
锁
ReentrantLock
可重入锁,是一种递归无阻塞的同步机制
比synchronized更强大、灵活的锁机制,可以减少死锁发生的概率
分为公平锁、非公平锁
底层采用AQS实现,通过内部Sync继承AQS
ReentrantReadWriteLock
读写锁,两把锁:共享锁:读锁、排他锁:写锁
支持公平性、非公平性,可重入和锁降级
锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁
Condition
Lock 提供条件Condition,对线程的等待、唤醒操作更加详细和灵活
内部维护一个Condition队列。当前线程调用await()方法,将会以当前线程构造成一个节点(Node),并将节点加入到该队列的尾部
LockSupport
当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作,LockSupport为构建同步组件的基础工具。
LockSupport定义了一组 以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。
Park有停车的意思,假设线程为车辆,那么park方法代表着停车,而unpark方法则是指车辆启动离开
LockSupport定义了一组 以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。
Park有停车的意思,假设线程为车辆,那么park方法代表着停车,而unpark方法则是指车辆启动离开
并发工具类
CyclicBarrier
使用场景:多个线程彼此等待,当所有的线程都到达指定“地点”(指定代码位置),才开始继续执行。
它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)
底层采用ReentrantLock + Condition实现
通俗讲:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活
CountDownLatch
使用场景:在多线程执行过程中设置几个门闩,当所有的门闩被打开时,被挡在门外的线程才能继续执行。
在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。
内部采用共享锁来实现
与CyclicBarrier区别
CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待
CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier
Semaphore
使用场景:需要控制访问某个资源或者进行某种操作的线程数量。当达到指定数量时,只能等待其他线程释放信号量。
信号量
一个控制访问多个共享资源的计数器
从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动
信号量Semaphore是一个非负整数(>=1)。当一个线程想要访问某个共享资源时,它必须要先获取Semaphore,当Semaphore >0时,获取该资源并使Semaphore – 1。如果Semaphore值 = 0,则表示全部的共享资源已经被其他线程全部占用,线程必须要等待其他线程释放资源。当线程释放资源时,Semaphore则+1
内部采用共享锁实现
Exchanger
使用场景:用于两个线程之间交换数据
可以在对中对元素进行配对和交换的线程的同步点
允许在并发任务之间交换数据。具体来说,Exchanger类允许在两个线程之间定义同步点。当两个线程都到达同步点时,他们交换数据结构,因此第一个线程的数据结构进入到第二个线程中,第二个线程的数据结构进入到第一个线程中
其他
ThreadLocal
一种解决多线程环境下成员变量的问题的方案,但是与线程同步无关。其思路是为每一个线程创建一个单独的变量副本,从而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本
ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制
四个方法
get():返回此线程局部变量的当前线程副本中的值
initialValue():返回此线程局部变量的当前线程的“初始值”
remove():移除此线程局部变量当前线程的值
set(T value):将此线程局部变量的当前线程副本中的值设置为指定值
ThreadLocalMap
实现线程隔离机制的关键
每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。
提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本
注意点
ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key
是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中
内存泄漏问题
ThreadLocalMap
key 弱引用 value 强引用,无法回收
显示调用remove()
Fork/Join
分隔任务
首先我们需要一个fork类来把大任务分割成子任务,有可能子任务还是很大,
所以还需要不停的分割,直到分割出的子任务足够小
所以还需要不停的分割,直到分割出的子任务足够小
执行任务合并结果
分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。
子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。
子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。
核心思想
“分治”
fork分解任务,join收集数据
工作窃取
某个线程从其他队列里窃取任务来执行
执行块的线程帮助执行慢的线程执行任务,提升整个任务效率
队列要采用双向队列
核心类
ForkJoinPool
执行任务的线程池
ForkJoinTask
表示任务,用于ForkJoinPool的任务抽象
ForkJoinWorkerThread
执行任务的工作线程
适用场景:一个用于并行执行任务的框架,是把一大把任务分隔成若干个小任务,最终汇总每个小任务的结构后得大任务结构的框架
收藏
0 条评论
下一页