首页 > 编程语言 >面试题——Java中的锁

面试题——Java中的锁

时间:2024-11-14 22:18:44浏览次数:3  
标签:面试题 Java Thread synchronized 获取 线程 内存 volatile

在这里插入图片描述
在这里插入图片描述

文章目录

谈谈你对线程安全的理解?

  • 谈到线程安全问题,就得先说一下什么是共享资源。所谓共享资源,就是说该资源被多个线程所持有或者说多个线程都可以去访问该资源。
  • 线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。

1、synchronized 关键字是怎么用的?

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

  • 主要有三种使用方法:
  1. 修饰实例方法: 给当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。

  2. 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。

  3. 修饰代码块指定加锁对象,对给定对象/类加锁。

    // 1.修饰实例方法
    synchronized void method1() {
    	//业务代码
    }
    //2.修饰静态方法
    synchronized static void method2() {
    	//业务代码
    }
    //3.修饰代码块
    synchronized(this) {
    	//业务代码
    }
    

1.1 构造方法可以使用 synchronized 关键字修饰么?

  • 不能。JAVA 语法规定构造方法不能被 synchronized 关键词修饰。
  • 前面说了,synchronized 关键字作用于方法上,是给当前对象实例/类加锁,而在构造方法上加 synchronized ,此时对象实例还没产生;另一方面,构造方法每次都是构造出新的对象,不存在多个线程同时读写同一对象中的属性的问题,所以不需要同步 。

1.2 使用 String 作为锁对象,会有什么问题?

  • 类似于 “String 对象创建了几个” 这样的问题,如果一个方法以参数中传来的 String 对象作为锁,那么就需要保证这个 String 对象在所有线程中的地址是一致的。

参考:https://blog.csdn.net/headingalong/article/details/86505420

1.3 synchronized 的底层原理有了解吗?

  • 每个 Java 对象都可以关联一个 Monitor 对象,也就是我们常说的
  • 使用 synchronized 关键字来同步代码块,在 JVM 层面使用了 monitorenter 和 monitorexit 指令来实现。
  • 在执行到 monitorenter 指令时,线程会尝试获取对象所对应的 Monitor 对象的所有权,只有获取到所有权的线程才能够执行同步代码块中的内容。而没有获取到 Monitor 所有权的线程会进入阻塞状态。

1.4 synchronized 怎么保证可重入性?可见性?抛异常怎么办?

可重入:

  • 可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为0,说明该锁没有被任何线程占用。当一个钱程获取了该锁时,计数器的值会变成1 ,这时其他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起。
  • 但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加+1,当释放锁后计数器值-1 。当计数器值为0 时,锁里面的线程标示被重置为null , 这时候被阻塞的线程会被唤醒来竞争获取该锁。

但是线程标识、计数器在锁中如何存储的?等到看完 synchronized 的膨胀过程以及MarkWord再说。

抛异常:

  • synchronized 修饰代码块之后,在字节码指令层面,会有覆盖该代码块的异常表 Exception Table,当同步代码块内抛出异常,会执行相应的字节码指令,保证锁能够正常释放。

  • 还可以再追问一下:锁重入之后,内层抛出异常,计数器减一还是直接释放掉锁?

    • 内层抛出异常,能保证内层正常释放掉锁(即计数器减一)。如果该异常在外层捕获并处理,那么并不影响外层,也就是不会导致外层也释放锁。

      public class TestSynchronizedException {
      public static void main(String[] args) {
      Task task = new Task();
      // 两个线程同时执行一个任务
      Thread thread1 = new Thread(task);
      Thread thread2 = new Thread(task);
      thread1.start();
      thread2.start();
      }

      static class Task implements Runnable{
          public synchronized void firstIn(){
              System.out.println(Thread.currentThread().getName() + "第一次进入");
              try {
                  secondIn(); // 捕获内层出现的异常
              } catch (Exception e) {
                  System.out.println(Thread.currentThread().getName() + e.getMessage());;
              }
      
      		// 等待 1s 之后继续执行外层的代码
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName() + "继续执行");
      	}
      
          public synchronized void secondIn() throws Exception {
              System.out.println(Thread.currentThread().getName() + "第二次进入");
              throw new Exception("内层异常");
          }
      
          @Override
          public void run() {
              firstIn();
          }
      }
      

      }

    • 执行结果:

      Thread-0第一次进入
      Thread-0第二次进入
      Thread-0内层异常
      Thread-0继续执行 //可以发现,内层出现异常,如果被捕获并处理,不会导致锁直接被释放
      Thread-1第一次进入
      Thread-1第二次进入
      Thread-1内层异常
      Thread-1继续执行

