java synchronized关键字详解

synchronized关键字,再java 程序开发中经常会使用。
当声明 synchronized代码块时,编译而成的字节码会包含Monitorenter和moinitorexit指令。这两种指定均会消耗消耗操作数栈上的一个引用类型的元素(也就是 synchronized 关键字括号里的引用),作为所要加锁解锁的锁对象。
示例:

public class App {
    public static void main(String[] args) throws Exception {}

    public void foo(Object lock) {
        synchronized (lock) {
            lock.hashCode();
        }
    }

    public synchronized void foo2(Object lock) { lock.hashCode(); }

    public static synchronized void foo3(Object lock) { lock.hashCode(); }
}

使用命令查看字节码如下:

  public void foo(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_1
         1: dup
         2: astore_2
(1)         3: monitorenter 
         4: aload_1
         5: invokevirtual #2                  // Method java/lang/Object.hashCode:()I
         8: pop
         9: aload_2
(2)        10: monitorexit
        11: goto          19
        14: astore_3
        15: aload_2
        16: monitorexit
        17: aload_3
        18: athrow
        19: return
      Exception table:
         from    to  target type
             4    11    14   any
            14    17    14   any
      LineNumberTable:
        line 5: 0
        line 6: 4
        line 7: 9
        line 8: 19
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 14
          locals = [ class App, class java/lang/Object, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public synchronized void foo2(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, (3)ACC_SYNCHRONIZED
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: invokevirtual #2                  // Method java/lang/Object.hashCode:()I
         4: pop
         5: return
      LineNumberTable:
        line 10: 0

  public static synchronized void foo3(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_STATIC, (4)ACC_SYNCHRONIZED
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #2                  // Method java/lang/Object.hashCode:()I
         4: pop
         5: return
      LineNumberTable:
        line 12: 0
}

(1)(2) :普通代码块生成的字节码,发现程序调用monitorEnter,monitorExit表示加锁与解锁。发现代码有一个monitorEnter有多个monitorExit对应,这是为了处理程序出现异常时也能释放锁
(3)(4) :这里是隐式添加的monitorEnter和monitorExit。成员函数有synchronized关键字时加锁对像为this,静态函数有synchronized关键字时加锁对象为当前class 对象。

当前线程执行到monitorEnter时,如果当前目标锁对象没有被其他线程锁住。则会获取锁,并将锁对象的计数器加一。
当线程执行到monitorExit时,则会将目标锁计数器减一

HotSpot 虚拟机实现

重量锁:实现是依赖于系统实现的,是jvm最基础的锁机制。例如linux是用pthread实现。但他是重量级锁需要调用系统内核态代码。为了避免这些昂贵的操作(线程阻塞,唤醒等)。
轻量锁:jvm会尝试自旋处理一段时间如果此时目标对象锁恰好释放了则线程不会进入阻塞状态。
jvm会自适应自选,根据以往自选登台时间,动态调整循环数目,其他线程是有该锁,此时jvm会把锁膨胀为重量级锁,并且阻塞当前线程。
偏向锁:从始至终只有一个线程请求锁的时候,使用偏向锁,当请求加锁的线程和锁对象标记字段保持的线程地址不匹配时(而且 epoch 值相等,如若不等,那么当前线程可以将该锁重偏向至自己),Java 虚拟机需要撤销该偏向锁,如果某一类锁对象的总撤销数超过了一个阈值(对应 Java 虚拟机参数 -XX:BiasedLockingBulkRebiasThreshold,默认为 20),那么 Java 虚拟机会宣布这个类的偏向锁失效。并将epoch值加1表示当前一代的偏向锁已经失效,而新的变相所则需要复制新的epoch值。
为了保证当前持有偏向锁并且已加锁的线程不至于因此丢锁,Java 虚拟机需要遍历所有线程的 Java 栈,找出该类已加锁的实例,并且将它们标记字段中的 epoch 值加 1。该操作需要所有线程处于安全点状态。
如果总撤销数超过另一个阈值(对应 Java 虚拟机参数 -XX:BiasedLockingBulkRevokeThreshold,默认值为 40),那么 Java 虚拟机会认为这个类已经不再适合偏向锁,再之后加锁过程中直接为该实例设置轻量级锁。

借鉴:https://wiki.openjdk.java.net/display/HotSpot/Synchronization

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。