synchronized与CAS(更新)
synchronized(重量级锁)
synchronized概念
原子性:synchronized保证语句块内操作是原子的
可见性:synchronized保证可见性(通过“在执行unlock之前,必须先把此变量同步回主内存”实现)
有序性:synchronized保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”)
synchronized原理
Java字节码层级:
synchronized(o)
字节码层级:
使用javap -c Synchronize可以查看编译之后的具体信息。monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor(在对象头中)与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;
CPU汇编层级:lock comxchg
CAS(轻量级锁)
CAS概念
Compare And Swap (Compare And Exchange) / 自旋 / 自旋锁 / 无锁 (无重量锁),作用是保证多个线程对一个线程的更新。
CAS底层
自己写过的原子类:
AtomicInteger i = new AtomicInteger();
i.incrementAndGet();
点击incrementAndGet(),跳转到unsafe类:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
发现调用了compareAndSwapInt方法。
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;
}
通过native实现,也是lock cmpxchg 指令。其中在硬件层级上lock指令在执行后面指令的时候锁定一个北桥信号。
ABA问题
其他线程修改数字值后,值与原值相同。
解决措施: 增加版本号或者boolean类型。
对象:
对象创建过程
①类加载检查: 虚拟机遇到⼀条 new 指令时,⾸先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引⽤,并且检查这个符号引⽤代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执⾏相应的类加载过程。
②分配内存: 在类加载检查通过后,接下来虚拟机将为新⽣对象分配内存。对象所需的内存⼤⼩在类加载完成后便可确定,为对象分配空间的任务等同于把⼀块确定⼤⼩的内存从 Java 堆中划分出来。分配⽅式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配⽅式由 Java 堆是否规整决定,⽽Java堆是否规整⼜由所采⽤的垃圾收集器是否带有压缩整理功能决定。
③初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。
④设置对象头: 初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是那个类的实
例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对
象头中。 另外,根据虚拟机当前运⾏状态的不同,如是否启⽤偏向锁等,对象头会有不同的设置⽅
式。
⑤执⾏ init ⽅法: 在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从
Java 程序的视⻆来看,对象创建才刚开始, ⽅法还没有执⾏,所有的字段都还为零。所以⼀
般来说,执⾏ new 指令之后会接着执⾏ ⽅法,把对象按照程序员的意愿进⾏初始化,这样
⼀个真正可⽤的对象才算完全产⽣出来。
对象的访问定位有哪两种⽅式
⽬前主流的访问⽅式有①使⽤句柄和②直接指针两种:
句柄: 如果使⽤句柄的话,那么Java堆中将会划分出⼀块内存来作为句柄池,reference 中存
储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃的具体地址信息;
直接指针: 如果使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型
数据的相关信息,⽽reference 中存储的直接就是对象的地址。
堆内存中对象的分配的基本策略
堆空间的基本结构:
上图所示的 eden区、s0区、s1区都属于新⽣代,tentired 区属于⽼年代。⼤部分情况,对象都会⾸先在 Eden 区域分配,在⼀次新⽣代垃圾回收后,如果对象还存活,则会进⼊ s0 或者 s1,并且对象的年龄还会加 1(Eden区i>Survivor 区后对象的初始年龄变为1),当它的年龄增加到⼀定程度(默认为15岁),就会被晋升到⽼年代中。对象晋升到⽼年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
另外,⼤对象和⻓期存活的对象会直接进⼊⽼年代。
对象内存布局
对象头(markwoed8字节,类型指针4字节) 实例数据0字节 对齐位 4字节,其中类型指针压缩时占用4字节(JVM默认开启),关闭时占用8字节。
问下面对象头占多少字节?16个字节,如果算上引用类型b还是16个字节,因为对齐位为0。
Object b = new Object();
对象头hotspot实现
对象头里面有锁状态信息、GC信息、hashCode
锁升级过程
用markword中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位。
- 对象的创建时,若偏向锁未启动则创建普通对象,如偏向锁启动则创建匿名对象。偏向锁会在jvm启动后4s默认开启。
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
打开偏向锁时间为0。上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程。 - 撤销偏向锁,升级轻量级锁线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁。
- 竞争加剧:有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半, 1.6之后,加入自适应自旋 Adapative Self Spinning , JVM自己控制升级重量级锁:-> 向操作系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间。
锁消除 lock eliminate
public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。
锁粗化 lock coarsening
public String test(String str){
int i = 0;
StringBuffer sb = new StringBuffer():
while(i < 100){
sb.append(str);
i++;
}
return sb.toString():
}
VM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。
锁降级
其实,只被VMThread访问,降级也就没啥意义了。所以可以简单认为锁降级不存在!