文章目录

  • 一、隐式锁synchronized
  • 1.1 作用
  • 1.2 使用
  • 1.2.1 同步成员方法
  • 1.2.2 同步静态方法
  • 1.2.3 同步代码块
  • 1.3 代码示例
  • 1.4 对象锁和类锁对比
  • 二、synchronized优缺点
  • 2.1 优点
  • 2.2 缺点
  • 三、synchronized原理
  • 3.1 字节码
  • 3.1.1 同步代码块
  • 3.1.1.1 monitorenter指令
  • 3.1.1.2 monitorexit指令
  • 3.1.2 同步方法
  • 3.1.2.1 accessFlags
  • 3.1.2.2 Klass
  • 3.1.2.2 ACC_SYNCHRONIZED
  • 3.2 思考
  • 3.2.1 同步成员方法
  • 3.2.2 同步静态方法
  • 3.2.3 同步代码块
  • 四、小结
  • 五、参考


一、隐式锁synchronized

1.1 作用

  • synchronized也叫内置锁或者隐式锁,它有如下作用:
1.确保线程互斥的访问同步代码  
2.保证共享变量的修改能够及时可见

1.2 使用

1.2.1 同步成员方法

  • 对象锁。执行该段代码的线程需要同步,锁对象是对应的实例对象

1.2.2 同步静态方法

  • 类锁。执行该段代码的线程需要同步,锁对象是类的Class对象

1.2.3 同步代码块

  • 可以使用对象做锁,也可以使用类Class对象做锁。(锁是括号里面的对象)

1.3 代码示例

  • 示例代码

1.4 对象锁和类锁对比

synchronized方法

锁类型

加锁方式

备注

成员方法

对象锁

使用对象进行锁的加锁和释放,锁对象是一个类型的实例

类锁和对象锁互不干扰,因为锁的对象是不一样的

静态方法

类锁

所有的静态方法竞争的锁对象是当前类的class对象,虚拟机保证只有一个class对象

类锁和对象锁互不干扰

二、synchronized优缺点

2.1 优点

  • 简单,代码简洁

2.2 缺点

  • 不能支持中断
  • 不能尝试获取锁
  • 不能支持超时
PS:如果在使用锁时有上面三种需求,就需要考虑使用显示锁,否则可以就使用synchronized来让代码更加简洁。

三、synchronized原理

3.1 字节码

  • 我们首先写下下面一个简单的类
public class Test {

    public synchronized void test1() {
        System.out.println("test1----");
    }

    public static synchronized void test2() {
        System.out.println("test2----");
    }

    public void test3() {
        synchronized (this) {
            System.out.println("test3----");
        }
    }
}
  • 然后执行: javac Test.java编译Test类,再javap -c Test > ./1.txt反编译,将反编译信息保存到1.txt文件,文件内容如下:
Compiled from "Test.java"
public class com.intellif.mozping.synchronizedp.Test {
  public com.intellif.mozping.synchronizedp.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public synchronized void test1();
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String test1----
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public static synchronized void test2();
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #5                  // String test2----
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public void test3();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #6                  // String test3----
       9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: aload_1
      13: monitorexit
      14: goto          22
      17: astore_2
      18: aload_1
      19: monitorexit
      20: aload_2
      21: athrow
      22: return
    Exception table:
       from    to  target type
           4    14    17   any
          17    20    17   any
}
  • 从上面我们看到同步方法test1和test2在编译成字节码之后没有什么太多特殊之处,但是带有同步代码块的test3却有很大不同,主要是monitorenter和monitorexit这一对指令,同步代码块正是通过这一对指令实现的。这里我们还看不出同步方法的实现机制,因为同步方法是依赖JVM中方法修饰符上的ACC_SYNCHRONIZED 实现的。

3.1.1 同步代码块

  • monitorenter和monitorexit指令分别插入到同步代码块的开始和结束位置,JVM保证每一个monitorenter都有一个对应的monitorexit。任何对象都有一个Monitor与之相关联,当且一个Monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的Monitor所有权,即尝试获取对象的锁。我们这里对应的是使用当前对象做锁(即this关键字做锁),当一个对象A执行到这段代码时,发现了monitorenter指令,就会尝试获取本对象所对应的Monitor,获取成功即代表获取锁的成功。
3.1.1.1 monitorenter指令
  • 关于monitorenter指令的JVM规范
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
  • 线程执行Monitorenter指令时尝试获取Monitor的所有权,过程如下:
如果Monitor的进入数为 0,则该线程进入Monitor,然后将进入数设置为1,该线程即为Monitor 的所有者。
如果线程已经占有该Monitor,只是重新进入,则进入Monitor的进入数加1。
如果其他线程已经占用了Monitor,则该线程进入阻塞状态,直到Monitor的进入数为0,再重新尝试获取Monitor的所有权。
3.1.1.2 monitorexit指令
  • 关于monitorexit指令的JVM规范
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.

