【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保证原子性。
相关推荐
Themberfue14 分钟前
Java 网络原理 ①-IO多路复用 || 自定义协议 || XML || JSON
xml·java·开发语言·网络·计算机网络·json
m0_6996595615 分钟前
DAY3 QT简易登陆界面优化
开发语言·qt·命令模式
wm104319 分钟前
JavaEE 3大组件 Listener Servlet Filter
java·servlet·java-ee
励志成为大佬的小杨40 分钟前
关键字初级学习
c语言·开发语言·算法
疯一样的码农1 小时前
基于Spring Boot + Vue3实现的在线商品竞拍管理系统源码+文档
java·spring boot·后端
szpc16212 小时前
100V宽压输入反激隔离电源,适用于N道沟MOSFET或GaN或5V栅极驱动器,无需光耦合
c语言·开发语言·人工智能·单片机·嵌入式硬件·生成对抗网络·fpga开发
m0_748251352 小时前
【SpringBoot】日志文件
java·spring boot·spring
m0_748234712 小时前
Java-33 深入浅出 Spring - FactoryBean 和 BeanFactory BeanPostProcessor
java·开发语言·spring
知初~2 小时前
java相关学习文档或网站整理
java·开发语言·学习
码农小灰2 小时前
什么是缓存穿透、缓存击穿、缓存雪崩,在项目中是如何解决和预防?它们分别会带来什么危害?
java·缓存