并发编程之JVM内置(隐式)锁--synchronized

上一篇文章说到了Volatile实现可见性和有序性,但是再并发里面还有一个原子性再jvm层面是依靠synchronized去保证的。下面文章就会对该关键字进行详解。

为什么会要加锁?

在多线程编程中 可能会出现多个线程同时访问同一个共享,可变资源(临界资源)的情况 由于是不可控的 所有要采用同步机制来协调对可变对象的访问。

加锁的目的是什么?序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)。

在java领域可以将锁分为两大类型:

显式 :ReentrantLock为例,实现juc里Lock接口,实现是基于AQS实现,需要手动加锁跟解锁ReentrantLock接口里面就定义了 lock(),unlock()

隐式 : Synchronized为例加锁机制 Jvm内置锁,不需要手动加锁与解锁 Jvm会自动加锁跟解锁

下面图为总结的锁的种类:

 用法:加statis(静态方法 不需要new ) 加锁加在当前类对象上面;不加statis 加锁加在this当前实例对象上面(谁new 这个对象,锁就加在那个对象上面) 当前bean由spring容器管理 bean的作用域必须为单例加在代码同步块上面 加锁加在代码块上面。

原理:Synchronized为jvm内置锁,通过内部对象Monitor(监视器锁)实现,在代码块进入与退出实现Monitor对象方法,Monitor(监视器锁)的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低. 在java1.6之后对这个进行优化 分为无锁,偏向锁,轻量级锁,重量级锁,

synchronized什么时候释放锁:执行monotorExit释放锁

主要有两种: 1 获取锁线程执行完该代码块。

                     2 线程执行出现异常 jvm会将锁释放 缺陷是假如有3个线程对文件操作 1,读 2,3 写 我们只需要保证写安全即可 而此时对于读写都会受到synchronized的影响。

当线程遇到synchronized会发生什么事情(加在对象上面为例)?

在底层 synchronzied被翻译成一对指令 monitorEnter monitorExit 会加在代码段的开始位置和结束位置 基于8大原子操作,在进行 lock(monitorEnter)-->read-->load->user->assign(赋值)->store(存储)->write->unlock(monitorExit) 这八步原子操作必修要顺序执行,而且必须走完(而且每一步保证为原子性cmpchxg汇编指令),但是可以不是连续执行 但是注意 read和load store(存储 )->write必须同时执行。

在每一个对象在创建之初在jvm里面都会对应一个mintor监视器锁,当很多线程接近Monitor零界点时会去竞争拿到锁,而其他线程则会被放入一个WaitSet同步队列(等待加锁的线程)里面,当这个线程走完MonitorEnter 和MonitorExit 就会去唤醒WaitSet里面的其他线程去拿锁 利用Unsafe类里面的方法去加锁解锁。

JVM对synchronzied的优化历程

在jdk1.6之前的版本对加了synchronzied关键字的操作都会直接向操作系统底层直接赋予一个重量级锁,就会造成对象从用户空间到内核空间瞬间转变 转变的过程非常消耗资源。

而在jdk1.6之后将该操作改成一个锁膨胀的过程,也就是会从无锁、偏向锁、轻量级锁、重量级锁一个膨胀的过程。该过程不可逆。锁的状态都会存在对象的对象头mark word上面(下面会详细说明)

膨胀过程详解:

Synchronized的作者认为进入同步代码块的都是单线程的,没有必要一开始就申请一个比较重的一个互斥量,一开始直接偏向锁就ok。

无锁:在对象创建出来即是无锁状态。

偏向锁 : 目的就是减少获取锁得成本 (单线程访问情况) 当T1拿到锁之后 当T1再次去尝试获取锁的时候 jvm认为T1大概率还可以获得锁有一个偏向得过程,这样就省去 了大量关于锁申请得过程。

轻量锁 : 偏向锁失败得情况下 (竞争不激烈,线程访问交替执行,使用场景:假设T1,T2两个线程当T1线程先获得CPU执行权 同时T2尝试获取CPU执行权,假设这个时候T1,T2执行 时间有重复的地方(产生竞争),此时CPU认为T2不会丢失CPU使用权,CPU也不会去阻塞T2也不会立马变成重量级锁 ,此时T2会自旋等待T1将剩下的逻辑执行完,)   CPU一直认为线程的阻塞和唤醒开销比较高。

重量锁 : 线程竞争激烈的时候就会升级为重量级锁。

jvm锁自适应自旋:

根据上一次拿到锁的自旋次数适当调整 如果第一次自旋成功 第二次在自旋jvm认为这次获取锁也会成功同时会适当增加自旋次数,如果第一次自旋失败,jvm会适当降低自旋次数,如果再次失败,jvm认为下次还会失败,直接阻塞。

锁的粗化,消除:

锁粗化:
StringBuffer stb = new StringBuffer();
public void test1(){
    //jvm的优化,锁的粗化
    stb.append("1");

    stb.append("2");

    stb.append("3");

    stb.append("4");
}
由于StringBuffer线程安全  所有stb.append()方法相当于加了4次锁  
 stb.append()相当于每一次都加了一个同步块,对此jvm进行优化
 在最上面加一个全局的synchronized  相当于加了一次锁就ok       叫做锁的粗化

锁消除:  
  相当于在方法里面创建一个对象,但是这个对象是不可能被此方法外的线程应用到的,瑜伽瘦身这个对象基本上不存在同步问题
  ,如果在对这个对象加锁,其实是没有必要的,JVM就会将这个锁进行去除,类似于下面的代码
下面这个代码创建对象其实是在线程栈上面创建的,  不可能逃逸分析
    public   void(){
      Object  object=new Object());
      sysnchronized(object){                
      }                                
  }
  
  java的八大数据类型可以称为标量,  而对象可以可作为标量的聚合量。

对象头记录加锁状态:

对象里面专门有一块内存区域来记录锁的状态(对象头上面记录锁状态 无锁状态 轻量级锁 重量级锁)。

对象的内存结构:

         1、对象头(Mark word):比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等 32位和64位的机器稍有不同。

        2、对象实际数据:即创建对象时,对象中成员变量,方法等。

        3、对齐填充:对象的大小必须是8字节的整数倍 强制定义的。

对象内存结构 :对象头 实例数据 对象填充位

    对象头结构:

                   Mark Word的32个Bits空间中的

                   25Bits用于存储对象哈希码(HashCode),

                   4Bits用于 存储对象分代年龄,

                   1Bit固定为0(无锁状态),在其他状态(轻 量级锁定、重量级锁定、GC标记、可偏向)

                    2Bits用于存储锁标志 位, 0 1 0 1等标识锁

             下一篇文章会详细说明显示锁的代表基于AQS体系的ReentrantLock

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值