网站可以做第三方检测报告,东方甄选采用了哪些网络营销方式,南宁网站建设公司怎么赚钱,网站开发后端JUC系列整体栏目 内容链接地址【一】深入理解JMM内存模型的底层实现原理https://zhenghuisheng.blog.csdn.net/article/details/132400429【二】深入理解CAS底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/132478786【三】熟练掌握Atomic原子系列基本…JUC系列整体栏目 内容链接地址【一】深入理解JMM内存模型的底层实现原理https://zhenghuisheng.blog.csdn.net/article/details/132400429【二】深入理解CAS底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/132478786【三】熟练掌握Atomic原子系列基本使用https://blog.csdn.net/zhenghuishengq/article/details/132543379【四】精通Synchronized底层的实现原理https://blog.csdn.net/zhenghuishengq/article/details/132740980 深入理解synchronized底层的实现原理 一深入理解synchronized底层的实现原理1初识synchronized1.1类锁1.2对象锁 2synchronized在jvm的字节码指令3Monitor监视器4对象的内存布局5锁的几种状态5.1偏向锁5.2轻量级锁5.3重量级锁 6锁的升降级方式6.1匿名偏向锁和偏向锁关系6.2无锁轻量级锁和重量级锁之间关系6.3偏向锁的撤销 7jvm对synchronized锁的优化7.1偏向锁批量重偏向优化7.2偏向锁批量撤销优化7.3重量级锁自旋优化7.4锁粗化和锁消除 8synchronized锁误区 一深入理解synchronized底层的实现原理
1初识synchronized
在最前面的两篇文章中谈了java的内存模型JMM得知了为何会出现共享变量的不安全性同时也谈到了通过无锁的方式实现共享变量安全的CAS但是CAS本身也存在着一定的缺陷不能适用于大规模并发的场景下因此从这篇开始讲解一个通过加锁的方式来实现共享变量的安全性就是这篇的主角 synchronized
synchronized同步块是java内部提供的一个内置锁又被称为监视器锁monitor其实现是通过操作系统底层的互斥量来实现的。主要是针对一些临界区中的临界资源进行上锁的操作其使用相对来说也比较简单主要分为类锁和实例锁。接下来谈一下这个关键字是如何使用的。
1.1类锁
顾名思义就是将锁加在类方法或者静态代码块的上面添加到类方法的方式如下
public static Integer data 0;
public synchronized static void increment(){data ;
}除了上面的加在方法上之外还可以直接添加到同步代码块里面
public static Integer data 0;
public static void toIncrement(){synchronized (SynchronizedByClass.class){data ;}
}1.2对象锁
对象锁指的就是加在实例方法的上面以及实例方法中的代码块上面添加到实例方法的方式如下
public Integer decrementData 0;
public synchronized void decrement(){this.decrementData -- ;
}除了加在方法上也可以直接通过加在代码块上面的形式加在对象上面
public void decrementData1(){synchronized (this){this.decrementData --;}
}private String lock ;
public void decrementData2(){synchronized (lock){this.decrementData --;}
}2synchronized在jvm的字节码指令
由于synchronized是一个关键字因此可以通过查看其字节码指令去了解底层是如何实现的。在分析之前需要在idea中安装一个插件 jclasslib 我在jvm系列中曾经讲过这个插件如何安装以及使用https://zhenghuisheng.blog.csdn.net/article/details/129610963
接下来通过一段简单的代码来分析加在方法中其底层是如何实现的
public class SynchronizedJvmCode {public Integer data 0;public synchronized void add(){this.data ;}
}在methods中找到这个add方法在右边可以看到一个重要的标志Access flags其对应的值是0x0021 在jdk关键字中详细的描述了这个flag标志的信息0x0021对应的就是这个ACC_SYNCHRONIAZED 这个指令因此显而易见在方法上加synchronized是通过这个 ACC_SYNCHRONIAZED 标志实现的 接下来通过一段简单的代码来分析加在代码块其底层是如何实现的
public class SynchronizedJvmCode {public Integer data 0;public void add(){synchronized (this){data ;}}
}这个add方法对应的字节码的指令如下可以发现在这里面多两个东西分别是monitorenter和monitorexit 分别代表着加锁和解锁的意思因此在代码块中就是通过这两个指令实现锁的操作的。
并且在这个字节码指令中存在两个 monitorexit 根据下面31行已经解锁了跳到后面41return了但是后面还有字节码操作通过第40行可以发现就是为了防止出现异常导致死锁类似于在try中有解锁操作在catch中也有解锁的操作这样不管有没有异常都能正常解锁 0 aload_01 dup2 astore_13 monitorenter //加锁4 aload_05 getfield #3 com/zhs/study/juc/synchronize/SynchronizedJvmCode.data8 astore_29 aload_0
10 aload_0
11 getfield #3 com/zhs/study/juc/synchronize/SynchronizedJvmCode.data
14 invokevirtual #4 java/lang/Integer.intValue
17 iconst_1
18 iadd
19 invokestatic #2 java/lang/Integer.valueOf
22 dup_x1
23 putfield #3 com/zhs/study/juc/synchronize/SynchronizedJvmCode.data
26 astore_3
27 aload_2
28 pop
29 aload_1
30 monitorexit //解锁
31 goto 41 (10)
34 astore 4
36 aload_1
37 monitorexit //解锁
38 aload 4
40 athrow
41 return也就是说这个加锁和解锁是jvm内部帮我们实现的不需要我们手动去加锁解锁相对于Lock这种显示锁synchronized就是一把隐式锁。
总结来说就是如果在方法上加这个synchronized其底层是通过ACC_SYNCHRONIAZED标志实现的如果是在同步块上synchronized其底层是通过monitorEnter和monitorExit实现的。但是这两种方式都是通过jvm去调用操作系统来实现的这样就会涉及到用户态到内核态之间的来回切换以及会涉及到阻塞等等问题因此这个关键字的使用也是挺耗性能的相对于volatile来说这个synchronized就是一把重锁。
3Monitor监视器
在操作系统中monitor又可以被称为这个管程主要是帮助共享变量在并发场景下可以保证数据的安全性。在java中实现管程的方式是由synchronized关键字和waitnotify和notifyAll这三个方法共同实现的。其底层的模型架构如下
在hotspot虚拟机中有关Monitor的底层实现的部分源码如下
ObjectMonitor() {_recursions 0; // 锁的重入次数 _object NULL; //存储锁对象_owner NULL; // 标识拥有该monitor的线程当前获取锁的线程 _WaitSet NULL; // 等待线程调用wait组成的双向循环链表_WaitSet是第一个节点_cxq NULL ; //多线程竞争锁会先存到这个单向链表中 FILO栈结构_EntryList NULL ; //存放在进入或重新进入时被阻塞的线程 (也是存竞争锁失败的线程)里面有一个锁的重入次数表示synchronized是一把可重入锁里面主要有三个队列一个是双向循环列表实现的waitSet队列里面存储的是调用wait方法释放锁之后阻塞的线程从外面进来的大量线程在没有拿到锁的情况下会进入这个cxq的队列里面而cxq的数据结构是栈的方式实现就是先进后出表示这是一把非公平锁并且不能保证有序性entryList存储的是被阻塞的线程会和cxq中的线程一起去抢锁
接下来用一段代码来演示一下内部的整个流程如下面这段代码首先三个方法同时抢一把锁此时模拟为三个线程由于代码从上往下执行因此这个thread1先进入cxq队列中随后是2,3。然后3先拿到锁有一个wait方法会释放资源和释放锁此时thread3就进入这个waitSet的队列里面thread2和thread3一样此时就剩thread1线程就会拿到锁。
private String lock ;
public void thread1() throws InterruptedException {synchronized (lock){... }
}
public void thread2() throws InterruptedException {synchronized (lock){wait(100);}
}
public void thread3() throws InterruptedException {synchronized (lock){wait(300); }
}如果此时thread2和thread3被notifyAll给唤醒此时这两个线程会从waitSet队列中进入entryList队列或者cxq队列主要是根据不同策略实现的随后这两个队列的线程再次一起去抢锁。如果entryList队列的数据为空则直接将cxq的队列数据全部存储到entryList里面如果entryList的数据不为空则优先唤醒entryList里面的线程。 总而言之CXQ队列是线程刚从外面进来的队列由于内部采用的是栈结构先进后出所以整体是一个非公平锁的操作waitSet队列存储的是加了wait被阻塞的线程wait是会释放资源的当被唤醒后会重新进入EntryList或者CXQ队列中这取决于不同的策略实现EntryList中线程被唤醒的优先级高于CXQ队列
synchronized在多线程抢占锁时采用的是cas的方式实现的。
4对象的内存布局
上面有提到monitor监视器是将锁加在对象上面的那么一个对象上面是否加锁那就得了解一下这个java中对象的内存布局其主要可以分为三个部分对象头实例数据和对齐填充。以下所有例子都是用64位的虚拟机
对象头里面主要是会记录一些对象的hashcode年龄线程id锁的标志和锁的状态等实例数据类中的一些属性信息等对齐填充每个对象所占的字节数必须是8的整数倍否则补齐 在对象头中又可以分为三个部分分别是Mark WordMetaData压缩指针和数组长度。
Mark word主要存储一些对象的hashcode年龄线程id锁的标志和锁的状态等一般占8个字节Klass Pointer指的是对象的压缩指针在jdk8中默认是开启压缩指针的一般占用4个字节如果没有开启则占8个字节。虚拟机通过这个指针来确定这个对象是属于哪个实例的如果一个对象中存在数组那么这个数组默认占用4个字节
因此看下面这个类如果new一个Data这个类那么占用的字节数如下对象头中的markWord占8个字节压缩指针占4个字节数组占4个字节实例数据age占4个字节总共占20个字节但是对齐填充中需要满足是8的整数倍因此总共占24个字节。
class Data{private int age;private int code[];
}5锁的几种状态
在这个markword中会储存关于锁的信息以jvm64位的虚拟机为代表如下图所示。在synchronized中主要可以分为无锁状态、偏向锁状态、轻量级锁状态和重量级状态无锁通过001表示偏向锁通过101表示轻量级锁通过00表示重量级锁通过10表示。 在这几种锁中会随着锁的竞争激烈程度不断的变强会从当没有线程时处于一个无锁状态当有一个线程时会处于偏向锁状态随后会随着并发的强度不断的上升锁的强度从轻量级锁再到重量级锁并且这是一个不可逆的过程。
5.1偏向锁
但是在jdk6开始默认这个偏向锁是延迟开启的因为在jvm进行类初始化等操作的时候会使用大量的synchronized关键字也就是说在加载阶段我们可以明确是可能存在多个线程并发的如果还按先偏向锁再到轻量级锁这样就可能会有部分性能问题因此为了解决这个问题干脆就直接从无锁到轻量级锁了从而将这个偏向锁省略或者延迟加载。jvm默认采用的是延迟加载的默认是在jvm虚拟机启动4s之后开始加载也就是说如果没有任何操作只有在jvm启动4s后加载的对象才有可能出现偏向锁。以下是关于jvm操作偏向锁的一些参数。
//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay0
//禁止偏向锁
-XX:-UseBiasedLocking
//启用偏向锁
-XX:UseBiasedLocking 当然也可以直接通过强行睡眠的方式来解决这个偏向锁问题
Thread.sleep(4000);但是根据下图可以发现在偏向锁中并没有存储这个对象hashcode的地方因此如果在睡眠4s之后再调用这个hashcode方法就会出现这个偏向锁撤销的情况又由于这几种锁的状态不可逆所以会直接从偏向锁状态升级为轻量级锁的状态也可能会升级成重量级锁。
除了调用这个hashcode之外也可能调用wait方法或者notifyAll方法等锁出现偏向锁失败的场景。
5.2轻量级锁
在无锁或者偏向锁中都可能升级为轻量级锁。轻量级锁顾名思义就是此时争取锁的线程不多没那么激烈或者说线程与线程之间交替执行。由于synchronized底层抢锁是过cas的方式实现轻量级锁并不需要cas就能拿到锁如果需要长时间cas那么就会进行一个锁膨胀的操作最后去获取一个monitor对象变成重量级锁。
由于延迟偏向锁是4s后开始的因此开启一个延迟偏向锁随后创建一个Object对象并且创建两个线程
public static void main(String[] args) throws InterruptedException {//开启延迟偏向Thread.sleep(5000);//延迟4s后才开始加载的对象Object lock new Object();System.out.println(ClassLayout.parseInstance(lock).toPrintable());//创建线程1new Thread(()-{synchronized (lock){System.out.println(ClassLayout.parseInstance(lock).toPrintable());}},thread1).start();//创建线程2new Thread(()-{synchronized (lock){System.out.println(ClassLayout.parseInstance(lock).toPrintable());}},thread2).start();}其输出打印的结果如下由于延迟偏向锁的开启此时状态为101但是此时并没有偏向哪个线程随后第二个线程打印出来的也是101还是延迟偏向锁代表刚刚那个偏向锁现在已经有执向的线程了又有了第二个线程来抢锁随后随着锁竞争的激烈程度锁就行了升级变成了00就是轻量级锁。 在轻量级锁中拿到锁的线程会将对象锁的markword存储在当前栈帧中 而markword中存储的线程id也是当前线程的id当有别的线程来抢锁时需要通过cas操作就是看是否携带这个markword以及线程的id是否匹配如果不匹配则需要继续自旋。而当前线程执行完成之后需要将轻量级锁变成无锁状态别的线程才能获取到锁锁的不可逆指的是重量级锁到轻量级锁的不可逆以及轻量级锁到偏向锁的不可逆。
5.3重量级锁
偏向锁和轻量级锁都是通过操作mark word来修改对象锁的状态的但是重量级锁不一样需要切换到内核态进行锁状态的修改需要调用底层的moniter机制来实现。也就是说前面两个不需要加锁或者cas就能操作后者需要用户态到内核态之间的来回切换。重量级锁就是在cas时经过长时间轮询还是不能获取到锁那么这个锁就会升级膨胀随后会去获取操作系统底层的monitor对象此时轻量级锁升级为重量级锁并且期间需要不断的cas自旋。只有在重量级锁需要长时间自旋轻量级锁和偏向锁是不需要自旋的
依旧是采用下面这段代码再在轻量级锁那段代码上面再加一个线程thread3
new Thread(()-{synchronized (lock){System.out.println(ClassLayout.parseInstance(lock).toPrintable());}
},thread3).start();上面的代码结果如下可以发现前面两步开始延迟偏向锁但是第三步开始就不一样了因为随着锁的竞争强度的增加从原来的00轻量级锁变成了现在的10重量级锁 重量级锁到轻量级锁的是不可逆的但是重量级锁可以直接到无锁状态。并且根据轻量级锁和重量级锁的两段代码可以发现并不存在无锁到偏向锁的过程要么就是无锁要么就是偏向锁而且都是用01表示表明其实这两个是互斥的。
总而言之如果线程没有开启延迟偏向锁那么对象刚加锁后会由无锁变成轻量级锁的状态轻量级锁在获取锁失败的情况下就会膨胀获取到monitor对象随后由轻量级锁变成重量级锁内部通过cas的方式竞争锁如果线程开启了延迟偏向锁那么对象会自动进入一个匿名偏向锁的状态随后在拿到一把锁之后对象会进入一个有指向线程id的偏向锁状态随后通过一些列的偏向锁锁撤销等操作随着偏向锁撤销等操作进入无锁轻量级锁或者重量级锁。
6锁的升降级方式
上面讲了几种synchronized锁的状态有无锁、偏向锁、轻量级锁和重量级锁这几种锁接下来详细谈一下底层是如何进行锁升降级的。接下来以下图为主要核心讲解这几个锁之间的关系。 6.1匿名偏向锁和偏向锁关系
假设此时延迟偏向锁没有关闭那么在4s后的延迟偏向锁开启之后创建一个锁对象因此这个锁对象中会有一个markword此时该对象是处于一个偏向锁的状态但是由于并没有线程来获取这把锁此时执行的线程id为0锁标志位101记录在markword中此时的锁为一个匿名偏向锁的一个状态。很多人会觉得匿名偏向锁是一个无锁状态其实不是通过标志位就可以知道101是一个偏向锁的状态001才是无锁状态。
假设此时有一个线程进来拿这把锁(可以看5里面的例子)那么此时还是一把偏向锁此时对象锁obj中的markword中的线程id会指向偏向抢这把锁的线程id该线程id为操作系统底层的id值。并且在偏向锁解锁后不会变成无锁状态还是一把偏向锁状态。
Object obj ;6.2无锁轻量级锁和重量级锁之间关系
1假设在不考虑偏向锁的情况下此时无锁、轻量级锁和重量级锁的升级关系是这样的
首先在没有线程来竞争这把对象锁时此时的对象锁中的markword的锁标志是001是一个无锁状态当有一个线程或者线程交替执行的时候此时对象锁会有指针指向拿到这把锁的线程并且将markword中的值改成00拿到锁的线程也会将无锁时的markword保存在栈帧内部此时无锁状态升级成轻量级锁状态在轻量级锁中会随着cas长时间拿不到锁而膨胀当拿到monitor对象之后会升级成一把重量级锁此时对象锁中的markword的锁标志位10。
上面三种情况是随着线程抢锁的激烈程度增加而增加的也有可能直接出现从无锁到重量级锁的情况如某一时刻的并发量大需要大量的长时间的cas那么此时会从无锁直接升级成重量级锁。
2既然存在锁升级的情况那么也肯定存在锁降级的情况其关系如下
轻量级锁状态在释放锁的时候如果此时没有其他线程来竞争锁那么此时会将锁释放并且将当前线程中保存的markword还原给初始的无锁状态。重量级锁和轻量级锁一样在释放锁时也会将锁从重量级锁降级成无锁状态。
不存在重量级锁到轻量级锁之间的降级这两个是不可逆的因为有monitor对象会优先使用monitor对象。在锁降级时当前线程会将一开始保存的初始markword还原回去这样不管过程如何修改最终都可以还原锁对象最初的无锁状态。
6.3偏向锁的撤销
通过上述5中的例子可以发现当偏向锁解锁之后还是处于偏向锁的状态而不是无锁因此就引入了这个偏向锁撤销的概念。还是得看着下面的这个图来解释假设此时对象锁处于偏向锁状态然后在内部调用hashcode方法而此时偏向锁中并没有存储hashcode值的地方那么就会出现三种情况 1假设此时还是一个匿名偏向锁如下面的lock锁此时是一把匿名偏向锁随后调用hashcode方法
public static void main(String[] args) throws InterruptedException {//开启延迟偏向Thread.sleep(5000);//延迟4s后才开始加载的对象Object lock new Object();System.out.println(ClassLayout.parseInstance(lock).toPrintable());lock.hashCode();System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}接下来查看他的打印日志有图有真相从101状态变成了001状态这就是锁撤销成了无锁状态 2假设此时是一把有偏向线程的偏向锁随后定义两个线程随后也是调用这个hashcode对象
public static void main(String[] args) throws InterruptedException {//开启延迟偏向Thread.sleep(5000);//延迟4s后才开始加载的对象Object lock new Object();System.out.println(ClassLayout.parseInstance(lock).toPrintable());//两个线程竞争成为轻量级锁new Thread(()-{synchronized (lock){//System.out.println(ClassLayout.parseInstance(lock).toPrintable());}},thread1).start();new Thread(()-{synchronized (lock){System.out.println(ClassLayout.parseInstance(lock).toPrintable());}},thread2).start();Thread.sleep(500);lock.hashCode();System.out.println(ClassLayout.parseInstance(lock).toPrintable());}其结果很明显第一个打印出来的是匿名偏向锁此时还没有线程来拿锁第二步是直接成为了00轻量级锁随后轻量级锁释放锁成为001无锁。 3依旧是有偏向线程id的偏向锁在一个线程中休眠一会再调用这个hashcode方法最后打印日志
public static void main(String[] args) throws InterruptedException {//开启延迟偏向Thread.sleep(5000);//延迟4s后才开始加载的对象Object lock new Object();System.out.println(ClassLayout.parseInstance(lock).toPrintable());//两个线程竞争成为轻量级锁new Thread(()-{synchronized (lock){System.out.println(ClassLayout.parseInstance(lock).toPrintable());lock.hashCode();System.out.println(ClassLayout.parseInstance(lock).toPrintable());}},thread1).start();Thread.sleep(500);
}这样就实现了从偏向锁101撤销到重量级锁10了。 锁的撤销一般是在程序的安全点进行操作如触发GC时程序异常时等。
7jvm对synchronized锁的优化
7.1偏向锁批量重偏向优化
在markword的偏向锁中有一个Epoch字段该字段主要是记录同一个对象偏向锁撤销的次数在多线程的条件下如果Epoch存储的值达到一定的阈值的时候就会触发这个批量重偏向的优化操作因为偏向锁的撤销是需要花费一定的性能的而大量线程一直去撤销同一个偏向锁对象因此这里就做了重偏向的优化
重偏向简而言之就是复用的意思原先在一个偏向锁中其对应指向的线程id是不变的后面在jvm内部是做了优化的假设第一个线程里面有50个对象锁存放在list里面第二个线程还是用list里面的这50个对象那么当第二个线程撤销重偏向的次数达到20的时候后面的对象会直接进行重偏向操作就是复用第一个线程的偏向锁从而减少偏向锁撤销所带来的性能影响。
主要是jvm会认为当前锁对象是不是重偏向错了于是会重置锁对象的线程ThreadId
intx BiasedLockingBulkRebiasThreshold 20 //默认偏向锁批量重偏向阈值这个就有点类似于线程池中线程复用的原理但是偏向锁在重偏向时会有对应的阈值主要是通过jvm内部优化
7.2偏向锁批量撤销优化
这个批量撤销相对而言更好理解因为偏向锁撤销肯定会影响性能因此也会对这个Epoch的统计做一个阈值处理当达到40时JVM就会觉得这个类干脆就不用偏向锁的状态直接进入无锁状态从而省去锁撤销锁带来的性能问题。
intx BiasedLockingBulkRevokeThreshold 40 //默认偏向锁批量撤销阈值批量重偏向和批量撤销主要是针对锁的优化并且偏向锁只能重偏向一次
7.3重量级锁自旋优化
在这几种锁中轻量级锁和偏向锁都不存在自旋操作只有这个重量级锁存在自旋。在自旋之前如果直接使用阻塞的方式抢锁那么需要不断的用户态切换到内核态去抢占那么jvm就直接在用户态通过cas的方式进行一个锁的竞争在用户态选出获取拿到锁的线程随后再去调用内核态进行操作从而避免大量线程阻塞问题。 在jdk6之后可以通过参数设置来决定是否开启自旋以及设置自旋的次数。 在jdk7之后不能对这个自旋的参数就行控制这个功能交给了jvm底层去自适应。
7.4锁粗化和锁消除
锁粗化指的是对同一个对象重复加锁jvm在编译期间会进行优化操作将多个锁变成一个锁。由于每个append内部都有一个synchronized锁因此内部会做一个合并将多个锁拆成一个锁
StringBuffer stringBuffer new StringBuffer();
stringBuffer.append(first).append(second).append(three);在jvm中对象有可能并不是垃圾回收器回收的而是随着入栈出栈被销毁的这种技术叫逃逸分析。逃逸分析主要有三种情况一种是标量替换一种是栈上分配还有一种是同步省略。这里主要讲的就是同步省略同步省略又被称为锁消除指的就是jit即时编译器发现每次调用的方法锁的都不是同一个对象锁了跟没锁一样而且效率还更慢那么就直接会将这把锁给消除。
标量替换和栈上分配可以看本人的jvm的博客https://zhenghuisheng.blog.csdn.net/article/details/129796509
for(int i 0; i 100 ; i){Student stu new Student();//发现每次调用该方法锁的根本不是同一个对象因此会将这个锁消除synchronized(stu){System.out.println(helloi stu);}
}8synchronized锁误区
详情可以查看c底层源码
1锁的不可逆指的是轻量级锁到重量级锁是不可逆的但是也存在轻量级锁到无锁或者重量级锁到无锁的状态
2不存在无锁到偏向锁的过程这两把锁相对独立但是偏向锁可以撤销成无锁
3轻量级锁中不存在cas自旋里面是属于线程交互执行一旦没拿到锁则立马升级膨胀最后拿到monitor对象之后直接升级成重量级锁
如有转载请标明出处https://zhenghuisheng.blog.csdn.net/article/details/132740980