嘿嘿嘿、我又双叒叕来了。上次的文章呢,是关于并发编程和JMM的。里面有一些东西呢可能和今天的synchronized有关联
并发编程和JMM: https://blog.csdn.net/a13521645939/article/details/106403363 这个有兴趣的话可以看下。今天的主要内容是synchronized、这个应该说面试的时候如果并发的话应该是一个必问的问题、和volatile一样,面试高频问题。
餐前小菜
设计同步器的意义
怎么解决线程并发安全问题?
实际上,所有的并发模式在解决线程安全问题时,采用的方案都是 序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问
Java中提供了两种方式来解决线程并发安全问题synchronized 和 Lock
同步器的本质就是加锁
加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。
锁的分类
宏观上可以把锁分为两种
如图、锁的类型可以分为显式和隐式两种、synchronized就是常见的隐式锁、我们不需要手动的加锁和解锁、JVM会自动的帮我们处理好锁。而常见的隐式锁就是Lock及其以下的一些类需要手动的lock和unlock调用方法进行处理。
java的锁从行为和细节上分的话可以分为一些这些
这是开发或者面试时日常会遇见的一些名称、可以记一下、帮助大家理解一下这些名词的意思
正餐(synchronized)
synchronized原理详解
- 同步实例方法、锁是当前实例对象(this)
- 同步静态方法、锁是当前类对象
- 同步代码块、锁是括号里面的实例对象
synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.6之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,经过JVM不断的优化,内置锁的并发性能已经基本与Lock持平。
![](https://i-blog.csdnimg.cn/blog_migrate/693f74a3884f7d414e54884b77ab2cbb.png)
ACC_SYNCHRONIZED
标示符
、起作用也是在进入方法时、尝试获取monitor对象、执行结束之后再释放对象。
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
对象内存布局(32位机器)
- 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等
- 实例数据:即创建对象时,对象中成员变量,方法等
- 对齐填充:对象的大小必须是8字节的整数倍
JVM对锁的优化
锁粗化:
针对这种情况获取object对象锁之后doSomething A 然后释放锁、在获取锁执行 doSomething B、这种情况下jvm会将锁进行粗化处理。
锁消除:删除没有必要的锁
StringBuffer是一个线程安全的类、append()方法上都加了synchronized。如图上这种情况两个线程执行方法、因为StringBuffer属于每一个线程里面的变量、没有返回或影响到公共变量、这种情况下的锁可以被忽略。JVM通过代码逃逸分析来判断时候可以进行锁消除
锁的膨胀升级:
JVM对锁的有优化升级从 无锁 -->偏向锁 -->轻量级锁 --> 重量级锁 依次进行升级锁优化升级的过程是不可逆的、不存在从重量级锁向轻量级锁逆转的情况
以下以32位虚拟机为例、图中是不同锁对应的对象头中的内存结构变化
偏向锁:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
无锁 --> 偏向锁:从无锁到偏向锁的过程中、头对象记录下获取到锁的线程ID、并且更改锁标识、以方便线程下一次可以直接获取到锁。
轻量级锁:倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁
偏向锁 --> 轻量级锁:修改锁的标志位、并且指向线程栈中锁记录的指针
重量级锁:同JDK1.6之前的直接获取monitor对象、其他线程不允许获取到锁
轻量级锁 --> 重量级锁:轻量级锁失败后,jdk1.6之前虚拟机会直接进行用户态到内核态的切换、线程进入阻塞状态、等到锁被释放之后再去重新获取锁。jdk1.6之后并不会直接升级到重量级锁、而是会进行一个自旋、JVM会假设获取锁的线程并不会执行很长时间、执行一个循环,循环之后再在尝试获取锁、再次获取到锁那么就不会升级到重量级锁。
逃逸分析
首先关闭逃逸分析之后、我们可以看到循环多少次就创建了多少个对象、使用jps + jmap -histo来查看
我们开启逃逸分析之后、再看明显少了很多。
上面说过逃逸分析对锁消除的影响