Synchronized
想搞明白 synchronized的机制,我们需要先搞清楚CAS方法的底层原理。
CAS
多线程的情况下,i++可能会出现脏数据情况,所以需要加synchronized 。
jdk 1.5之后出现了CAS方法,一种乐观锁,自旋锁,在线程安全的情况下可以进行数据更改
CAS操作时会拿到三个值(目前值,期望值,结果值),当目前值==期望值,则对数据进行操作。
可能会出现ABA问题。其他线程对数据进行操作,操作之后又将数据改为期望值。
解决方法:对数据加version。只要出现更改,则将version+1;
CAS底层
CompareAndSwap 比较和交换,CAS底层由C++代码调用汇编指令实现,如果是多核cpu则使用 lock cmpxchg指令,单核则compxch指令。
CPU层级CAS过程:从内存中读取数据到cpu寄存器中,在寄存器中把数据更改,将数据返回内存前检查内存中的数据是否发生改变,如果一致则将数据放回。只要不成功则一直循环。
同时 lock cmpxchg指令也是 synchronized和volatile的底层实现
AtomicInteger中CAS的实现:
有一个increamAndGet()方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
其中 调用unsafe类的getAndAddInt()
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
synchronized锁升级过程:
无锁–>偏向锁–>轻量级锁–> 自旋锁
当加synchronized关键字后,会生成monitorenter字节码监视,当sunchronized代码块结束后会执行monitorexit退出监视。
在监视过程中,会执行C++代码,大概过程如下:
if(支持偏向锁){
执行 fast_enter快速上锁
}else{
执行 slow_enter慢速上锁
}
fast_enter(){
为对象加偏向锁
如果偏向锁失败了,就会执行 slow_enter();
}
slow_enter(){
为对象加轻量级锁(自旋锁、无锁)
如果失败,则进行锁粗化。膨胀为重量解锁
}
对象的内存布局
我们使用JOL包里面的 ClassLayout.parseInstance()方法打印出 Object o对象的对象头详细信息:
Object o=new Object();
String s= ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);
//结果如下:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) (前两个称为MarkWor) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) (指向类) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
new一个新的对象时,大小为16个字节。最后四个四节用于对齐。任何一个对象的大小必须是8的倍数。
对象头MarkWord:
锁升级:
-
偏向锁:某个线程进入synchronized时,将自己的线程ID存放在对象头中,之后CPU轮询其他线程,此线程中断。等恢复后此线程查看对象头中的线程ID如果是自己,则继续使用
-
轻量级锁(自旋锁):当偏向锁状态时有其他线程来争夺资源时,进入轻量级锁。 对象头内记录了指向栈中锁记录的指针。进入轻量级锁时,多个线程使用CAS方式抢夺这个指针(使其的值等于线程自身内的LR(LockRecord 锁记录)地址) 。此时对象头中的其他信息(对象的HashCode、分代年龄等)会存放在线程栈中
-
重量级锁:在竞争加剧时(有些线程自旋次数超过10次、自旋线程数超过CPU核数的一半)会升级为重量级锁。重量级锁会惊动操作系统,从用户态转换为内核态,操作系统会进行调度。 其他线程想使用资源时,首先转换为内核态,然后排队等待资源
-
对于是否竞争加剧的判断,1.6之后加入了自适应自旋,整个过程由JVM自行判断
锁消除
public void add(String str1,String str2){
StringBuffer sb=new StringBuffer();
sb.append(str1).append(str2);
}
当某个对象不可能被其他线程引用(比如局部变量,栈私有)时,JVM会自动消除对象的锁。如上代码中,sb使StringBuffer对象,属于线程安全。但sb为局部变量,只能在add方法中调用
锁粗化
public String test(String str){
int i=0;
StringBuffer sb=new StringBuffer();
while(i<100){
sb.append(str);
i++;
}
return sb.toString();
}
JVM会检测到一连串的操作都是堆同一个对象加锁,此时JVM会把加锁的范围粗化到这串操作的外部,使得只加一次锁。
volatile
volatile的用途
-
线程可见性
某个值在线程A中被改变后,会将值刷新到主内存,通知其他线程去获取新的值。
-
防止指令重排序
指令重排序:CPU在读等待同时会执行不影响结果的其他指令,而写的同时可以进行合并写。
volatile实现细节
加了volatile关键字后,编译出的字节码中会出现 ACC_VOLATIlE修饰符,告诉虚拟机这个部分需要实现内存屏障。
内存屏障实现
-
JVM层级内存屏障
四种内存屏障
- StroreStoreBarrier 两条写指令之间不可以重排序
- StoreLoadBarrier 写指令在前,读指令在后,不可以重排序
- LoadLoadBarrier 两条读指令之间不可以重排序
- LoadStoreBarrier 读指令在前,写指令在后,不可以重排序
-
CPU层级内存屏障
- 在指令前加 Lock。(HotSpot虚拟机是通过Lock指令实现Volatile)
- 汇编指令:sfence、lfence、mfence
- MESI协议 (CPU缓存一致性协议) MESI对应缓存的四种状态,M 修改;E独享互斥;S共享;I无效。
- 锁总线。(总线:计算机中的IO总线)
- 在指令前加 Lock。(HotSpot虚拟机是通过Lock指令实现Volatile)
转载:https://blog.csdn.net/m0_46171384/article/details/104958625