实现线程安全的方式之互斥同步锁(synchronized)

本文详细解析了Java中的synchronized锁机制,包括对象锁、类锁、同步代码块的实现方式,以及轻量级锁、偏向锁、重量级锁的升级过程。同时介绍了自旋优化、锁消除和批量重偏向等并发优化策略,帮助读者深入理解Java并发编程中的锁概念和技术细节。
摘要由CSDN通过智能技术生成

synchronized实际上用对象锁保证了临界区内代码的原子性(也就是说是不可分割的,一起成功或一起失败),并且synchronized锁住的需要的是同一个对象,如果有多个对象,不同的线程访问的是不同的对象,不存在竞争关系,无共享资源,那么也就不存在线程安全的问题

简单来说就是,无共享,就无竞争。

synchronized锁的实现

synchronized可以修饰成员方法、静态方法,代码块。

synchronized加在成员方法上,就相当于是锁住的是this对象。也可以称为对象锁。等价成同步代码块的形式就是以下形式。

synchronized(this){
   /....
   临界区
  ....../
}

synchronized加在静态方法上,就相当于是锁住了类对象。也可以称为类锁。等价成同步代码块的形式就是以下形式。

synchronized(demo.class){
  /.....
  临界区
  ...../
}

synchronized加在代码块上,就是给指定的对象进行加锁

8锁问题结论总结

  • 一个对象中有多个synchronized修饰的方法(都是成员方法),有多个线程去访问,只要有一个线程调用了其中一个synchronized修饰的方法,那么其他线程是不能进入其他synchronized修饰的方法,只能等待。换句话来说就是,因为锁住的是对象this,因此在某一时刻中,此对象只能有唯一的一个线程去访问这些synchronized方法。
  • 加个普通方法后发现与同步锁无关了。普通方法和synchronized修饰的方法多线程访问是不影响的。
  • 一个对象中有静态同步方法和普通同步方法,那么两个线程去分别调用静态同步方法和普通同步方法异步的,因为静态同步方法锁住的是class对象,普通同步方法锁住的是this对象。
  • 一个类中有多个静态同步方法,那么创建了两个对象,两个线程分别调用对象中的不同的静态同步方法,会按照顺序执行,一个静态同步方法获得锁后,其他的静态同步方法需要等待。因为尽管是创建了两个对象,但还是同一个类的模板。

Monitor

以32位的虚拟机为例

Java的对象头

普通对象的对象头包括了存储对象的运行时数据类型指针

数组对象的对象头包括了存储对象的运行时数据类型指针记录数组长度数据

Mark Word是用来存储对象自身的运行时数据,其中包括了分代年龄GC哈希码锁状态标识线程持有的锁偏向线程ID偏向时间戳

 synchronized在给对象加锁的时候,对象与Monitor相关联,对象是Java中提供的,Monitor并不是Java中提供的,他是由操作系统所提供的,其相关联的实现是对象中的MarkWord指向Monitor,原本存放哈希码改为存放Monitor的指针地址,Owner用来记录当前线程EntryList用来存放阻塞的排队线程。

 当然,当前线程内容执行完成,释放锁时,持有锁的对象会将对象头中的MarkWord重置(原本是hashcode),还原为hashcodes,并且唤醒EntryList中阻塞的线程

那么synchronized加锁其实也分为了三种锁,轻量级锁、偏向锁和重量级锁(Monitor)。

轻量级锁

轻量级锁用到的语法依然是synchronized。所用到的场景是一个对象虽然有对线程访问,但是多线程访问的时间是错开的,无竞争,所以用轻量级锁优化。

轻量级锁上锁以及解锁的一系列流程如下:

Klass word是类型指针,Object body存放的是成员变量信息

  • 创建锁记录对象(Lock Record),每个线程的栈帧都会包含一个锁记录的结构Lock Record,内部可以存储锁定对象的MarkWord

  •  让锁记录中object reference指向锁对象(存放对象的引用地址),并尝试用CAS替换object的MarkWord(哈希码的位置),将MarkWord的值存入锁记录00代表的轻量级锁的锁状态,01代表的是无锁状态。

              a)若CAS替换成功,对象头中就存储了锁记录地址和锁状态00,表示由该线程给对象加锁。

              b)若CAS替换失败,大概会是由两种原因造成:其他线程已经持有了此轻量级锁,存在竞争,进入锁膨胀;自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入计数。

 如果发生了锁重入状态如下,创建了新的锁记录对象放入线程中,但是CAS替换失败,因为Object中MarkWord的预期值与当前值不符。

  •  解锁过程,当退出synchronized代码块,如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减1;若不为null,用CAS将MarkWord的值恢复给对象头。成功就代表着解锁成功,失败就代表轻量级锁进行了锁膨胀或者已经升级为了重量级锁,进入重量级锁解锁过程。

