完全理解synchronized关键字的深入用法,对象、类、字符串锁、字节码

本文从synchronized关键字的作用出发,探讨其本质和理解方式,通过key的概念阐述对象锁、类锁以及特殊情况如字符串锁的原理。内容包括synchronized修饰对象方法与代码块的等效性,类锁与对象锁的key区别,继承场景下的锁行为,以及字符串锁的陷阱和内存地址在互斥执行中的关键作用。通过本文,读者能更好地理解和运用synchronized进行并发编程。

synchronized关键字的作用

Java中的关键字synchronized的作用应该都明白,在并发编程中经常使用,是JVM级的锁实现,这里就不做过多说明。它的用法是提供一个key来使代码块互斥的被多个线程执行。笔者这里的说法可能和常规说法有点不同。因为我用了一个key的概念。笔者认为,它的底层实现就是一个key。哪个线程竞争到了key,就能开锁进入被锁住的代码块中执行,否则将无限等待下去。

如何理解synchronized的key

我们知道使用synchronized时,可以有类锁、对象锁、静态方法块锁、对象方法锁。这些情况是真的多,初看还真有点复杂。但今天我们不从这个角度来理解,我们从我这里强调用key的思路来理解。

//伪代码,
//只要在同一个JVM中,下面这段代码无论在哪里执行(同一个类中、不同类中、不同jar包)
//只要key相同(这里的相同,耐人寻味哈),那么必定互斥的执行。
//这段注释便是synchronized的本质。
synchronized(key){
   //锁住代码块
}

synchronized的本质

只要在同一个JVM中,上面这段代码无论在哪里执行(同一个类中、不同类中、不同jar包)
只要key相同(这里的相同,耐人寻味哈),那么必定互斥的执行。
这便是synchronized的本质。

如何理解key相同

内存地址相同。(这里对JMM和JVM的分布还不是特别的清楚,查了资料后再补充)

将key与Java写法相对应

key=this的情况

public synchronized void fun1(){}
 public void fun2(){
    synchronized(this){
        
    }
}

字节码

@groovyx.ast.bytecode.Bytecode
public synchronized void fun1() {
  return
}

@groovyx.ast.bytecode.Bytecode
public void fun2() {
  trycatchblock l0,l1,l2,null
  trycatchblock l2,l3,l2,null
  aload 0//this地址
  dup
  astore 1
  monitorenter
 l0
  aload 1
  monitorexit
 l1
  _goto l4
 l2
  astore 2
  aload 1
  monitorexit
 l3
  aload 2
  athrow
 l4
  return
}

这两种写法在底层时,synchronized会使用this这个内存地址作为key来判断是否互斥执行。当synchronized修饰对象方法时,与代码块时等效的。

key=Class情况

public static synchronized void fun3(){}
public void fun4(){
    synchronized (Demo1.class){
        
    }
}
// access flags 0x29
public static synchronized fun3()V
L0
 LINENUMBER 17 L0
 RETURN
 MAXSTACK = 0
 MAXLOCALS = 0

// access flags 0x1
public fun4()V
 TRYCATCHBLOCK L0 L1 L2 null
 TRYCATCHBLOCK L2 L3 L2 null
L4
 LINENUMBER 19 L4
 LDC Lsync/Demo1;.class
 DUP
 ASTORE 1
 MONITORENTER
L0
 LINENUMBER 21 L0
 ALOAD 1
 MONITOREXIT
L1
 GOTO L5
L2
FRAME FULL [sync/Demo2 java/lang/Object] [java/lang/Throwable]
 ASTORE 2
 ALOAD 1
 MONITOREXIT
L3
 ALOAD 2
 ATHROW
L5
 LINENUMBER 22 L5
FRAME CHOP 1
 RETURN
L6
 LOCALVARIABLE this Lsync/Demo2; L4 L6 0
 MAXSTACK = 2
 MAXLOCALS = 3

这两种写法在底层时,synchronized会使用该类的静态区内存地址作为key来判断是否互斥执行。
因此才有了类锁和对象锁不是互斥执行的。因为它们的key不同

key的其他情况

public void fun5(Object obj){
   synchronized (obj){
       
   }
}
@groovyx.ast.bytecode.Bytecode
public void fun5(Object a) {
  trycatchblock l0,l1,l2,null
  trycatchblock l2,l3,l2,null
  aload 1//传入的Object地址作为key
  dup
  astore 2
  monitorenter
 l0
  aload 2
  monitorexit
 l1
  _goto l4
 l2
  astore 3
  aload 2
  monitorexit
 l3
  aload 3
  athrow
 l4
  return
}

继承的一些情况

继承的情况有时会会很复杂,但是我们需要靠考虑清楚key到底是哪个就很容易判定了。
虽然Java拥有继承,但子类对象和父类对象在内存中是不同的。所以随便怎么继承代码块互斥我们仅需要考虑是哪个的this,是哪个的Class。

字符串的锁一些陷阱

有时我们会使用字符串作为锁,来实现互斥执行的功能。但是需要了解

synchronized ("aaa")                      //例1
synchronized (new String("aaa"))          //例2
synchronized (new String("aaa"))          //例3
synchronized (new String("aaa").intern()) //例4


synchronized (Integer.valueOf(1))  //例5
synchronized (new String(1))       //例6
synchronized (new String(1))       //例7

根据我们上面通过key的方式理解synchronized的互斥行为。我们可以很容易的判断出,
上面的所有例子中,被多个线程调用。有且仅有例1和例4会互斥的调用其他的情况都将视为不同的锁

  • 例1中和例2中,字符串的地址不同,因为一个是指向运行时字符串常量池地址,一个是指向对象地址。
  • intern()方法返回的是此字符串对象的字符串常量池地址,因为它们的key相同了。达到了互斥的效果
  • 同理,例2和例3都new了对象,地址不同。

再次理解key的相等

其实说了那么多,key相等的定义 等效于 Java中 ==的含义。在使用synchronized时,我们考虑代码是否互斥执行,仅需要考虑的就是synchronized修饰的key是==

总结

相信用这种方式理解,对于synchronized的用法很难出错了。

  • 关于类继承这些情况也是好理解的,仅需要要考虑key是否相等就行。
  • 关于字符串、传递对象等情况,仅需要关注内存地址就行。
  • 这是从底层出发的理解,错不了。但是我们所理解的synchronized也是可能失效的,这是因为Spring的事务,AOP技术与synchronized的范围冲突导致。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值