JMM(java memory model):定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性、原子性的规则和保障
原子性
synchronized加锁
可见性
不同线程可能在各自的 CPU 缓存中持有变量的副本,导致读到的值不一致
解决:volatile 易变关键字
他可以用来修饰成员变量和静态变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取他的值,线程操作volatile变量都是直接操作主存
synchronized也可以保证可见性,但是属于重量级操作,性能相对较低
有序性
同一个线程,JVM会在不影响正确性的前提下,调整语句的执行顺序
volatile 保证"前面的先发生,后面的后发生"
volatile修饰变量会禁止指令重排
JDK5以上版本volatile才真正有效
happens-before
happens-before规定了哪些写操作对其他线程的读操作可见,他是可见性和有序性的一套规则总结
- 线程解锁之前对变量的写,对于接下来对m加锁的其他线程对该变量的都可见
- 线程对volatile变量的写,对接下来 其他线程对该变量的读可见
- 线程start前对变量的写,对该线程开始后对该变量的读可见
- 线程t1打断t2前对变量的写,对于其他线程得知t2被打断后对变量的读可见
- 对变量默认值的写,对其他线程对该变量的读可见
- 具有传递性
变量都是指成员变量,静态变量(共享变量)
CAS
原子类 compare and swap,它体现的一种乐观锁的思想
获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰,结合CAS和volatile可以实现无锁并发,适用于竞争不激烈,多核CPU的场景下
竞争激烈,重试频繁发生,效率受影响
juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、AtomicBoolean等,底层采用CAS技术+volatile来实现
synchronized优化
每个对象都有对象头(包含class指针和Mark Word)。Mark Word平时存储这个对象的哈希码、分代年龄,当加锁时,这些信息就根据情况被替换为标记位、线程锁记录指针、重量级锁指针,线程ID等内容
Mark Word 中包含锁标志位用于标识当前锁状态,在不同状态下其余位的含义不同;当对象处于偏向锁时存储线程ID,处于轻量级锁时存储指向线程栈中锁记录的指针,处于重量级锁时存储指向 Monitor 的指针,这些信息在同一时刻只会存在一种,而不是同时存在。
synchronized 锁在运行过程中整体是向更高开销方向演进的,轻量级锁在竞争加剧时会膨胀为重量级锁且不会降级,而偏向锁在特定情况下可能被撤销并转为轻量级锁或无锁状态。
偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作,java6引入了偏向锁来进一步优化,只有第一次使用CAS将线程ID设置到对象的Mark Work头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS
- 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要运行到安全点暂停(STW)
- 访问对象的hashCode也会撤销偏向锁
- 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,只需重置对象的threadID
- 撤销偏向和重偏向都是批量进行的,一类为单位
- 如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的
- 可以使用-XX:-UseBiasedLocking禁止偏向锁
轻量级锁
一个对象虽然有多个线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化
每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
重量锁
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,说明成功概率大,多自旋几次
加锁流程
synchronized 加锁时,首先尝试偏向锁,将线程ID写入对象头;若存在竞争,则升级为轻量级锁,将对象头的 Mark Word 拷贝到线程栈中的 Lock Record,并通过 CAS 将对象头替换为指向该 Lock Record 的指针;若 CAS 失败,会判断是否为当前线程重入,若是则直接进入临界区,否则自旋或升级为重量级锁;重量级锁通过 Monitor 实现,竞争线程会进入阻塞队列;释放锁时,轻量级锁通过 CAS 恢复对象头,若失败说明已膨胀为重量级锁,由 Monitor 机制唤醒等待线程。
其他优化
- 减少上锁时间
同步代码块中尽量短 - 减少锁的粒度
将一个锁拆分为多个锁提高并发度(例如hash表添加元素我不需要锁整个表,只需要锁对应的桶) - 锁粗化
多次循环进入同步代码块不如同步代码块中多次循环
另外JVM可能做如下优化,把多次append的加锁操作粗化为一次 - 锁消除
JVM会进行代码的逃逸分析,例如某个加锁对象时方法内部局部变量,不会被其他线程所访问到,这时候就会被JIT忽略掉所有同步操作 - 读写分离
CopyOnWriteArrayList
ConyOnWriteSet