1.4 还使用过其他锁吗?(ReentrantLock)

  • 还使用过 ReentrantLock 可重入锁。
  • 相同点:和 synchronized 一样都是可重入锁 —— 即支持一个线程对资源的重复加锁。
  • 不同点:synchronized 是隐式地获取和释放锁的,而 ReentrantLock 需要显示地获取和释放锁,在锁获取和释放时有更多的可操作性,支持可中断地获取锁、超时获取锁、公平锁,并且可以创建多个条件变量 Condition,实现选择性通知。
  1. 可中断地获取锁:指的是处于阻塞状态等待锁的线程可以被打断等待
  2. **超时获取锁:**等待一段时间,另一线程仍然没有释放锁,那么不再等待 —— 本次获取锁失败
  3. **公平锁:**公平地锁获取,就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的,先对锁进行获取请求一定最先满足。

在这里插入图片描述

1.5 ReentrantLock 的实现原理了解吗?(公平锁、可重入、可中断是怎么实现的?)

首先讲AQS

  • 队列同步器 AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者他同步组件的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。
  • AQS 的核心思想是,成功获取同步状态的线程会被设置为当前工作线程,而获取同步状态失败的线程会被加入到同步队列的尾部。

公平锁与非公平锁

  • 公平锁和非公平锁在获取同步状态失败之后,都会进入到 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断同步队列是否有线程,如果有则不去抢锁,被加入到同步队列尾部。

可重入

  • 如果线程获取同步状态时发现 state != 0,即锁已经被占有了,则会去判断当前线程是否是占有锁的线程,如果是,则仍可以获取到 state 并对 state 进行累加操作。

1.6 加锁会带来哪些性能问题?如何解决?

  • 带来的问题主要有:死锁、饥饿、线程切换带来的资源消耗等等。

JAVA并发之加锁导致的活跃性问题

  • 解决:减小资源的消耗方面——synchronized的优化;死锁的避免;饥饿可采用公平锁。

Java中锁的优化
锁的性能影响与优化

2、volatile 有什么作用?

volatile 的第一个语义:

  • Java 中,为了解决内存可见性问题,提供了一种形式的同步,也就是使用 volatile 关键字。
  • volatile 可以确保对一个变量的更新对其他线程马上可见。
  • 当一个变量被声明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存
  • 当其他线程读取该共享变量时,会从主内存中重新获取最新值,而不是使用当前线程的工作内存中的值。
  • volatile 的内存语义和 synchronized 有相似之处 —— 当线程写入了 volatile 变量时就等价于线程退出 synchronized 同步块(把写入工作内存的变量同步到主内存),读取 volatile 变量时就相当于进入同步代码块(先清空本地内存的变量值,再从主内存获取最新值)。

volatile的第二个语义:

  • 禁止指令重排序优化。

  • 普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

  • 例如,在如上代码中,变量c 的值依赖 a 和b 的值,所以重排序后能够保证( 3 )的操作在( 2 ) ( 1 )之后, 但是( 1 ) ( 2 )谁先执行就不一定了,这在单线程下不会存在问题,因为并不影响最终结果。

    int a = 10; (1)
    int b = 20; (2)
    int c = a + b; (3)	
    
  • 再例如,常见的 DCL 单例模式(代码见文章末尾),如果不加 volatile 修饰,那么在多线程情况下,可能出现t1 线程先将引用地址赋值给了 instance 变量(1),之后才执行构造方程进行初始化(2),但是在 t1 线程执行到(1)时,t2 线程进来发现 instance 已经不为空了,直接返回了该实例,可是此时该实例还并没有初始化完毕。

2.1 原理是什么?

首先说一下 CPU 缓存的相关知识

  • CPU 读取数据的方式(顺序)为

    CPU <------>寄存器 <---->缓存<----->内存

  • 寄存器(register)是 CPU(中央处理器)的组成部分,是一种直接整合到 CPU 中的有限的高速访问速度的存储器。寄存器是一种容量有限的存储器,并且非常小。因此只把一些计算机的指令等一些计算机频繁用到的数据存储在其中,来提高计算机的运行速度。

  • 缓存 Cache :即高速缓冲存储器,是位于 CPU 与主内存间的一种容量较小但速度很高的存储器。CPU Cache 缓存的是内存数据,用于解决 CPU 处理速度和内存不匹配的问题。现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache。(下图转载于 JavaGuide)

接着再说 Java 的内存模型(JMM)

  • 从抽象的角度看,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储于主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存存储了以读/写共享变量的副本

  • 本地内存是 JMM 的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。(Java 内存模型的抽象示意图见下——《Java并发编程艺术》)
    在这里插入图片描述

    计算机硬件底层的内存结构过于复杂,JMM的意义在于避免程序员直接管理计算机底层内存,用一些关键字synchronized、volatile等可以方便的管理内存。

最后来看 volatile 的实现原理

  • 如果对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令,将这个变量所在的缓存行的数据写回到系统内存。
  • 一个处理器的缓存回写到内存会导致其他处理器的该缓存行无效,当处理器对这个数据进行操作的时候,会重新从系统内存中把数据读到处理器缓存中

或者说:更底层使用 lock 指令,在对 volatile 变量的读写时加入内存屏障

  • 对 volatile 变量的写指令会加入写屏障
  • 对 volatile 变量的读指令会加入读屏障

实现下面的作用:

  • 可见性
    • 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
    • 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据
  • 有序性
    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

