暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

面试官:说说你对 synchronized 的理解!

JavaFish 2021-08-16
561

作者 | JasonGaoH

来源 | juejin.im/post/5d439934e51d4561b416d3f2

synchronized 关键字

什么是 synchronized

JDK 官网对 synchronized 关键字有个比较权威的解释。
Synchronized keyword enable a simple strategy for preventing thread interference and memory consistency errors: if an object is visible to more than one thread, all reads or writes to that object's variables ard done through synchronized methods.
上述解释的意思是:synchronized 关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来进行,具体表现如下:
  • synchronized 关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致的问题出现
  • synchronized 关键字包括 monitor enter 和 monitor exit 两个 JVM 指令,它能够保证在任何时候任何线程执行到 monitor enter 成功之前都必须从主内存中获取数据,而不是缓存中,在 monitor exit 运行成功之后,共享变量被更新后的值必须刷入主内存
  • synchronized 的执行严格遵守 java happens-before 规则,一个 monitor exit 指令之前必定要有一个 monitor enter

synchronized 关键字的用法

synchronized 可以用于对代码块或方法进行修饰,而不能够用于对 class 以及变量进行修饰。

同步方法

public synchronized void sync() {
    //...
}

同步方法块

private final Object lock = new Object();
public void sync() {
    synchronized(lock) {
        //...
    }
}

深入分析 synchronized 关键字

线程堆栈分析

synchronized 关键字提供了一种互斥机制,也就是说在同一时刻,只能有一个线程访问同步资源。看下面这段程序:
import java.util.concurrent.TimeUnit;

public class TestSync {

    private final static Object lock = new Object();

