谈谈并发编程之synchronized

嘿嘿嘿、我又双叒叕来了。上次的文章呢,是关于并发编程和JMM的。里面有一些东西呢可能和今天的synchronized有关联

并发编程和JMM: https://blog.csdn.net/a13521645939/article/details/106403363 这个有兴趣的话可以看下。今天的主要内容是synchronized、这个应该说面试的时候如果并发的话应该是一个必问的问题、和volatile一样,面试高频问题。

餐前小菜

设计同步器的意义

多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等。
共享:资源可以由多个线程同时访问
可变:资源可以在其生命周期内被修改
引出的问题:
由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问

怎么解决线程并发安全问题?

实际上,所有的并发模式在解决线程安全问题时,采用的方案都是 序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问

Java中提供了两种方式来解决线程并发安全问题synchronized Lock

同步器的本质就是加锁

加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。

锁的分类

宏观上可以把锁分为两种

如图、锁的类型可以分为显式和隐式两种、synchronized就是常见的隐式锁、我们不需要手动的加锁和解锁、JVM会自动的帮我们处理好锁。而常见的隐式锁就是Lock及其以下的一些类需要手动的lock和unlock调用方法进行处理。

java的锁从行为和细节上分的话可以分为一些这些

这是开发或者面试时日常会遇见的一些名称、可以记一下、帮助大家理解一下这些名词的意思

正餐(synchronized)

synchronized原理详解

synchronized内置锁是一种 对象锁 (锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。
加锁的方式:
  • 同步实例方法、锁是当前实例对象(this)
  • 同步静态方法、锁是当前类对象
  • 同步代码块、锁是括号里面的实例对象

synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.6之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,经过JVM不断的优化,内置锁的并发性能已经基本与Lock持平。

synchronized关键字被编译成字节码后会被翻译成 monitorenter monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。

 

第二个 monitorexit 是如果方法发生异常之后、尾部追加的这个命令可以也可以保证锁的释放。
 
另外一种当 synchronized修饰方法的时候、在其常量池中多了  ACC_SYNCHRONIZED 标示符 、起作用也是在进入方法时、尝试获取monitor对象、执行结束之后再释放对象。
 

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:

通过获取 Monitor(监视器)对象、进行加锁。在加锁成功后锁的状态是如何进行记录?
锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起认识一下对象的内存布局。
 

对象内存布局(32位机器)

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

 

  • 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等
  • 实例数据:即创建对象时,对象中成员变量,方法等
  • 对齐填充:对象的大小必须是8字节的整数倍

在synchronized加锁时、锁的状态都会被存放在对象头中、当锁等级发生改变时、对象头中对应的内存信息也会发生改变。
 

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会假设获取锁的线程并不会执行很长时间、执行一个循环,循环之后再在尝试获取锁、再次获取到锁那么就不会升级到重量级锁。

逃逸分析

使用逃逸分析,编译器可以对代码做如下优化:
一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
 
是不是所有的对象和数组都会在堆内存分配空间?
不一定、在jdk1.7之后默认是开启逃逸分析、在源代码被编译时通过逃逸分析,如果对象不会被其他的线程访问到、则对象可能不会被创建到堆内存中。
­
-XX:+DoEscapeAnalysis : 表示开启逃逸分析 ­
-XX:­-DoEscapeAnalysis : 表示关 闭逃逸分析

首先关闭逃逸分析之后、我们可以看到循环多少次就创建了多少个对象、使用jps + jmap -histo来查看

我们开启逃逸分析之后、再看明显少了很多。

上面说过逃逸分析对锁消除的影响

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值