【Java】面试题 并发安全 (1)

文章目录

synchronized

在Java编程中,synchronized关键字是处理多线程并发访问共享资源的重要机制,它确保在同一时刻,只有一个线程能够访问被synchronized修饰的代码块或方法,从而保证了数据的一致性和完整性。以下是对synchronized的详细解析:

1. 基本概念与作用

  • 互斥性:synchronized保证了在同一时刻,最多只有一个线程可以获取到对象锁,进而执行被其修饰的代码块或方法。其他试图获取该锁的线程将被阻塞,直到持有锁的线程释放锁。
  • 可见性:当一个线程修改了被synchronized修饰的共享变量后,其他线程在获取锁后能够立即看到该变量的最新值。这是因为synchronized会在释放锁之前,将对变量的修改刷新到主存中,保证了变量在多个线程间的可见性。

2. 使用方式

  • 修饰代码块 :可以为一段特定的代码块添加锁,语法为synchronized (对象锁) { // 需要同步的代码块 }。例如,在上述抢票代码中,synchronized (objectLock) {... }确保了在同一时刻只有一个线程能够执行getTicket方法中的抢票逻辑,防止超卖现象。
  • 修饰方法:直接修饰整个方法,表示整个方法体都需要同步。当一个线程访问该方法时,其他线程必须等待该线程执行完方法并释放锁后才能访问。

3. 底层原理(基于monitor)

  • 字节码层面体现 :通过分析字节码信息,可以看到在使用synchronized时,会出现monitor enter(表示上锁)和monitor exit(表示解锁)指令。存在两个monitor exit是因为在底层使用了隐式的try finally结构,确保在代码执行过程中,即使发生异常,也能正确释放对象锁,避免其他线程无法获取锁而导致死锁等问题。
  • monitor的结构与属性
    • owner:关联当前持有锁的线程,同一时刻只能有一个线程与owner关联。当线程进入synchronized代码块时,会尝试将对象锁与monitor关联,并判断owner是否为none,如果是则该线程成为owner,即获得锁。
    • entry list:用于存放等待获取锁的线程。当一个线程尝试获取锁但owner不为none时,该线程会进入entry list并处于阻塞状态。当持有锁的线程释放锁后,会唤醒entry list中的线程来争抢锁的拥有权,这些线程之间是无序竞争的,并非先来后到顺序获取锁。
    • wait set :当线程调用了wait方法后,会进入wait set并释放锁,处于等待状态。直到其他线程调用notifynotifyAll方法,唤醒wait set中的线程,被唤醒的线程会重新竞争锁,竞争成功后才能继续执行。

4. 面试题回答要点

  • 阐述作用:明确说明synchronized采用互斥方式,确保同一时刻只有一个线程拥有对象锁,保证共享资源在多线程环境下的安全访问。
  • 解释底层实现:指出其底层由monitor实现,monitor是JVM级别的机制,获取锁需要使用对象锁关联monitor。
  • 描述monitor属性:详细解释monitor的三个属性owner、entry list和wait set的作用和关联关系,如owner关联持有锁的线程且只能有一个,entry list关联阻塞状态线程,wait set关联等待状态线程。

5. 注意事项与进阶理解

  • 性能影响:虽然synchronized保证了线程安全,但过度使用可能会导致性能问题,因为获取和释放锁需要一定的开销。在实际应用中,需要根据具体场景权衡是否使用synchronized以及如何优化锁的使用。
  • 可重入性:synchronized是可重入锁,即同一个线程可以多次获取同一把锁。例如,一个线程在已经获得对象锁的情况下,再次进入被该对象锁修饰的代码块或方法时,不会被阻塞。
  • 与其他并发机制的结合 :在复杂的多线程编程中,可能需要结合其他并发机制,如volatile关键字、java.util.concurrent包下的工具类等,来更高效地实现并发控制和线程安全。例如,volatile可以保证变量的可见性,但不能保证原子性,与synchronized结合使用可以在某些场景下提高性能。
  1. 面试题回答要点
    • 回答synchronized原理面试题时,先说作用是采用互斥方式,同一时刻只能让一个线程拥有对象锁;底层由monitor实现,monitor是JVM级别的,获取锁需用对象锁关联monitor,并解释monitor的三个属性,owner关联持有锁的线程且只能关联一个,entry list关联阻塞状态线程,wait set关联等待状态线程。

Java中synchronized锁相关知识总结

  1. monitor实现重量级锁的原因

    • monitor是JVM提供的C++实现的锁,线程获取锁时需关联monitor,涉及用户态和内核态切换,资源权限不同,切换成本高,且存在进程上下文切换,性能低,因此被称为重量级锁。
  2. JDK1.6引入偏向锁和轻量级锁的原因

    • 当一个线程重复获取锁且无竞争时,若一直使用重量级锁monitor,性能低,所以引入偏向锁。例如,一个线程多次进入同一个带锁方法时,偏向锁可提升性能。
    • 当多个线程交替获取锁且无竞争时,使用重量级锁monitor性能不高,因此引入轻量级锁。
  3. 对象锁与monitor的关联方式

    • Java对象在堆中,其内存结构包括对象头、实例数据和对齐填充数据。对象头的mark word记录了对象与monitor的关联信息。
    • 在32位虚拟机中,mark word不同状态下存储内容不同。无锁状态下包含哈希值、分代年龄、偏向锁标识和锁标识;偏向锁状态下记录线程id、偏向锁时间戳等;轻量级锁占用30位记录相关信息;重量级锁则指向monitor对象。对象通过在mark word中记录monitor的地址与monitor关联。
  4. 轻量级锁的工作原理

    • 适用于同步代码块无竞争或多线程交替执行的情况。线程进入同步代码块时,创建锁记录,通过CAS操作将对象头的mark word与锁记录交换,若成功,对象头存储锁记录地址,表示该线程拥有锁;若失败,可能是多线程竞争(升级为重量级锁)或锁重入(添加锁记录作为重入计数)。
    • 退出同步代码块时,若锁记录为null,表示有重入,重置锁记录(计数减一);若不为null,则再次通过CAS将mark word换回原来的值解锁。
    • CAS(Compare and Swap),即比较并交换,是一种乐观锁策略,用于实现多线程环境下的同步和并发控制,它通过原子性的比较和交换操作来保证数据的一致性。
  5. 偏向锁的执行过程

    • JDK1.6后引入,适用于长时间只有一个线程使用锁的情况。线程第一次获取锁时,通过CAS操作将线程id写入对象头的mark word并设置偏向锁标识,后续该线程再次获取锁只需判断mark word中的线程id是否为自己,无需再次CAS操作,性能较好。例如,一个线程连续多次进入多个带锁且相互调用的方法时,偏向锁可提升性能。
  6. 三种锁的对比与面试题解答

    • 对比
      • 重量级锁基于monitor实现,性能最低,涉及用户态和内核态切换及上下文切换。
      • 轻量级锁在无竞争时通过修改对象头锁标志实现,性能较好,但每次添加锁记录需CAS操作保证原子性。
      • 偏向锁适用于单线程多次获取锁的场景,性能最佳,第一次获取锁后再次获取只需简单判断。
    • 面试题解答
      • synchronized中有偏向锁、轻量级锁和重量级锁。偏向锁只有一个线程持有,轻量级锁不同线程交替持有,多线程竞争时则为重量级锁。锁发生竞争时会升级为重量级锁。

Java内存模型(JMM)

  1. 面试题引入

    • 问题:谈一谈Java内存模型(JMM)。
    • 注意事项:JMM指Java Memory Model,不要与Java内存结构混淆,答题前需确认题意,不确定可询问面试官。
  2. Java内存模型(JMM)概念

    • 定义:共享变量中多线程程序读写操作的行为规范,通过规范内存读写保证指令正确性。
    • 共享内存示例:Java代码中的成员变量、创建的对象、数组等。
  3. 内存划分

    • 工作内存
      • 分配:每个线程创建时分配一个工作内存。
      • 用途:存储线程内私有数据。
      • 访问限制:线程只能访问自己的工作内存,工作内存中的数据对每个线程私有,不存在线程安全问题。
    • 主内存
      • 性质:存储共享变量,是共享区域。
      • 共享变量内容:Java实例对象、成员变量、数组等。
      • 线程访问:每个线程都能访问主内存中的共享变量,可能存在线程安全问题。
      • 数据同步:多线程同步数据需通过主内存。
  4. 回答面试题要点

    • JMM概念:Java内存模型,定义共享内存中多线程程序读写行为规范,保证指令正确性。
    • 内存划分:分为线程私有的工作内存和所有线程可访问的主内存中的共享内存。
    • 线程交互:线程间相互隔离,交互需通过主内存共享数据。

CAS概述及重要性

  • 定义与思想:CAS全称compare and swap,即比较再交换,体现乐观锁思想,在无锁状态下保证线程操作共享数据的原子性。很多底层框架(如AQS框架、以automa开头的类等)以及之前讲过的轻量级锁和偏向锁都用到了CAS。
  • 操作流程
    • 主内存有共享变量(如int a = 100),线程操作前需将其读入各自工作内存。
    • 线程a对数据操作(如a++)后想同步到主内存,需拿自己工作内存中的旧预期值a与主内存中的当前值v比对,若相同则将更新后的值b赋给主内存中的v;若不同则失败并自旋(while循环)
    • 线程b同理,若比对失败则自旋,自旋即不断重新读取共享变量数据、操作后再次比对,直到成功或达到设定阈值。
  • 自旋锁特点
    • 优势是线程不会阻塞,效率较高。
    • 劣势是线程竞争激烈时,若多次循环都不能替换成功,效率会降低,所以有时会设置自旋次数阈值。
  • 底层实现 :依赖unsafe类,直接调用操作系统底层的CAS指令,unsafe类中的compare and swap object、int、long三个方法(均为native修饰,由系统提供,用C或C++语言实现)用于完成CAS操作。
  1. 乐观锁与悲观锁区别
    • 乐观锁(CAS):基于乐观锁思想,认为在大多数情况下,共享变量不会被其他线程修改,所以不会像悲观锁那样提前加锁,减少了锁的竞争和线程阻塞,提高了并发性能。若被修改则自旋重试,通过不断重试最终修改共享变量。
    • 悲观锁(如Synchronized):基于悲观锁思想,要防着其他线程修改共享变量,上锁后其他线程无法修改,直到解锁后其他线程才有机会操作。

volatile关键字

  1. volatile关键字介绍及问题引出

    • 关键字特性 :可修饰类的成员变量或静态成员变量,被修饰后具备保证线程间可见性禁止指令重排序两层含义。
  2. volatile保证线程间可见性分析

    • 代码示例逻辑:代码中有共享变量stop(初始值为false),线程一在100ms后将stop改为true并打印,线程二在200ms后获取stop的值并打印,线程三有一个以取反stop为条件的循环。
  3. 代码执行结果及问题分析

    • 测试过程:先执行线程一和线程二,结果显示线程一将stop改为true后,线程二能获取到新值并打印true,证明线程一和线程二之间共享变量可见。然后放开线程三再次执行,发现线程一修改了stop为true,但线程三的循环未结束,说明线程三没有读到stop的新值。
    • 原因分析:JVM中的即时编译器(JIT)对代码进行了优化,将取反stop的条件优化为固定的true,导致即使stop被修改,优化后的代码也无法读到新值。
  1. 解决方案介绍及测试
    • 第一种方案:加vm参数禁用即时编译器(JIT),添加"-Xint"参数。但此方案不推荐,因为会影响其他程序使用即时编译器。
    • 第二种方案 :在共享变量stop上加volatile修饰,告诉JIT不要对其做优化。给stop加上volatile修饰后再次测试,循环结束并打印循环次数,说明线程三能读到stop的新值。
  2. 总结与推荐
    • 一般在项目开发中,推荐使用加volatile的方式来达到线程之间对共享变量的可见性,第一种方案了解即可,实际项目开发中使用较少。
  1. 禁止指令重排序特性介绍
    • 含义:用Volatile修饰的共享变量在读写时加入屏障,防止其他读写操作越过屏障,阻止重排序。
  2. 指令重排序现象分析
  1. 多线程测试工具介绍及使用

    • 工具及注解 :引入jc stress test工具,在方法上加@actor注解保证方法内代码在同一线程执行,测试结果才正确;@outcome注解可打印输出结果。
  2. 用Volatile关键字解决指令重排序问题

    • 解决方法 :在共享变量上加Volatile关键字(value to),在y变量上加Volatile关键字后再次测试,指令重排序问题解决。
  3. 加Volatile关键字解决指令重排序及原理说明

    • 原理 :在共享变量上加入不同屏障,阻止其他读写操作越过屏障。加在y上时,写操作阻止上方其他写操作往下走,读操作阻止下方其他读操作往上走,将y与其他读写操作隔离。
    • 测试验证 :将Volatile关键字加到x上,经过六轮测试10的情况仍很多,说明加到x上不能禁止指令重排序,因为写操作加的屏障只能阻止上方指令往下走,不能阻止下方指令往上走,读操作同理。
  4. 尝试在不同变量上加关键字及分析

    • 变量都加关键字 :两个变量都加Volatile关键字能解决问题,但会影响性能,因为指令重排序对CPU是一种优化,可减轻CPU压力。
    • 使用技巧 :写变量时修饰的变量放在代码最后位置,读变量时修饰的变量放在代码最上面,这样加载Volatile较少且能解决指令重排序问题。
  5. 课程总结

    • Volatile关键字特性
      • 保证线程间可见性,阻止编译器优化,使一个线程对共享变量的修改对其他线程可见。
      • 禁止指令重排序,通过在读写共享变量时加入屏障来防止其他读写操作越过屏障,达到防止重排序的效果。

文章目录


AQS简介

  1. AQS简介与对比
    • AQS定义:AQS是抽象队列同步器(AbstractQueuedSynchronizer),是JUC中提供的一种锁机制,作为其他组件(如ReentrantLock、Semaphore、CountDownLatch等)的基础框架,由Java语言实现。
    • 对比Synchronized
      • 实现方式:Synchronized是关键字,由C++语言实现;AQS是纯粹的Java API。
      • 锁释放:Synchronized自动释放锁;AQS需要手动开启和关闭锁(因为是API)。
      • 性能特点:在锁竞争激烈时,Synchronized会自动升级为重量级锁,性能较差;AQS提供多种解决方案,性能较好。
  2. AQS基本工作机制
  • 状态管理 :内部有一个由volatile修饰的状态state,保证多个线程之间的可见性,0表示无锁,1表示有锁。线程获取锁时需修改state值,若当前state为0,线程可将其改为1并持有锁;若state为1,修改失败的线程进入队列等待。
    • 队列结构:维护一个先进先出的双向队列,内部是双向链表,有指向队列头部(最早的线程)的head属性和指向队列最后一个元素(最新的线程)的tail属性。当持有锁的线程执行完,将state改为0(无锁状态)并唤醒队列中的头元素线程去持有锁。
  1. 多线程抢资源与原子性保证
    • 多线程抢锁情况:多个线程同时抢资源时,例如线程0和线程四同时尝试修改state(初始为0),AQS使用CAS(Compare and Swap)操作来保证原子性,最终只有一个线程(如线程零)能抢到锁,将state改为1,其他线程进入队列等待,同时tail指向最新进来的线程。
  2. 公平锁与非公平锁实现
  • 非公平锁 :当线程0持有锁并释放后(state由1改为0),会唤醒队列中的头元素线程(如线程一)去抢锁,此时若有新线程(如线程五)也来抢锁,(新线程5) 与队列中的 (线程1) 共同竞争资源,队列中的其他线程还在继续等待,这就是非公平锁。
    • 公平锁 :新线程(如线程五)到来后,不能直接抢资源,而是直接到队列的最后一个元素等待。当state为0时,直接唤醒队列中的头元素线程(如线程一)去持有锁,遵循先来先得原则,类似生活中的排队,这就是公平锁。在AQS的不同实现类中,公平锁和非公平锁都可实现。
  1. 总结
    • 回答AQS相关面试题时,应提及AQS是队列同步器和锁机制,作为基础框架被其他类使用;其内部维护先进先出双向队列存储排队线程;通过state控制锁,默认0为无锁,线程通过修改state获取锁,多个线程修改state时用CAS保证原子性。
相关推荐
Scott9999HH6 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔6 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社7 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海7 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖8 小时前
JDK 26 新特性详解
java·开发语言
马优晨9 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区11 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大11 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai11 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说11 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json