乐观锁和悲观锁:
CAS:Compare And Swap 比较和替换 是乐观锁的一种
是一种轻量级锁,在java的JUC中很多工具类的时效件都基于CAS的;
CAS保证线程安全的方法:
线程在读取数据时候不进行加锁,在准备写会数据的时候先去查询原值,操作的时候比较原始是否被修改,若未被其他先修改则写回,若已被修改,则重新执行读取流程。
存在的问题:ABA问题:
- 线程1进行读数据A,但是操作时间长没写回数据;
- 线程2进行读数据A;
- 然后线程2进行CAS对比原来还是A,可以修改为B;
- 线程3进行读数据B;
- 然后线程3进行CAS对比原来还是B,可以修改为A;
- 然后线程1结束操作准备写回数据,进行CAS比较原来还是A,写自己的数据。
其实数据是已经进行变化的,而A线程不知道而已;
解决:例如搞个自增的字段,操作一次就自增加一,或者搞个时间戳,比较时间戳的值。
而且CAS一直不成功会,相当于死循环,产生自旋,导致CPU压力变大。
unsafe
底层硬件支持 :最终执行时 lock cmpxchg指令 lock原子性,cmpxchg非原子性
悲观锁:JVM层的synchronize
加锁过程就相当于对临界资源的操作,PV操作,内部实施的就是一个monitor,这个留到后面来说
内存在内存中的布局:
在JVM中,对象在内存中分为四块区域,对象头(MarkWord)、类型指针(Class Pointer)、实例数据(Instance Data)、对其填充(Padding)。
三种就是对象头包含MarkWord和Class Pointer
public class ObjecyStructure {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
前提都是压缩指针效果的:
public class ObjecyStructure {
public static void main(String[] args) {
Object o = new Object();
synchronized (o){
int i = o.hashCode();
System.out.println(i);
}
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
- Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
- Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
那么时间synchronize是怎样进行所操作的呢?
- synchronized 应用在方法上时,在字节码中是通过方法的 ACC_SYNCHRONIZED 标志来实现的。
- synchronized 应用在同步块上时,在字节码中是通过 monitorenter 和 monitorexit 实现的。
- 每个对象对会有一个monitor相关联,当拥有了之后就会被锁住,并且有一个进行记录拥有次数的计数器,从0开始,有一个就加1一次。
- 当对象进行释放锁的时候,执行的是monitorexit指令,然后计数器在进行减1,当计数器为0 时,其他线程就可以进行获取。
Object o = new Object()在内存中占多少字节?
压缩的 Markword8+class pointer4+对象4+补齐4=20 或者对象空0 16字节
没压缩的:8+8+0+0=16
一个User user = new User(Integer id,String name)在内存中占多少字节?
MarkWord 8个、Class Pointer 4个,Instance(4+4个)、Padding 4个 24个字节
锁升级:
并且锁不能降级,只能升级。
降级是JVM可以操作,没有实际意义,就是进行回收时。
锁升级条件:
-
new----->偏向锁:指针之向就行,相当于上一个标签说明,直接对标志为加1就行;偏向锁关闭,或者多个线程竞争偏向锁怎么办呢?
-
偏向锁------>轻量级锁:跟MarkWord相关,JVM会在对应的线程的栈帧建立一个锁记录(Lock Record)的空间,用来存储锁对象的Mark Word 拷贝,使用LR的owner去指向当前对象。(产生了竞争关系)
-
轻量级锁----->自旋锁----->重量级锁:自旋,过来的现在就不断自旋,防止线程被挂起,一旦可以获取资源,就直接尝试成功,直到超出阈值,自旋锁的默认大小是10次或者占一半内存资源升级为重量级锁,等待唤起
-XX:PreBlockSpin可以修改。
锁消除
虚拟机根据一个对象是否真正存在同步情况,若不存在同步情况,则对该对象的访问无需经过加锁解锁的操作。比如StringBuffer的append方法,append方法需要判断对象是否被占用,代码不存在锁的竞争,那么这部分的性能消耗是无意义的;虚拟机在即时编译的时候就会将上面代码进行优化,也就是锁消除。
pub1ic void add(String strl,String str2){
StringBuffer sb = new stringBuffer();
sb. append(str1). append(str2);
//同如下代码
//sb.append(str1);
//sb.append(str1);
我们都知道StringBuffer是线程安全的,因为它的关键方法都是被synchronized修饰过的,但我们看上面这段代
码,我们会发现,sb这个引用只会在add方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因
此sb是不可能共享的资源,JVM 会自动消除StringBuffer对象内部的锁。
锁粗化
public String test(String str){
int i = 0;
stringBuffer sb = new stringBuffer():
while(i < 100){
sb. append(str);
i++;
}
return sb. tostring():
}
JVM会检测到这样一连串的操作都对同一个对象加锁(while 循环内100次执行append,没有锁粗化就是执行100次加锁/解锁),此时JVM就会将加锁的范围粗化到这一-连串的操作的外部(比如while虚幻体外),使得这一连串操作只需要加一次锁即可。