The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. 

Other threads that are blocking to enter the monitor are allowed to attempt to do so.
  • 线程执行Monitorexit指令条件和过程如下:
执行Monitorexit的线程必须是Objectref所对应的Monitor的持有者。指令执行时,Monitor的进入数减1,如果减1后进入数为0,那线程退出
Monitor,不再是这个Monitor的所有者。其他被这个Monitor阻塞的线程可以尝试去获取这个Monitor的所有权。
  • 由上述2个指令的执行过程,我们应该能很清楚的看出 Synchronized成员方法的实现原理。Synchronized的语义底层是通过一个Monitor的对象来完成,其实Wait/Notify 等方法也依赖于Monitor对象。因此只有在同步的块或者方法中才能调用 Wait/Notify 等方法,否则会抛出java.lang.IllegalMonitorStateException 的异常。

3.1.2 同步方法

  • synchronized方法会被翻译成普通的方法调用和返回指令,如上面的invokevirtual、areturn 指令,如上所示invokevirtual一行代表调用println方法,上面一行的String test1----就是打印的内容。我们看到在JVM字节码层面并没有任何特别的指令来实现synchronized修饰的方法,(我们看到同步成员方法和同步静态方法编译后的字节码几乎一样),
    而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置设置为1,表示该方法是同步方法,并使用调用该方法的对象或该方法所属的Class在JVM 的内部对象表示Klass作为锁对象(具体使用对象还是类对象得看是是成员方法还是静态方法了)。(摘自:《JVM内部细节之一:synchronized关键字及实现细节(轻量级锁Lightweight Locking)》)
  • 到此如果继续深入,需要了解Class文件的结构和关于部分虚拟机的内部细节了。
3.1.2.1 accessFlags
  • 关于accessFlags,参考AccessFlags文章。
3.1.2.2 Klass
  • 关于Klass,Klass简单的说是Java类在HotSpot中的c++对等体,用来描述Java类。
3.1.2.2 ACC_SYNCHRONIZED
  • 在同步方法中并未使用到monitorenter和monitorexit这一对指令,前面我们提到过,Class文件的方法表中的access_flags 字段中的 synchronized 标志位置会设置为1。JVM根据该标示符来实现方法的同步的,当方法调用时,调用指令
    将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置。
如果设置了,执行线程将先获取Monitor,获取成功之后才能执行方法体,方法执行完后再释放 Monitor。在方法执
行期间,其他任何线程都无法再获得同一个 Monitor 对象。其实本质上没有区别,只是方法的同步是一种隐式的
方式来实现,无需通过字节码来完成,起那么通过指令的方式,在调用同步代码块时会遇到指令,然后去获取Monitor
对象,这种方式会在调用同步方法的时候首先检查ACC_SYNCHRONIZED字段。

3.2 思考

  • 现在回过头来看同步方法,我们就能够理解了,这些理解对我们理解并发编程很有帮助,包括后面的显示锁,不过显示锁整个体系的实现是基于AQS,在往底层是基于CAS和volatile了,和隐式锁还是有所区别。

3.2.1 同步成员方法

  • 在同步成员方法的调用过程中,我们是通过对象去调用的。假设对象O1有成员方法f1和f2,如果O1被线程t1和t2持有,那么在t1和t2中不管是调用f1还是f2,都是需要同步的,因为判断对应的方法表中ACC_SYNCHRONIZED是1,因此需要去获取这个对象锁关联的Monitor,同一时刻只会有一个线程获得,释放之后才有可能被其他线程获取,但是如果线程t3持有对象O2,则不会有影响,因为对象不一样,对应的Monitor不一样。

3.2.2 同步静态方法

  • 同步静态方法和成员方法使用的方式是一样的,借助ACC_SYNCHRONIZED字段实现,不过静态方法使用的锁对象是类对象,因此对整个类的实例都是需要同步的。

3.2.3 同步代码块

  • 同步代码块使用什么锁取决于代码中的写法,可以对象锁也可以是类锁。不过我发现同步代码块中我使用类的Class对象和使用this做锁,编译之后没有发现区别,估计这里机制在其他的地方实现,后续研究了再补充。

四、小结

  • 除了几种锁的类型,比较难理解的是几种同步在底层的实现,同步代码块是在编译之后的字节码中包含monitorenter和monitorexit这两个指令,指令会获取到对象锁关联的Monitor对象,而同步方法是在Class文件的方法表中将access_flags字段中的synchronized设置为1表示为同步方法,虽然没有借助前面的指令但是还是通过获取Monitor对象来实现锁的语义。
  • Monitor是可以自增或者自减少来实现锁的重入的