1.synchronized锁进阶
1.1 使用场景
修饰普通方法:锁对象是当前实例对象
修饰静态方法:锁对象是当前的类对象
修饰代码块:看括号里面写了啥就是啥
1.2 实现原理
1、java对象的对象头
一个对象在堆内存中的结构如下图,其中对象头中包含:MarkWord和Klass Point(指向类元数据)
对象头中的MarkWord:
2、monitor锁对象由ObjectMonitor对象实现(c++),数据结构如下:
3、竞争锁时,线程状态间的转换
①当多个线程同时访问该方法,那么这些线程会先被放进_EntryList队列,此时线程处于blocking状态
②当一个线程获取到了实例对象的监视器(monitor)锁,那么就可以进入running状态,执行方法,此时,ObjectMonitor对象的owner指向当前线程,count加1表示当前对象锁被一个线程获取
③当running状态的线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的owner变为null,count减1,同时线程进入WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程重新获取monitor对象进入Owner区
④如果当前线程执行完毕,那么也释放monitor对象,进入waiting状态,ObjectMonitor对象的owner变为null,count减1
4、修饰代码块和修饰方法,都是基于进入和退出monitor对象来实现的,具体实现却有很大区别:
代码块:
方法:
1.3 synchronized锁优化升级
1、无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)
偏向锁:java6后引进,大多数情况,锁的竞争不激烈,锁总是由同一线程多次获取,为了减少线程获取锁(设计CAS操作,耗时)引入偏向锁。(单个线程执行场景)
轻量级锁:1.6加入,T1执行时,大部分时间没有竞争,少量竞争可以通过cas来解决(线程交替执行场景)
重量级锁:线程会放弃cpu执行权,涉及内核态用户态转换(线程竞争激烈场景)
2、锁升级
-
线程T1先判断锁标志位为01,偏向锁0,则将前54位设置为指向自己的指针;
-
此时T2尝试获得锁,如果T1刚好执行完,则直接向偏向锁指针指向T2;如果T1没有执行完,则向虚拟机申请将偏向锁升级为轻量级锁,申请成功后,T1和T2会将对象头的MarkWord复制一份到自己的栈空间(Lock Record),然后分别用CAS把对象头中的指针指向本线程存储锁记录的地址(Lock Record),比如此时T1成功了(T1将之前的时间片执行完后,再cas,虚拟机裁决此时T1,T2谁CAS成败),则T2的cas失败,T2就会尝试指定次数的自旋来等待T1释放锁。
-
如果T2超过自旋次数,T1还没有释放锁,则T2懒得自旋了,跟虚拟机申请, 将轻量级锁升级为重量级锁,此时的MarkWord中指针指向的是ObjectMonitor,T2自己(底层调用pthread.mutex_lock)去挂起在_waitSet队列里,T1执行完后发现MarkWord都变了(不指向自己栈上的LockRecord了,那肯定是升级成重量级了),于是释放锁+去唤醒_waitSet队列里的线程。
3、锁粗化
StringBuffer stb = new StringBuffer()//append方法是同步方法
public void test(){
stb.append("1");//按道理是每个方法都要加同步块,共4个,但是jvm优化后,会把这4个合成一个同步块
stb.append("1");
stb.append("1");
stb.append("1");
}
4、锁消除
public void test2(){
synchronized (new Object()){//这种无意义的加锁就会被jvm优化消除
}
}
参考:https://blog.csdn.net/tongdanping/article/details/79647337
图灵高并发专题