线程和同步性能
线程同步
同步和java并发工具
同步是指代码块对一组变量的访问看上去是串行的:每次只能有一个线程访问内存
synchronized关键字保护的代码块
java.util.concurrent.Lock类实例保护的代码块
java.util.concurrent.atomic包中的代码
原子类不使用同步指令,从CPU编程方面,原子类利用比较并交换(Compare and Swap, CAS)的CPU指令
同步则需要独占访问资源
使用CAS指令的线程同时访问资源不会造成阻塞(无锁设计,但是还是会出现阻塞结构的大部分行为),使用同步锁的线程另一个线程持有该资源时会阻塞
最终的结果会让开发人员认为,线程只能串行的访问受保护的内存
同步的代价
可扩展性
应用程序在同步块中花费的时间会影响应用程序的可扩展性
阿姆达尔定律:
P是并行运行的程序量,N是使用的线程数量(每个线程都有运行的CPU)
如果20%的代码存在于串行块中(P=80%)在有8个CPU时,预期代码运行速度是原来的3.33倍
如果更多的代码运行于串行块中,拥有多个线程性能受益也会减少,限制串行块中的代码数量。
锁定对象的开销
1、获取同步锁的开销
如果锁是无竞争的,两个线程同时访问锁的开销会非常小
无竞争的synchronized锁被称为非膨胀锁(uninflated lock)获取时间大概几百纳秒
无竞争的CAS结构会有更小的性能损失
有竞争的
synchronized
第二个线程访问synchronized锁时,锁会变得膨胀(inflated),会增加获取的时间,而且第二个线程必须等待第一个线程释放锁,这个等待取决于应用程序。
CAS
竞争的开销是不可预知的。
使用CAS原语的类是基于一种乐观的策略:线程设置一个值,执行代码,然后确保初始值不变
如果初始值变了,基于CAS的代码必须再次执行这些代码。
最坏的情况下两个线程可能会陷入无限循环。
因为每个线程修改CAS保护值之后,发现其他线程同时进行了修改。线程数量增加,重试次数也会增加。
2、java特有的内存模型上的开销,关于同步语义的保证
JMM 同步语义
happens-before 发生前
保证在写操作前,读操作可以获取到最新的写操作的结果
visibility 可见性
一个线程对某个变量的写操作发生后,其他线程对该变量的读操作也能够看到最新的写结果
volatile关键字
具有可见性和原子性,当任何线程修改volatile变量的值会立刻被其他线程看到,并且保证其原子性,即不会出现多线程的强占,保证线程的安全性。
完全初始化的值才能完成赋值
确保完全初始化后其他线程才能看到存储到变量中的值
确保产生变量中保存的结果不能被编译器优化
同步的目的是保护对内存中的值的访问,变量会临时存储在寄存器中
寄存器比直接在主内存中访问要高效
寄存器的值对其他线程来说并不可见
修改寄存器中的值的线程必须在某个时刻将该寄存器刷新到主内存中,满足其他线程看到
刷新到主内存中的时机是由线程确定,线程依赖于JMM
从代码层理解
当一个线程离开同步块,必须将修改过的变量的值刷新到主内存中满足其他线程的使用
基于CAS的结构也保证了在操作过程中修改变量会被刷新到主内存中
对于标记volatile的变量,无论什么时候修改都会被刷新到主内存中
使用同步集合代码过度刷新寄存器缓存的惊人开销
避免同步
现实情况中多线程也不会过分的访问共享的数据(如果过分访问的话说明需要处理代码问题),增加线程也只是在增加不现实的竞争
1.每个线程中使用不同的对象
通过线程局部变量实现ThreadLocal
2.使用基于CAS的替代方案。
CAS的无锁设计,通过减少同步的损失,可以得到与避免同步相同的效果。
如果对资源的访问是无竞争的——设计师考虑的问题——基于CAS的保护则比传统的同步块
如果对资源的访问存在轻度或者适度的竞争,基于CAS的保护会比传统的同步更快
随着资源竞争激烈,传统同步会成为高效的选择,实践中发生在运行很多线程的大型机器上——服务器设计
当读取而不写入,基于CAS的保护不会受竞争的影响-读共享
伪共享
缓存系统中是以缓存行(cache line)单位存储
当多线程修改相互独立也就是非共享的变量,如果这些变量共享缓存行,
除了当前核心的缓存行失效外其他缓存行也随之失效,影响其他核心的性能——伪共享
Pros
加载内存到缓存行的同时加载相邻的值,如果程序访问某个特定变量,如果继续访问
相邻变量的时候因为已经在缓存行中,内存访问速度会高很多。
Cons
当程序更新本地缓冲中的某个值,该核心必须通知所有其他核心,相关内存被回收,缓存行作废,强制从内存重新加载缓冲区的数据
volatil关键字的写入失效测试
每个写操作都会使所有其他缓存行失效,而且性能是串行的
伪共享并不一定设计同步变量,当CPU缓存中的数据值被写入时,
持有相同数据范围的其他缓存必须失效。
JMM要求只有在同步原语(cas,volaile)结束数据才能写到主内存中。
也就是说在JMM的语义下,如果同步块没有结束,缓存失效是最常遇到的事情。
tips: 如果一次普通的性能分析显示某个循环花费了惊人的时间
请检查是否有多个线程在循环中访问非共享变量
-XX:-RestrictContended 开启非JDK包使用填充注解@Contended
-XX:-EnableContended 标志禁止JDK自动填充 默认开启