    public void accessResource() {
        synchronized(lock) {
            try {
                TimeUnit.MINUTES.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        final TestSync sync = new TestSync();
        for(int i =0;i<5;i++) {
            new Thread(){
                @Override
                public void run() {
                    sync.accessResource();
                }   
            }.start();
        }
    }
}

上面的代码定义一个方法 accessResource,并且使用 synchronized 来对代码进行同步,同时定义了 5 个线程调用 accessResource 方法,由于 synchronized 的互斥性,只能有一个线程获得 lock 的 monitor 锁,其他线程只能进入阻塞状态,等待获取 lock 的 monitor 锁。
针对这个 monitor 锁我们如何从线程堆栈信息来看呢?
其实,jstack 命令在 Java 中可以用来打印进程的线程堆栈信息。我们来运行这个 Java 程序,在终端通过 top 命令查看运行起来的 Java 程序的进程 id,然后执行 jstack ‘pid’。我们来看下打印出来的信息:
日志
通过截图可以看到 Thread-0 持有 monitor<0x00000007955f2130> 的锁并且处于休眠状态中,而其他几个线程则是处于 BLOCKED 状态中,它们是在等待着获取 monitor<0x00000007955f2130 > 的锁。

JVM 指令分析

从 JVM 指令角度再来分析 synchronized 关键字。
我们可以使用 javap 这个命令来对上面这个 TestSync 类生成的 class 字节码进行反编译,得到下面的 JVM 指令。
Compiled from "TestSync.java"
public class main.TestSync {
  static {};
    Code:
       0new           #3                  // class java/lang/Object
       3: dup
       4: invokespecial #10                 // Method java/lang/Object."<init>":()V
       7: putstatic     #13                 // Field lock:Ljava/lang/Object;
      10return

  public main.TestSync();
    Code:
       0: aload_0
       1: invokespecial #10                 // Method java/lang/Object."<init>":()V
       4return

  public void accessResource();
    Code:
       0: getstatic     #13                 // Field lock:Ljava/lang/Object;
       3: dup
       4: astore_1
       5: monitorenter
       6: getstatic     #20                 // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
       9: ldc2_w        #26                 // long 10l
      12: invokevirtual #28                 // Method java/util/concurrent/TimeUnit.sleep:(J)V
      15: goto          23
      18: astore_2
      19: aload_2
      20: invokevirtual #32                 // Method java/lang/InterruptedException.printStackTrace:()V
      23: aload_1
      24: monitorexit
      25: goto          31
      28: aload_1
      29: monitorexit
      30: athrow
      31return
    Exception table:
       from    to  target type
           6    15    18   Class java/lang/InterruptedException
           6    25    28   any
          28    30    28   any

  public static void main(java.lang.String[])
;
    Code:
       0new           #1                  // class main/TestSync
       3: dup
       4: invokespecial #44                 // Method "<init>":()V
       7: astore_1
       8: iconst_0
       9: istore_2
      10: goto          27
      13new           #45                 // class main/TestSync$1
      16: dup
      17: aload_1
      18: invokespecial #47                 // Method main/TestSync$1."<init>":(Lmain/TestSync;)V
      21: invokevirtual #50                 // Method main/TestSync$1.start:()V
      24: iinc          21
      27: iload_2
      28: iconst_5
      29: if_icmplt     13
      32return
}

从上面的指令中可以看到,在 accessResource () 方法中,先后出现了一个 monitor enter 和两个 monitor exit。
我们主要选取 accessResource () 这部分代码块来重点分析。
public void accessResource();
    Code:
       0: getstatic     #13                 //①获取lock
       3: dup
       4: astore_1
       5: monitorenter                      //②执行monitorenter JVM指令
       6: getstatic     #20                 // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
       9: ldc2_w        #26                 // long 10l
      12: invokevirtual #28                 // Method java/util/concurrent/TimeUnit.sleep:(J)V
      15: goto          23                  //③跳转到23行
      18: astore_2
      19: aload_2
      20: invokevirtual #32                 // Method java/lang/InterruptedException.printStackTrace:()V
      23: aload_1                           //④
      24: monitorexit                       //⑤ 执行monitor exit JVM指令
      25: goto          31
      28: aload_1
      29: monitorexit
      30: athrow
      31return

首先①获取到 lock 引用,然后执行②monitorenter JVM 指令,休眠结束后 goto 至③monitorexit 的位置 (astore_n 表示存储引用到本地变量表;aload_n 表示从本地变量表加载应用;getstatic 表示从 class 中获取静态属性)

monitorenter

每一个对象都与一个 monitor 相关联,一个 monitor 的 lock 的锁只能被一个线程在同一时间获得,在一个线程尝试获得与对象关联的 monitor 的所有权时会发生如下的几件事情。
如果 monitor 的计数器为 0,则意味着该 monitor 的 lock 还没有被获得,某个线程获得之后将立即对该计数器加 1,从此该线程就是这个 monitor 的所有者了。如果一个已经拥有该线程所有权的线程重入,则会导致 monitor 的计数器再次累加。如果 monitor 已经被其他线程所拥有,则其他线程尝试获取该 monitor 所有权时,会被陷入阻塞状态直到 monitor 变为 0,才能再次尝试获取对 monitor 的所有权。

monitorexit

释放对 monitor 的所有权,想要释放某个对象关联的 monitor 所有权的前提是,你曾经拥有了所有权。释放 monitor 所有权的过程比较简单,就是将 monitor 的计数器减一,如果计数器的结果为 0,则意味着该线程不在拥有对该 monitor 的所有权,通俗地讲就是解锁。

synchronized 的锁优化

在虚拟机规范对 monitorenter 和 monitorexit 的行为描述中,有两点是需要特别注意的,首先,synchronized 同步块对于同一条线程是可重入的,不会出现自己锁死自己的问题。其次,同步课在已进入的线程执行完以前,会阻塞后面其他线程的进入。
Java 的线程是映射到操作系统线程上的,吐过要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态切到核心态,因此状态转换需要耗费很多的处理器时间,对于简单的同步块(如被 synchronized 修饰的 getter 或 setter 方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。所以 synchronized 是 Java 语言中的一个重量级的操作。
其实大多数时候,共享数据的锁定状态一般只会持续很短的一段时间,为了这段时间去挂起和恢复线程其实并不值得。
如果物理机上有多个处理器,可以让多个线程同时执行的话。我们就可以让后面来的线程 “稍微等一下”,但是并不放弃处理器的执行时间,看看持有锁的线程会不会很快释放锁。这个 “稍微等一下” 的过程就是自旋。
自旋锁在 JDK 1.4 中已经引入,在 JDK 1.6 中默认开启。只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快,因为上面刚说到,线程的状态切换会耗费很多 CPU 时间。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用 CPU 时间。如果线程竞争不激烈,并且保持锁的时间段,适合使用自旋锁。

文章转载自JavaFish,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论