多线程
CAS
CAS:Compare and Swap,即比较再交换。
jdk5 增加了并发包 java.util.concurrent.*,其下面的类使用 CAS 算法实现了区别于 synchronized 同步锁的一种乐观锁。JDK 5 之前 Java 语言是靠 synchronized 关键字保证同步的,这是一种独占锁,也是是悲观锁。
原理:
对 CAS 的理解,CAS 是一种无锁算法,CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。
就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的 commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。
CAS 采用了指令级别的,效率高于 synchronized。
问题:
- 循环时间长,开销很大。
- 只能保证一个变量的原子操作。
- ABA 问题。
循环时间长,开销大:
CAS 通常是配合无限循环一起使用的,我们可以看到 getAndAddInt 方法执行时,如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销。
只能保证一个变量的原子操作:
当对一个变量执行操作时,我们可以使用循环 CAS 的操作来保证原子操作,但是对于多个变量操作时,CAS 无法直接保证操作的原子性。
可以通过一下两种办法解决:
- 使用互斥锁来保证原子性。
- 将多个变量封装成对象,通过 AtomicReference 来保证原子性。
ABA 问题:
CAS 的使用流程通常如下:1)首先从地址 V 读取值 A;2)根据 A 计算目标值 B;3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。
如果在这段期间它的值曾经被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。这个漏洞称为 CAS 操作的“ABA”问题。Java 并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证 CAS 的正确性。因此,在使用 CAS 前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
个人理解为:第一阶段为比较,两次 CAS 操作之间,他们的第一阶段之间。
CAS 与 synchronized
- 对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 cpu 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
- 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
- 补充: Java 并发编程这个领域中 synchronized 关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在 JavaSE 1.6 之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。
- synchronized 的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和 CAS 类似的性能;而线程冲突严重的情况下,性能远高于 CAS。
线程池?类型,场景,拒绝策略,执行过程
为什么使用线程池
提高程序的执行效率
如果程序中有大量短时间任务的线程任务,由于创建和销毁线程需要在底层操作系统交互,大量时间都耗费在创建和销毁线程上,因而比较浪费时间,系统效率很低。
而线程池里的每一个线程任务结束后,并不会死亡,而是会再次回到线程池中成为空闲状态,等待下一个对象来使用,因而借助线程池可以提高程序的执行效率。
控制线程的数据量,防止程序崩溃
如果不加限制的创建和启动线程,很容易造成程序崩溃,比如高并发 1000W 个线程,JVM 就需要有保存 1000W 个线程的空间,这样极易出现内存溢出。
线程池中线程数量是一定的,可以有效避免线程溢出。
介绍
多线程之线程池基础 - 九分石人的个人空间 - OSCHINA - 中文开源技术交流社区
http://note.youdao.com/noteshare?id=34aae084ebbe593f23d41a5ef95ccf6d⊂=F8F2A01AF87F43198AB8D005C6FFCD19
使用场景
JAVA 线程池场景化总结
线程池的使用场景以及 java 中 ThreadPoolExecutor 类的讲解γìńɡ 雄尐年ぐ的博客-CSDN 博客线程池应用场景
合理选择线程池的大小:
CPU 核数 _ CPU 核数 _(1+等待时间 / 计算时间),在 java 中,Runtime.getRuntime().availableProcessors()可以获取当前机器的 CPU 核数。
场景分类:
- 并发高,业务执行时间短的任务,线程池线程数可以设置为 cpu 核数+1,减少线程上下文的切换。
- 并发不高,业务执行时间长的任务,要区分看:
- 假如是业务时间长,集中在 IO 操作上,也就是 IO 密集型业务,因为 IO 操作并不占用 cpu,所以不要让所有的 cpu 闲下来,可以加大线程池中的线程数目,让 cpu 处理更多的业务。
- 假如是业务时间长集中在计算上,也就是计算密集型业务,这就和 1 一样,少的线程数,减少线程上下文的切换。
- 并发高,业务执行时间长,解决这种类型任务的关键不在于不在于线程池而在于整体架构设计。看看这些任务里面某些数据能否做缓存是第一步,增加服务器是第二步,至于线程池的设置参考 2。业务执行时间长,看能否通过中间件对任务进行拆分和解耦。
怎么设定:
根据几个参数进行设定:
tasks:每秒的任务数,假设为 500~1000
taskcost:每个任务花费的时间,假设为 0.1s
responsetime:系统允许容忍的最大响应时间,假设为 1s
做个计算:
corePoolSize:每秒需要多少个线程处理?
threadcount = tasks / (1 / taskcost ) = tasks _ taskcout = ( 500 ~ 1000 ) _ 0.1 = 50~100 个线程。
corePoolSize 设置应该大于 50
根据 8020 原则,如果 80%的每秒任务数小于 800,那么 corePoolSize 设置为 80 即可。
queueCapacity = ( coreSizePool / taskcost ) * responsetime
计算可得 queueCapacity = 80/0.1*1 = 80。意思是队列里的线程可以等待 1s,超过了的需要新开线程来执行。
切记不能设置为 Integer.MAX_VALUE,这样队列会很大,线程数只会保持在 corePoolSize 大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
maxPoolSize = ( max ( tasks ) - queueCapacity ) / ( 1 / taskcost )(最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数
计算可得 maxPoolSize = (1000-80)/10 = 92
rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓存机制来处理。
keepAliveTime 和 allowCoreThreadTimeout:采用默认通常能满足