最牛并发编程总结
2020-10-10 10:54:54 11 举报
AI智能生成
java并发编程核心知识点总结(不断完善中)
作者其他创作
大纲/内容
阻塞队列
ArrayBlockingQueue
一个有数组实现FIFO有界阻塞队列
有界且固定,在构造函数式确定大小,确认后不支持改变
在多线程环境下不保证“公平性”
实现
ReentrantLock
Condition
LinkedBlockingQueue
基于链表,无解的FIFO阻塞队列
PriorityBolckingQueue
支持优先级的无解阻塞队列
默认情况下元素采用自然顺序升序排序,可以通过制定Comparator来对元祖进行排序
二叉堆
分类
最大堆,父节点的键值总是大于或等于任何一个子节点的键值
最小堆,父节点的键值总是小于或等于任何一个节点的键值
添加操作是不断“上冒”,而删除操作则是不断“下掉”
实现
ReentrantLock+Condition
二叉堆
DelayQueue
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。
队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。
只有在延迟期满时才能从队列中提取元素
队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。
只有在延迟期满时才能从队列中提取元素
应用
缓存:清楚缓存中超时的缓存数据
任务延时处理
实现
ReentrantLock+Condition
根据Delay时间排序的优先级队列
Delayed接口
用来标记哪些应该在给定延迟时间之后的对象
该接口要求实现他的实现类必须定义一个compareTo方法,该方法提供与此接口的getDelay()方法一致的排序
SynchronousQueue
一个没有容量的阻塞队列
应用
交换工作,生产者的线程和消费者的线程同步以传递某信息、事件、任务
不太好理解,和Exchanger有一拼
LinkedBlockingDeque
由链表组成的双向阻塞队列
容量可选,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容易大小为Integer.MAX_VALUE
应用
“工作窃取”模式
原子操作
基本类型
用于通过原子的方式更新基本类型
AtomicBoolean:原子更新boolean类型
AtomicInteger:原子更新整形类型
AtomicLong:原子更新长整型类型
数组
通过原子的方式更新数组里的某个元素
AtomicIntegerArray:原子更新整形数组里的元素
AtomicLongArray:原子更新长整型数组里的元素
AtomicReferenceArray:原子更新引用类似数组里的元素
引用类型
如果需要原子的更新多个变量,就需要使用这个原子更新引用类型提供的类
AomicRefernce:原子更新引用类型
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(下一次扩容阈值),表示下一次扩容的大小
核心思想
根据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所经过的黑色结点的个数一样的。
[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!]
[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!]
JDK18与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实现
线程池
线程池原理
提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
好处
降低资源消耗
通过重复利用已创建的线程降低线程的创建和销毁造成的不必要资源消耗
提高响应速度
当任务到达时,任务可以不需要等到线程创建就立即开始执行
提高线程的可管理性
统一分配、监控、调优
Executor
Executors
newCachedThreadPool
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行
很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造
的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并
从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资
源。
很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造
的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并
从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资
源。
newFixedThreadPool
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大
多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,
则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何
线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之
前,池中的线程将一直存在。
多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,
则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何
线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之
前,池中的线程将一直存在。
newScheduledThreadPool
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
newSingleThreadExecutor
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程
池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
ThreadPoolExecutor
参数含义
corePoolSize
线程池中核心线程的数量
当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,
线程池会提前创建并启动所有基本线程。
线程池会提前创建并启动所有基本线程。
maximumPoolSize
线程池允许的最大线程数
线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
keepAliveTime
线程空闲时间
线程池的工作线程空闲后,保持存活的时间。所以,
如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
unit
线程空闲时间单位
用于保存等待执行的任务的阻塞队列。可以选择以下几
个阻塞队列。
个阻塞队列。
workQueue
用来保存等待执行的任务的阻塞队列
使用的是阻塞队列
ArrayBlockingQueue
LinkedBlockingQueue
SynchronousQueue
PriorityBlockingQueue
threadFactory
用于设置创建线程的工厂,用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
DefaultThreadFactory
handler
RejectedExecutionHandler.线程池的拒绝策略
拒绝策略分类
AbortPolicy:直接抛出异常,默认策略
CallerRunsPolicy:用调用者所在的线程来执行任务
DiscardOldestPolicy:丢弃阻塞对类中靠最前的任务,并执行当前任务
DiscardPolicy:直接丢弃任务
任务提交
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进行排序
封装PriorityQueue,这个PriorityQueue会对队列中的ScheduledFutureTask进行排序
Future
异步计算
Future
提供操作
执行任务的取消
查询任务是否已经执行完成
获取任务的执行结果
FutureTask
实现RunnableFuture接口,即可以作为Runnable被执行,也可以
作为Future得到Callable的返回值
作为Future得到Callable的返回值
内部基于AQS实现
线程基础
进程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 方法来终止线程。
线程间通信
内存共享
线程之间共享程序的公共状态,
通过读和写内存中的公共状态进行隐式通信
通过读和写内存中的公共状态进行隐式通信
消息传递
线程之间必须通过发送消息来现实进行通信
java内存模型
线程通信机制
内存共享
Java采用
消息传递
内存模型
重排序
为了程序的性能,处理器、编译器都会对程序进行重排序处理
条件
在单线程环境下不能改变程序运行的结果
存在数据依赖关系的不允许重排序
重排序在多线程环境下可能会导致数据不安全
数据依赖性
如果两个操作访问统一变量,且这两个操作有一个是写操作,此时这两个操作之间就存在数据依赖性,
写一个变量之后,再读一个变量
写一个变量之后,再写一个变量
读一个变量后,再写一个变量
顺序一致性
多线程环境下的理论参考模型
为程序提供了极强的内存可见性保证
特性
一个线程中的所有操作必须按照程序的顺序来执行
所有线程都只能看到一个单一的操作执行顺序,不管程序是否同步
每个操作都必须原子执行且立刻对所有线程可见
happens-before
定义
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行熟悉怒排在第二个操作之前。
两个操作之间存在happens-before关系,并不意味java平台的具体实现必须按照happens-before关系指定的顺序来执行。如果重排序后的执行结构,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(JMM允许这种重排序)
as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结构不被改变。
两个操作之间存在happens-before关系,并不意味java平台的具体实现必须按照happens-before关系指定的顺序来执行。如果重排序后的执行结构,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(JMM允许这种重排序)
as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结构不被改变。
JMM中最核心的理论,保证内存可见性
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
as-if-serial
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结构不能被改变。
为了遵守as-if-serial语义,编译器和处理器不会存在数据依赖关系的操作 做重排序,但是如果操作之前不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
为了遵守as-if-serial语义,编译器和处理器不会存在数据依赖关系的操作 做重排序,但是如果操作之前不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
双重检查锁
顾名思义,通过两次检查,并基于加锁机制,实现某个功能。
final内存语义
在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
synchronized
同步、重量级锁
原理
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入临界区,同时还可以保证共享变量的内存可见性(单台JVM内)
锁对象
普通同步方法,锁的是当前实例对象
静态同步方法,锁的是当前类的class对象
同步代码块,锁的是括号里的对象(实例对象或者class对象)
实现机制
Java对象头
synchronized的锁就是保存在Java对象头中的
包括两部分数据
Mark Word(标记字段)
Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间
哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳
Klass Pointer(类型指针)
monitor
monitorenter
monitorexit
初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
锁优化
无锁
偏向锁
标记方式
为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径
主要尽可能避免不必须要的CAS操作,如果竞争锁失败,则升级为轻量级锁
轻量级锁
自旋方式
该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁(循环方式)
自旋字数较难控制(-XX:preBlockSpin)
存在理论:线程的频繁挂起、唤醒负担较重,可以认为每个线程占有锁的时间很短,线程挂起再唤醒得不偿失
缺点:自旋次数无法确定
适应性自旋
自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
自旋成功,则可以增加自旋次数,如果获取锁经常失败,那么自旋次数会减少
重量级锁
阻塞方式
锁消除
若不存在数据竞争的情况,JVM会消除锁机制
判断依据:变量逃逸
锁粗化
将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。例如for循环内部获取锁
使用方式
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
结合wait/notify/notyfyAll的使用
volatile
特性
原子性
对任意(包括64位long类型和double类型)单个volatile变量的读/写具有原子性。但是类似于a++这种复合操作不具有原子性。
可见性
(最重要的特性)。当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主内存中。当一个线程用到被volatile关键字修饰的值的时候,虚拟机会强制要求它从主内存中读取。
禁止重排序
指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果时正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。非常经典的例子是在单例方法中同时对字段加入voliate,就是为了防止指令重排序。
实现机制
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。volatile不会引起上下文的切换 和 调度。
内心语义
写操作
当写一个Volatile 变量的时候,JMM会把该线程对应的本地内存的共享变量刷新到主内存中. 使用上面的代码中示例,
当在执行writer() 方法 写了flag变量后,JMM会立即可本地内存的共享变量刷新到主内存中。
当在执行writer() 方法 写了flag变量后,JMM会立即可本地内存的共享变量刷新到主内存中。
读操作
当读取一个volatile变量的时候,JMM会将当前线程的本地内存共享变量设置为无效,线程在接下来的操作,会从主内存中读取共享变量。
volatile 的内存语义就是,在修改一个volatile变量的时候,会将本地内存的共享变量刷新到主内存,然后读取一个volatile变量的时候会将本地内存作废,然后从主内存中读取数据。所以线程1更新volatile变量的值,线程2读取volatile变量,实际上就是线程1向线程2发送消息通知线程2共享变量发生改变。
并发基础
AQS
AbstractQueuedSynchronizer,同步器,实现JUC核心基础组件
解决了子类实现同步器时涉及到的大量细节性问题,例如:同步状态、FIFO同步队列等
采用模板方法模式,AQS实现了大量的通用方法,子类通过继承方式实现其抽象方法来管理同步状态
CLH同步队列
FIFO双向队列,AQS依赖他来解决同步状态的管理问题
首节点唤醒,等待队列加入到CLH同步队列的尾部
同步装货区与释放
独占式
获取锁
获取同步状态:acquire
响应中断:acquireInterruptiably
超时获取:tryAcquireNanos
释放锁
release
共享式
获取锁
acquireShared
释放锁
releaseShared
线程阻塞和唤醒
当前线程获取锁了,其他线程再获取锁时需要阻塞,当线程释放锁后,AQS负责欢迎其他线程。
LockSupport
是用来创建和其他同步类的基本线程阻塞原语
每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,并且可在进行中使用,则调用park()将立即返回,否则可能阻塞。
如果许可尚不可用,则可以调用unpark使其可用。
如果许可尚不可用,则可以调用unpark使其可用。
park()和unpark()
CAS
Compare and swap,整个JUC体系下最核心、最基础的理论
内存值V、旧的预期值A,需要更新的值B,当且仅当内存值V等于旧的预期值A时我才会将内存值V修改为B,否则啥都不做
native中存在四个参数
缺陷
循环时间过长
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
只能保证一个共享变量原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子
性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对之间的原子性,就可以把多个变量放在一个对象里来进行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
使用场景:多个线程彼此等待,当所有的线程都到达指定“地点”(指定代码位置),才开始继续执行。
它允许一组线程相互等待,直到到达某个公共屏障点
底层采用:ReenTrantLock+Condition来实现的
通俗的讲:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CountDownLatch
适用场景:在多线程执行过程中设置几个门闩,当所有的门闩被打开时,被挡在门外的线程才能继续执行。
在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
用给定的计数,初始化CountDownLatch,由于调用了countDown()方法,所以在当前计数到达0之前,await()方法会一直受阻塞。
之后,会释放所有等待的线程,await的所有后续调用都将立即返回。
这种现象只出现一次----计数无法被充值,如果需要重置计数,建议使用CyclicBarrier.
之后,会释放所有等待的线程,await的所有后续调用都将立即返回。
这种现象只出现一次----计数无法被充值,如果需要重置计数,建议使用CyclicBarrier.
内部采用共享锁来实现的
与CyclicBarrier的区别
CountDownLatch的作用是允许1个或者多个线程等待其他线程执行完成;而CyclicBarrier则是允许N个线程相互等待
CountDownLatch的技术无法被重置,用完了就没了;但是CyclicBarrier的计数器是可以被重置后继续使用,也因此称之为循环的barrier
Semaphore
使用场景:需要控制访问某个资源或者进行某种操作的线程数量。当达到指定数量时,只能等待其他线程释放信号量。
一个控制访问多个共享资源的计数器
从概念上讲,信号量维护了一个许可集。如有必要,在许可可用钱会阻塞每个aquire(),然后再获取该许可。
每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不适用实际的许可对象,
Semaphore只对可用许可的号码进行计数,并采用相应的行动。
每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不适用实际的许可对象,
Semaphore只对可用许可的号码进行计数,并采用相应的行动。
内部采用共享锁实现的
Exchanger
使用场景:用于两个线程之间交换数据
可以在对其中元素进行配对和交换的线程的同步点
允许在并发任务之间交换数据。具体地说就是Exchanger类允许在两个线程之间定义同步点。
当两个线程都到达同步点时,他们交换数据结构,因此第一个线程的数据结构进入第二个县城中,第二个线程的数据结构进入第一个中
当两个线程都到达同步点时,他们交换数据结构,因此第一个线程的数据结构进入第二个县城中,第二个线程的数据结构进入第一个中
其他
ThreadLocal
一种解决多线程环境下成员变量的问题的方案,但是与线程同步无关,其思路就是为每个线程创建一个单独的变量副本。
从而每个线程都可以独立的改变自己所拥有的变量副本,而不会影响其他线程对应的变量副本。
从而每个线程都可以独立的改变自己所拥有的变量副本,而不会影响其他线程对应的变量副本。
ThreadLocal不是用于解决共享变量的问题,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制
重要的四个方法
get()
返回此线程局部变量的当前线程副本中的值
initalValue()
返回此线程局部变量的当前线程的值
remove()
移除此线程局部变量当前线程的值
set(T value)
将此线程局部变量的当前线程副本中的值设置为指定值
ThreadLocalMap
实现线程隔离机制的关键
每个ThreadLocal内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量。
该成员变量用来存储时间的ThreadLocal变量副本。
该成员变量用来存储时间的ThreadLocal变量副本。
提供了一种用键值对方式存储每一个线程的变量副本的方法,
key为当前线程的ThreadLocal对象,value则是对应线程的变量副本。
key为当前线程的ThreadLocal对象,value则是对应线程的变量副本。
注意点
ThreadLocal失恋了本身是不存储值的,它只是提供了一个在当前县城中找到副本值的key
是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中
内存泄漏问题
ThreadLocalMap
key弱引用,value强引用,无法回收
显示调用remove方法
Fork/Join
分隔任务
首先我们需要一个fork类来把大任务分割成子任务,有可能子任务还是很大,
所以还需要不停的分割,直到分割出的子任务足够小。
所以还需要不停的分割,直到分割出的子任务足够小。
执行任务合并结果
分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。
子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。
子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。
核心思想
分治
fork分解任务,join手机数据
核心类
ForkJoinPool
执行任务的线程池
ForkJoinTask
表示任务,用于ForkJoinPool的任务抽象
ForkJoinWorkerThread
执行任务的工作现场
适用场景:一个用于并行执行任务的框架,是把一大把任务分隔成若干个小任务,最终汇总每个小任务的结构后得大任务结构的框架
java并发编程实战
生产者与消费者模式
线上问题定位
异步任务池
优化
建议使用不可变类
建议最小化锁的粒度
建议使用本地变量
建议使用同步代替线程的wait和notify
建议使用同步代码块代替同步方法
建议避免使用静态变量
建议使用并发集合代替同步的框架(Hashtable、Vector等)
建议使用BlockingQueue
0 条评论
下一页