java 锁 访问普通方法_Java Synchronized 锁的实现原理详解及偏向锁-轻量锁-重量锁...

Synchronize是重量级锁吗?是互斥锁吗?

它的实现原理?

前言

线程安全是并发编程中的重要关注点,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多个线程共同操作共享数据。因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。

在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块。

synchronized底层语义原理

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的

要深入理解synchronized实现原理,就需要先来了解在JVM中Java对象的结构,如图所示:在堆中的对象分为三块区域:对象头、实例数据和对齐填充。

实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

对象头:如下表格

虚拟机位数头对象结构说明

32/64bit

Mark Word

存储对象的hashCode、锁信息或分代年龄或GC标志等信息

32/64bit

Class Metadata Address

类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

20200322195858605316.png

synchronized实现的三种方式

1、同步普通方法

public class SyncMethod {

public int i;

public synchronized void syncTask(){

i++;

}

}

java -P

Classfile src/main/java/com/zejian/concurrencys/SyncMethod.classLast modified2017-6-2; size 308bytes

MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94

Compiled from"SyncMethod.java"

public classcom.zejian.concurrencys.SyncMethod

minor version:0major version:52flags: ACC_PUBLIC, ACC_SUPER

Constant pool;//==================syncTask方法======================

public synchronized voidsyncTask();

descriptor: ()V//方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法

flags: ACC_PUBLIC, ACC_SYNCHRONIZED

Code:

stack=3, locals=1, args_size=1

0: aload_01: dup2: getfield #2 //Field i:I

5: iconst_16: iadd7: putfield #2 //Field i:I

10: returnLineNumberTable:

line12: 0line13: 10}

SourceFile:"SyncMethod.java"

从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,接下来我们将简单了解一下Java官方在JVM层面对synchronized锁的优化。

JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放

2、同步方法块

public voidsync1() {

synchronized(this) {

// do somethings 锁的是当前实例对象

}

}

public voidsync2() {

synchronized(MyTest.css) {

// do somethings 锁的的是当前类class对象

}

}

//反编译class文件后,可以看到在sysnchronized

1:flags: ACC_PUBLIC

.............

........

3: monitorenter //进入同步方法 //

4:..........省略其他

15: monitorexit //退出同步方法

16: goto

24 //省略其他.......

21: monitorexit //退出同步方法

从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置.

3、同步静态方法

1 public static synchronized voidsync2() {

2 // do somethings //锁的是当前类class

3 }

总结:JVM基于进入和退出Monitor对象来实现方法同步和代码块同步, 但是两者的实现细节不一样.

代码块同步: 通过使用monitorenter和monitorexit指令实现的.

同步方法: ACC_SYNCHRONIZED修饰

锁的竞争

随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级

JVM 默认几秒后开启偏向锁

如果你确定应用程序中所有的锁通常是在竞争状态,你可以通过JVM参数关闭偏向锁UseBiasedLocking = false,那么程序会默认进入轻量锁状态。

1、偏向锁(A线程独占锁,不用上下文切换。对象头标识)

在实际场景中,如果一个同步方法,没有多线程竞争,并且总是由同一个线程多次获取锁,如果每次还有阻塞线程,唤醒cpu从用户态转核心态,那么对于cpu是一种资源的浪费,为了解决这类问题,旧引入了偏向锁的概念。

偏向锁的核心思想是,如果不存在竞争的线程一个线程获得了锁,那么锁就进入偏向模式。此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果。

当出现多个线程竞争锁之后偏向锁失败后,会升级为轻量级锁。

2、轻量锁(A线程拥有锁,B获取,竞争,自旋(jdk1.7以后智能自转))

如果说偏向锁是为了解决同步代码在单线程下访问性能问题,那么轻量锁是为了解决减少无实际竞争情况下,使用重量级锁产生的性能消耗

轻量锁,顾名思义,轻量是相对于重量的问题,使用轻量锁时,不需要申请互斥量(mutex),而是将mark word中的信息复制到当前线程的栈中,然后通过cas尝试修改mark word并替换成轻量锁,如果替换成功则执行同步代码。如果此时有线程2来竞争,并且他也尝试cas修改mark word但是失败了,那么线程2会进入自旋状态,如果在自旋状态也没有修改成功,那么轻量锁将膨胀成状态,mark word会被修改成重量锁标记(10) ,线程进入阻塞状态。当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。

3、自旋锁 (A线程拥有锁,B线程自旋尝试获取)

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,会进行自旋锁的优化。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(默认10次),在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。最后没办法也就只能升级为重量级锁了。

4、重量锁 (B线程自旋获取不到锁,膨胀重量锁,阻塞A线程。直到B执行完)

在jvm规范中,synchronized是基于监视器锁(monitor)来实现的。如前文所提到的,它会在同步代码之前添加一个monitorenter指令,获取到该对象的monitor,同时它会在同步代码结束处和异常处添加一个monitorexit指令去释放该对象的monitor,需要注意的是每一个对象都有一个monitor与之配对,当一个monitor被获取之后 也就是被monitorenter,它会处于一个锁定状态,其他尝试获取该对象的monitor的线程会获取失败,只有当获取该对象的monitor的线程执行了monitorexit指令后,其他线程才有可能获取该对象的monitor成功。

所以从上面描述可以得出,监视器锁就是monitor它是互斥的(mutex)。由于它是互斥的,那么它的操作成本就非常的高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值