服务端开发之Java备战秋招面试篇6-Java各种并发锁

努力了那么多年,回头一望,几乎全是漫长的挫折和煎熬。对于大多数人的一生来说,顺风顺水只是偶尔,挫折、不堪、焦虑和迷茫才是主旋律。我们登上并非我们所选择的舞台,演出并非我们所选择的剧本。继续加油吧!

目录

1、Java中主流锁分类体系介绍

2、乐观锁CAS原理刨析

 3、线程自旋、阻塞

4、synchronized原理分析

5、进程与线程的上下文切换


1、Java中主流锁分类体系介绍

下面看一下Java中锁的大体结构,根据存取数据是都锁住资源分为乐观锁与悲观锁。锁住资源失败,若不阻塞,可以使用自旋锁。当然还有无锁,偏向锁,轻量级锁,重量级锁,公平锁,非公平锁,可重入锁,非可重入锁,共享锁,排它锁。

乐观锁在获取数据的时候会加锁,悲观锁在获取数据时不加锁,只有在更新数据的时候判断有没有被其它线程更新。 

悲观锁会导致线程阻塞的问题,某一线程加锁使用资源,其它线程等待,也就是阻塞,等线程释放锁,cpu会唤醒阻塞的线程,线程再次去尝试获取锁。

乐观锁,本质上就是线程取数据不上锁,但是更新内存中的同步资源之前需要先判断资源是否被其它线程修改,若未修改,则获取同步资源,若已修改,则根据实现方法不同执行不同的操作。

CAS算法:是无锁算法,基于硬件原语实现的,在不使用锁的情况下实现多线程之间的变量同步,比如concurrent包中的原子类就是通过CAS算法实现了乐观锁。

2、乐观锁CAS原理刨析

如果想要并发更改主内存中的值,在更改的时候,比较线程1中得A值是否与主内存中得V值相等,若A==V,则线程1可以更改主内存中的值。假设线程2也想更改,如果A与V不相等,则会进行自旋,就是重新加载内存中的V值到线程栈中未A。一般有默认的自旋次数。线程不会阻塞,会一直占用CPU,可能导致CPU空转。

CAS可能存在的问题:ABA问题,就是变量其它线程更新无感知,通过在变量前加版本号的方式解决ABA问题,还有就是循环的开销比较大,以及只能保证一个共享变量的原子操作。

 
3、线程自旋、阻塞

自旋锁,是指锁被其它线程获取,那么当前线程不会阻塞,而是循环等待,不停地判断锁能否被成功获取,自旋直到获得锁才会退出。一直占用CPU,不会执行CPU状态的切换。

自适应自旋锁是自旋锁的改进,会根据上一次自旋的时间调整下一次自旋的时间。

4、synchronized原理分析

synchronized是JVM的内置锁,内部通过监视器锁实现,会被编译成为monitorenter和monitorexit,然后JVM判断是否加锁。

每个同步对象都有一个自己的监视器锁,需要判断对象能否拿到监视器锁,如果拿到监视器锁,才能进入同步块执行同步逻辑,否则需要进入同步队列等待。

锁升级过程:由无锁->偏向锁->轻量级锁->重量级锁。偏向锁:只有一个线程进入临界区访问同步块。轻量级锁:多线程竞争不激烈,同步块执行响应快。重量级锁:多线程竞争,同步块执行时间较长。 

对象的实例是存储在堆空间,对象的元数据存在方法区,对象的引用存储在栈空间。

对象存储包括两部分,对象头和对象的实际数据,随着锁的升级对象头中的数据是动态变化的。

5、进程与线程的上下文切换

首先看一下什么是内核态和用户态,程序运行在内核空间的状态称为内核态,运行在用户空间的状态称为用户态,用户态和内核态是操作系统的两种运行状态,划分为这两种空间状态主要是为了对应用程序的访问能力进行限制,防止应用程序随意进行一些危险的操作导致系统崩溃,比如设置时钟、内存清理,这些都需要在内核态下完成,如下:

① 内核态:内核态运行的程序可以不受限制地访问计算机的任何数据和资源,比如外围设备网卡、硬盘等。处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且所占用的 CPU 不会发生抢占情况。
② 用户态:用户态运行的程序只能受限地访问内存空间,只能直接读取用户程序的数据,不允许访问外围设备网卡、硬盘等,用户态下所占有的 CPU 会被其他程序抢占,不允许独占。

  如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成,从用户空间切换到内核空间,直到完成相关的操作后再切合用户空间,两种状态间的切换,就涉及到 CPU 的上下文切换

那么如何发生CPU上下文切换的呢?我们先看两个概念:

  • ① CPU 寄存器,是 CPU 内置的容量小、但速度极快的内存。
  • ② 程序计数器,则是用来存储 CPU 正在执行的指令位置以及即将执行的下一条指令位置。

    CPU 寄存器和程序计数器都是 CPU 在运行任何任务时必须的依赖环境,因此也被叫做 CPU 上下文。而 CPU 上下文切换,就是先把前一个任务的 CPU 上下文保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。

        回到系统调用的问题上,为了切换到内核态,需要先保存 CPU 寄存器中用户态的指令位置,然后更新 CPU 寄存器为内核态指令的新位置,最后跳转到内核态运行内核任务。而系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。

进程都是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文切换不仅包括内核堆栈、寄存器等内核空间的状态,还包括了虚拟内存、栈、全局变量等用户空间的资源。因此进程的上下文切换就比系统调用导致的上下文切换多了一个步骤,在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来,等加载了下一个进程的内核态后,还需要刷新进程的虚拟内存和用户栈。
 

线程与进程最大的区别在于,进程是资源分配的基本单位,而线程是调度的基本单位,内核中的任务调度,实际上的调度对象是线程;同一个进程中的所有线程共享进程的虚拟内存、全局变量等资源。

        在处理多线程并发任务时,处理器会给每个线程分配CPU时间片,线程在各自分配的时间片内占用处理器并执行任务,当线程的时间片用完了,或者自身原因被迫暂停运行的时候,就会有另外一个线程来占用这个处理器,这种一个线程让出处理器使用权,另外一个线程获取处理器使用权的过程就叫做上下文切换。

         一个线程让出CPU处理器使用权,就是“切出”;另外一个线程获取CPU处理器使用权,就是“切入”,在这个切入切出的过程中,操作系统会保存和恢复相关的进度信息,而这个进度信息就是我们常说的“上下文”,也就是我们上文提到的 CPU寄存器以及程序计数器。

        这么一来,线程的上下文切换就可以分为两种情况:

① 前后两个线程属于同一个进程。此时,因为共享虚拟内存,所以在切换时,虚拟内存、全局变量这些资源就保持不动,只需要切换线程的私有数据,比如栈和寄存器等不共享的数据。
② 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程的上下文切换一样,不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态的修改
 

也可以参考这篇博客看看:java中的各种锁总结(简单全面版)_Mr.米斯特儿赵的博客-CSDN博客

多线程常见知识点总结(简单版)_Mr.米斯特儿赵的博客-CSDN博客

死锁的四个条件:互斥、不可剥夺、请求和保持、循环等待。

猜你喜欢

转载自blog.csdn.net/nuist_NJUPT/article/details/129231569