面试永远逃不过的synchronized(上)

简介: 面试永远逃不过的synchronized

用户态与内核态


所有的JVM底层原理逃不开操作系统,从操作系统层面看程序分为内核态和用户态


什么是用户态和内核态


  • 用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。


  • 内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。


为什么要有用户态和内核态?


由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, 划分出两个权限等级 -- 用户态和内核态。


为什么要讲用户态和内核态?


因为要理解syncronized锁升级就必须有这方面的基础,JVM更多的是定义规范实现很多种,本文主要介绍oracle实现的Hotspot版本,像IBM的J9、taobaoVM都有类似的实现。


syncronized锁升级


很多文章已经介绍过JDK早期(1.6之前不包括1.6),syncronized是重量级锁。


什么是锁


从古代的门闩、铁锁到现在的密码锁、指纹锁,锁的便携性和安全性不断提高,对私有财产保护更加高效和健全。在计算机世界里,单机线程时代里没有锁的概念。自从出现了资源竞争,我们才意识到需要对部分场景执行的现场加锁表明自己短暂的拥有。计算机开始的锁都是悲观锁,发展到现在乐观锁、偏向锁、分段锁等。锁主要提供了两种特性:互斥性和不可见性。因为有锁的存在,某些操作对外界来说是黑箱进行的,只有锁的持有者才知道对变量做了什么修改。


什么是重量级锁?


早期synchronized因为申请锁资源必须通过内核kernel系统调用,所以称为重量级锁。


为什么是重量级锁


为什么经过内核就是重量级锁了???这就要从synchronized底层说起,早期底层实现为了简便直接用了互斥锁,synchronized应该称为监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象,这个互斥锁的CPU与内存的北桥信号或总线。同时,执行互斥锁需要操作系统的调用,由于Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到内核态中,状态转换需要耗费很多的处理器时间。所以synchronized是Java语言中的一个重量级操作,重量的原因是需要操作系统大哥帮忙调度,这就会涉及系统调用和中断,下面汇编代码简单说明系统调用过程


section .text
global _start
_start:
    mov edx, len
    mov ecx, msg
    mov ebx, 1 ;文件描述符1 std_out
    mov eax, 4 ;write函数系统调用号 4
    int 0x80
    mov ebx, 0
    mov eax, 1 ;exit函数系统调用号
    int 0x80

当程序执行系统调用时首先使用类似int 80H的软中断命令保存现场,去系统调用、内核执行、然后恢复现场,每个线程都会有两个栈,一个内核态栈和一个用户态栈。当中断执行时就会有用户态转到内核态,系统调用会执行栈的切换,而且内核态对用户态是不信任的,需要做一系列额外的检查,系统调用的返回过程需要很多检查比如是否需要调度、同时还要保存上下文,这都是说明为什么是重量级锁的原因。


为什么说是同步监视器


先看如下代码,一个简单的synchronized代码块

public class SynLock {
    public void testSynBlock() {
        synchronized (this) {
            System.out.println("steven");
        }
    }
}

javac编译后可以看下字节码,如果用文本工具直接打卡都是16进制代码,除了最开始java版本基本看不懂,所以用javap反编译来看,javap可以把字节码编译成一些可读的代码形式,反编译class后如下:


image.png

从图中反编译可以看到多了monitorenter 和 monitorexit指令,JVM规范里对moniterenter 和 monitorexit的介绍:


大体意思: 每个对象都有一个监视器(Moniter)与它相关联,执行moniterenter指令的线程将获得与objectref关联的监视器的所有权,如果另一个线程已经拥有与objectref关联的监视器,则当前线程将等待直到对象被解锁为止。


大体意思: 一个monitorenter和一个或多个monitorexit指令来实现Java语言的同步代码块 monitorenter和monitorexit指令没有被用在同步方法上

现在又多出两个疑问:


  1. 为什么一个monitorenter和一个或多个monitorexit
  2. 为什么monitorenter和monitorexit指令没有被用在同步方法上


答案来了:


monitorenter过程如下:


如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1(典型的重入锁逻辑). 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权  

 

monitorexit的执行线程必须是monitor的所有者。


指令执行时,monitor的进入数减1 如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者 其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权


也就是说monitorexit对应释放锁的过程,也就必须在代码块所有可能的出口执行,从上图反编译图中看出一个monitorenter对应了两个monitorexit,此处就是因为代码块有两处结束的可能一是执行完,而是抛出异常。


具体实现Hotspot源码如下,InterpreterRuntime:: monitorenter方法:

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
  if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
         "must be NULL or an object");
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END


第二个问题:首先看下方法代码


public class SynLock {
    private synchronized void testSynMethod() {
        System.out.println("steven");
    }
}

用命令行执行编译和反编译很是繁琐,所以这次改用IDEA和Jclasslib插件来分析:

image.png

从图中可以看出方法内字节码没有monitor相关操作

image.png

但是在方法的access _Flags上可以看到加上了synchronized标签,此时把方法修改成静态方法,可以看到 flags上又多出了static标签,学过字节码应该都知道此处含义,JVM就是通过这个标签来识别方法属性,当JVM执行引擎执行某一个方法时,其会从方法区中获取该方法的access_flags,检查其是否有ACC_SYNCRHONIZED标识符,若是有该标识符,则说明当前方法是同步方法,需要先获取当前对象的monitor,再来执行方法,

image.png

果是实例方法获取的对象就是this,如果是静态方法获取的对象就是class。 常见flags如下图所示:

image.png




目录
相关文章
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
358 0
|
存储 安全 Java
面试高频:Synchronized 原理,建议收藏备用 !
本文详解Synchronized原理,包括其作用、使用方式、底层实现及锁升级机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
面试高频:Synchronized 原理,建议收藏备用 !
|
存储 安全 Java
面试题:再谈Synchronized实现原理!
面试题:再谈Synchronized实现原理!
312 0
|
安全 Java
Java面试题:解释synchronized关键字在Java内存模型中的语义
Java面试题:解释synchronized关键字在Java内存模型中的语义
263 1
|
存储 安全 Java
【多线程面试题十七】、如果不使用synchronized和Lock,如何保证线程安全?
这篇文章探讨了在不使用`synchronized`和`Lock`的情况下保证线程安全的方法,包括使用`volatile`关键字、原子变量、线程本地存储(`ThreadLocal`)以及设计不可变对象。
【多线程面试题十五】、synchronized可以修饰静态方法和静态代码块吗?
这篇文章讨论了Java中的`synchronized`关键字是否可以修饰静态方法和静态代码块,指出`synchronized`可以修饰静态方法,创建一个类全局锁,但不能修饰静态代码块。
|
Java 调度
【多线程面试题十四】、说一说synchronized的底层实现原理
这篇文章解释了Java中的`synchronized`关键字的底层实现原理,包括它在代码块和方法同步中的实现方式,以及通过`monitorenter`和`monitorexit`指令以及`ACC_SYNCHRONIZED`访问标志来控制线程同步和锁的获取与释放。
【多线程面试题十三】、说一说synchronized与Lock的区别
这篇文章讨论了Java中`synchronized`和`Lock`接口在多线程编程中的区别,包括它们在实现、使用、锁的释放、超时设置、锁状态查询以及锁的属性等方面的不同点。
|
安全 Java API
Java面试题:解释synchronized关键字在Java中的作用,并讨论其使用场景和限制。
Java面试题:解释synchronized关键字在Java中的作用,并讨论其使用场景和限制。
281 0
|
存储 安全 Java
Java面试题:请解释Java内存模型,并说明如何在多线程环境下使用synchronized关键字实现同步,阐述ConcurrentHashMap与HashMap的区别,以及它如何在并发环境中提高性能
Java面试题:请解释Java内存模型,并说明如何在多线程环境下使用synchronized关键字实现同步,阐述ConcurrentHashMap与HashMap的区别,以及它如何在并发环境中提高性能
290 0