锁膨胀(轻量级锁->重量级锁)

在尝试轻量级锁的过程中,CAS操作无法成功,其他线程为此对象加上了轻量级锁,此时存在竞争,需要进行锁膨胀,也就是当前占有锁的线程相关联的对象申请Monitor锁,由轻量级锁升级为重量级锁。

其过程如下:

  • Thread-0占有了轻量级锁,来了个Thread-1也尝试获取轻量级锁失败,进入了锁膨胀流程。
  • Thread-0关联的object对象申请Monitor锁,让object指向重量级锁地址。Thread-1进入Monitor的EntryList中进行排队等待,进入阻塞状态。
  • 当Thread-0中的内容全部执行完成,准备退出同步代码块解锁时,使用CAS将Mark Word的值恢复给对象头,如果失败(因为如果变为了重量级锁,锁状态已经改变),就进入重量级锁的解锁过程。
  • 重量级锁的解锁过程,根据object中的Monitor地址找到Monitor对象,将Monitor中的Owner设置为null(原本记录的是当前线程),唤醒EntryList中阻塞的线程。

自旋优化

在多核CPU的情况下,可以通过自旋来避免重量级锁阻塞线程再到唤醒线程,上下文切换的消耗,提高了并发的性能。

具体是Thread-0在cpu1上执行并获得了锁,在执行同步代码块的期间,Thread-1在cpu2上执行,访问同步代码块,想要获取Monitor,但是发现Monitor已经被占用了,就自旋重试,如果Thread-0的同步代码块并不多,执行的时间较短,那么自旋成功,如果同步代码块过多,执行的时间过长,自旋可能就失败了,那么Thread-1就进入阻塞队列

Java6之后的自旋锁是自适应的,如果对象在之前自旋操作成功过,说明再次自旋成功的可能性会提高,再次占用自旋锁时就会多自旋几次,但如果未成功过,可能就会少自旋几次或者不自旋。在Java7之后使用synchronized就不能再控制是否开启自旋了。

偏向锁

轻量级锁再没有竞争时,每次重入仍然需要执行CAS操作。Java6中偏向锁的优化是只用第一次使用CAS将线程ID设置到对象头的Mark Word中,之后再访问时,发现对象头中的线程ID是自己的线程ID就知道无竞争,不用再重复CAS(提高了性能),只要后续无竞争,这个对象都归该线程

  • 如果不设置关闭偏向锁,那么偏向锁是默认开启的,那么对象创建后,Mark Word值为最后三位为101(偏向状态),这时它的thread、epoch、age都为0
  • 偏向锁是默认延迟的,不会在程序启动时立即生效,如果想要避免延迟,可以修改VM参数 -XX:BiasedLockingStartupDelay=0来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,Mark Word值为最后3为为001(无锁状态),这时它的hashcode、age都为0,这一次用到hashcode时才会赋值

撤销锁

1.禁用偏向锁

在加锁的过程中,如果禁用了偏向锁,就会有轻量级锁,在有竞争的情况下,就会升级为重量级锁。

2.hashcode

对象在调用hashcode方法时会禁用偏向锁,撤销对象的偏向状态。原因是调用hashcode方法需要调用对象头MarkWord中的哈希码,但是线程ID也是存放在Mark Word中。

3.其他线程使用当前对象

Thread-0对A对象加锁并执行同步代码块中的内容,执行完成后再唤醒Thread-1,Thread-1仍对A对象加锁,这样就会从偏向锁升级为轻量级锁。

4.调用wait\notify

批量重偏向

无竞争的情况下,偏向了线程1的对象仍有机会重新偏向线程2,重偏向会重置对象的ThreadID。撤销偏向锁阈值超过20次,JVM会给这些对象加锁时重偏向来访问的线程

20次前        撤销偏向锁--->升级为轻量级锁--->变为无锁(在解锁之后)

20次后        全部偏向为线程2,不用再撤销偏向锁,直接重偏向访问的线程。

批量撤销

当撤销偏向锁超过40次后,JVM认为自己偏向错了,将整个类的所有对象都变为不可偏向的,新创建的对象也是这样的。不可偏向也就是无锁状态(001),也就是说明这个类竞争激烈,不适合偏向锁。

锁消除

加锁势必会带来性能的损耗,但是有些加锁实属没有必要,在Java运行时开启JIT即时编译器,它对于字节码进行进一步的优化,比如对于局部变量的线程安全的对象上锁,JIT就会消除锁的作用,使性能得到提高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值