1.并发编程的挑战
什么是上下文切换
CPU通过时间片分配算法循环执行任务[线程],当任务执行完一个时间片后会切换到下一个任务,在切换前会保存上一个任务的状态,以便下次切换回来时可以再加载这个任务的状态。所以任务从保存到加载的过程就是一次上下文切换。
如何减少上下文切换
- 无锁并发编程:如数据取模分段,不同线程处理不同数据
- CAS算法:Atomic包使用CAS算法来更新数据, 而不需要加锁
- 使用最少线程:避免创建不必要的线程,使得大多数线程处于等待状态
- 协程:在单线程里实现多任务调度,并在单线程里维持多个任务的切换
如何避免死锁
- 避免一个线程同时获得多个锁
- 避免一个锁同时占用多个资源
- 限制加锁顺序
- 尝试使用定时锁 lock.tryLock(timeout)
- 对应数据库锁,加锁和解锁必须在同一个数据库连接里
2.Java并发机制底层实现原理
volatile
轻量级的synchronized,在多处理器开发中保证了共享变量的“可见性”(当一个线程修改一个共享变量时,另一个线程能读到这个修改的值)
比synchronized的使用和执行成本低,因为它不会引起线程上下文的切换和调度
背景
为了提高处理速度,CPU不直接和内存进行通信,而是将系统内存的数据读到内部缓存(L1,L2,L3)后再操作,但操作完不确定何时会写回内存。这样可能导致其他CPU不能获取到共享变量的最新值。
实现原理
在x86处理器下通过工具获取JIT编译器生成的汇编指令来看看对Volatile进行写操作CPU
类别 |
代码 |
Java代码 |
instance = new Singleton(); //instance是volatile变量 |
汇编指令 |
0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp); |
Lock前缀的命令作两件事:
- 将当前CPU缓存行的数据写回到内存
- 令其他CPU里缓存了该内存地址的数据失效
缓存一致性协议
每个CPU通过嗅探在总线中传播的数据来检查自己的缓存的值是否过期,当CPU发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当CPU对这个数据进行修改操作时,会重新从系统内存中把数据读到CPU缓存中。
使用优化
LinkedTransferQueue部分源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| private transient final PaddedAtomicReference < QNode > head; private transient final PaddedAtomicReference < QNode > tail; static final class PaddedAtomicReference < T > extends AtomicReference < T > { Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe; PaddedAtomicReference(T r) { super(r); } } public class AtomicReference < V > implements java.io.Serializable { private volatile V value;
|
因为缓存行是64个字节且不支持部分填充缓存,这意味着,如果队头队尾不足64字节,有可能他们会读到同一个缓存行中,那么有可能在修改头结点时,会将整个缓存行锁定,使得其他CPU无法访问尾节点,导致效率低下。
在PaddedAtomicReference内部类中将共享变量追加到64字节,使得头结点和尾节点不会加载同一个缓存行,即不会互相锁定。
不适合使用这种优化的情景
- 缓存行非64字节的CPU
- 共享变量不会被频繁地写
synchronized
liuxiaopeng | Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
原子操作
不可被中断的一个或一系列操作
CPU实现原子操作
总线锁定
使用CPU提供的一个LOCK#信号,当一个CPU在总线上输出此信号时,其他CPU的请求将会被阻塞,那么该CPU就能独享共享内存了
缓存锁定
内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不需要在总线上声言LOCK#信号,而是修改内部的内存地址,通过缓存一致性机制保证操作的原子性。
例外:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行,处理器会调用总线锁定。或者处理器不支持缓存
Java实现原子操作
循环CAS(CompareAndSet|CompareAndSwap)
jvm中的CAS操作是基于处理器的CMPXCHG指令实现的,CAS存在三个问题:
- ABA问题(解决:使用版本号A-B-A变成1A-2B-3A)
- 循环时间长开销大
- 只能保证一个共享变量的原子操作(解决:AtomicReference类将多个变量放到一个对象中进行CAS操作)
锁
锁机制保证了只有获得锁的线程才能操作锁定的内存区域