2.2 和 synchronized 有什么区别?

  • volatile 关键字是线程同步的轻量级实现,不会引起线程上下文的切换和调度。
  • 但是 volatile 关键字只能用于变量。
  • volatile关键字主要用于解决变量在多个线程之间的可见性不能保证数据的原子性
  • synchronized 关键字两者都能保证

3. 写一个单例模式

public class Singleton{
	private static volatile Singleton instance;
	// 构造方法私有化
	private Singleton(){}
	
	public static Singleton getInstance(){
		if(instance == null){
			synchronized(Singleton.class){
				if(instance == null){
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

标签:面试题,Java,Thread,synchronized,获取,线程,内存,volatile
From: https://blog.csdn.net/m0_74823778/article/details/143782381

相关文章

  • 每日OJ题_牛客_计算字符串的编辑距离_DP_C++_Java
    目录牛客_计算字符串的编辑距离_DP题目解析C++代码Java代码牛客_计算字符串的编辑距离_DP计算字符串的编辑距离_牛客题霸_牛客网描述:Levenshtein 距离,又称编辑距离,指的是两个字符串之间,由一个转换成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换......
  • 基于Java+SpringBoot+Mysql在线课程学习教育系统功能设计与实现九
    一、前言介绍:免费获取:猿来入此1.1项目摘要随着信息技术的飞速发展和互联网的普及,教育领域正经历着深刻的变革。传统的面对面教学模式逐渐受到挑战,而在线课程学习教育系统作为一种新兴的教育形式,正逐渐受到广泛关注和应用。在线课程学习教育系统的出现,不仅为学生提供了更加灵......
  • 基于Java+SpringBoot+Mysql在线课程学习教育系统功能设计与实现十
    一、前言介绍:免费获取:猿来入此1.1项目摘要随着信息技术的飞速发展和互联网的普及,教育领域正经历着深刻的变革。传统的面对面教学模式逐渐受到挑战,而在线课程学习教育系统作为一种新兴的教育形式,正逐渐受到广泛关注和应用。在线课程学习教育系统的出现,不仅为学生提供了更加灵......
  • Python并行编程1并行编程简介(上)高频面试题:GIL进程线程协程
    1并行编程简介首先,我们将讨论允许在新计算机上并行执行的硬件组件,如CPU和内核,然后讨论操作系统中真正推动并行的实体:进程和线程。随后,将详细说明并行编程模型,介绍并发性、同步性和异步性等基本概念。介绍完这些一般概念后,我们将讨论全局解释器锁(GIL)及其带来的问题,从而了解Py......
  • Typescript面试题
    简述typescript简称ts,是js的一个超集,也是带有类型检测的js,拓展了js语法。优点:程序更容易理解;减少错误(编译期间排除常见错误);包容性强(兼容js)。特点:跨平台;面向对象(类、接口、枚举);类型检测。 ts的数据类型除js的类型外,还包含enum(枚举)、any(任意值)、void(表示无,常用于表示无返回值......
  • Java 数组操作:反转、扩容与缩容
    在Java中,数组是一种固定长度的数据结构,一旦创建,其大小无法更改。然而,常常在实际编程中,我们需要对数组进行扩容、缩容或其他操作。本文将介绍如何通过Java实现数组反转、扩容和缩容的操作,并在代码中演示这些常见的数组操作。1.数组反转数组反转是一个常见的操作,通常用于......
  • Java常见排序算法详解:快速排序、插入排序与冒泡排序
    在程序设计中,排序是最基本的操作之一。Java提供了多种排序算法,今天我们将介绍三种常见的排序方法:快速排序、插入排序和冒泡排序。我们不仅会分析它们的基本原理,还会提供实际的代码实现,帮助大家更好地理解并应用这些排序算法。一、快速排序(QuickSort)快速排序是一种分治法的排......
  • 30道Spring高频面试题,学完吊打面试官(实用干货!!!)
    1、什么是Spring框架?Spring框架有哪些主要模块?答:Spring框架是一个为Java应用程序的开发提供了综合、广泛的基础性支持的Java平台。Spring帮助开发者解决了开发中基础性的问题,使得开发人员可以专注于应用程序的开发。Spring框架本身亦是按照设计模式精心打造,这使得我们可......
  • java 反序列化 cc3 复现
    版本要求:jdk版本<=8u65,common-collections版本<=3.2.1在很多时候,Runtime会被黑名单禁用.在这些情况下,我们需要去构造自定义的类加载器来加载自定义的字节码.类加载机制双亲委派这里直接粘别人的了.实现一个自定义类加载器需要继承ClassLoader,同时覆盖findClass方法......
  • java 反序列化 cc4 复现
    复现环境:jdk<=8u65,commonsCollections=4.0CommonsCollections4.x版本移除了InvokerTransformer类不再继承Serializable,导致无法序列化.但是提供了TransformingComparator为CommonsCollections3.x所没有的,又带来了新的反序列化危险.cc4的执行命令部分依然沿用cc3的TemplatesI......