文章目录
- synchronized
-
-
- [1. 基本概念与作用](#1. 基本概念与作用)
- [2. 使用方式](#2. 使用方式)
- [3. 底层原理(基于monitor)](#3. 底层原理(基于monitor))
- [4. 面试题回答要点](#4. 面试题回答要点)
- [5. 注意事项与进阶理解](#5. 注意事项与进阶理解)
-
- Java中synchronized锁相关知识总结
- Java内存模型(JMM)
- CAS概述及重要性
- volatile关键字
- AQS简介
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并释放锁,处于等待状态。直到其他线程调用notify
或notifyAll
方法,唤醒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结合使用可以在某些场景下提高性能。
- 面试题回答要点
- 回答
synchronized
原理面试题时,先说作用是采用互斥方式,同一时刻只能让一个线程拥有对象锁;底层由monitor
实现,monitor
是JVM级别的,获取锁需用对象锁关联monitor
,并解释monitor
的三个属性,owner
关联持有锁的线程且只能关联一个,entry list
关联阻塞状态线程,wait set
关联等待状态线程。
- 回答
Java中synchronized锁相关知识总结
-
monitor实现重量级锁的原因
- monitor是JVM提供的C++实现的锁,线程获取锁时需关联monitor,涉及用户态和内核态切换,资源权限不同,切换成本高,且存在进程上下文切换,性能低,因此被称为重量级锁。
-
JDK1.6引入偏向锁和轻量级锁的原因
- 当一个线程重复获取锁且无竞争时,若一直使用重量级锁monitor,性能低,所以引入偏向锁。例如,一个线程多次进入同一个带锁方法时,偏向锁可提升性能。
- 当多个线程交替获取锁且无竞争时,使用重量级锁monitor性能不高,因此引入轻量级锁。
-
对象锁与monitor的关联方式
- Java对象在堆中,其内存结构包括对象头、实例数据和对齐填充数据。对象头的mark word记录了对象与monitor的关联信息。
- 在32位虚拟机中,mark word不同状态下存储内容不同。无锁状态下包含哈希值、分代年龄、偏向锁标识和锁标识;偏向锁状态下记录线程id、偏向锁时间戳等;轻量级锁占用30位记录相关信息;重量级锁则指向monitor对象。对象通过在mark word中记录monitor的地址与monitor关联。
-
轻量级锁的工作原理
- 适用于同步代码块无竞争或多线程交替执行的情况。线程进入同步代码块时,创建锁记录,通过CAS操作将对象头的mark word与锁记录交换,若成功,对象头存储锁记录地址,表示该线程拥有锁;若失败,可能是多线程竞争(升级为重量级锁)或锁重入(添加锁记录作为重入计数)。
- 退出同步代码块时,若锁记录为null,表示有重入,重置锁记录(计数减一);若不为null,则再次通过CAS将mark word换回原来的值解锁。
- CAS(Compare and Swap),即比较并交换,是一种乐观锁策略,用于实现多线程环境下的同步和并发控制,它通过原子性的比较和交换操作来保证数据的一致性。
-
偏向锁的执行过程
- JDK1.6后引入,适用于长时间只有一个线程使用锁的情况。线程第一次获取锁时,通过CAS操作将线程id写入对象头的mark word并设置偏向锁标识,后续该线程再次获取锁只需判断mark word中的线程id是否为自己,无需再次CAS操作,性能较好。例如,一个线程连续多次进入多个带锁且相互调用的方法时,偏向锁可提升性能。
-
三种锁的对比与面试题解答
- 对比
- 重量级锁基于monitor实现,性能最低,涉及用户态和内核态切换及上下文切换。
- 轻量级锁在无竞争时通过修改对象头锁标志实现,性能较好,但每次添加锁记录需CAS操作保证原子性。
- 偏向锁适用于单线程多次获取锁的场景,性能最佳,第一次获取锁后再次获取只需简单判断。
- 面试题解答
- synchronized中有偏向锁、轻量级锁和重量级锁。偏向锁只有一个线程持有,轻量级锁不同线程交替持有,多线程竞争时则为重量级锁。锁发生竞争时会升级为重量级锁。
- 对比
Java内存模型(JMM)
-
面试题引入
- 问题:谈一谈Java内存模型(JMM)。
- 注意事项:JMM指Java Memory Model,不要与Java内存结构混淆,答题前需确认题意,不确定可询问面试官。
-
Java内存模型(JMM)概念
- 定义:共享变量中多线程程序读写操作的行为规范,通过规范内存读写保证指令正确性。
- 共享内存示例:Java代码中的成员变量、创建的对象、数组等。
-
内存划分
- 工作内存
- 分配:每个线程创建时分配一个工作内存。
- 用途:存储线程内私有数据。
- 访问限制:线程只能访问自己的工作内存,工作内存中的数据对每个线程私有,不存在线程安全问题。
- 主内存
- 性质:存储共享变量,是共享区域。
- 共享变量内容:Java实例对象、成员变量、数组等。
- 线程访问:每个线程都能访问主内存中的共享变量,可能存在线程安全问题。
- 数据同步:多线程同步数据需通过主内存。
- 工作内存
-
回答面试题要点
- 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操作。
- 乐观锁与悲观锁区别
- 乐观锁(CAS):基于乐观锁思想,认为在大多数情况下,共享变量不会被其他线程修改,所以不会像悲观锁那样提前加锁,减少了锁的竞争和线程阻塞,提高了并发性能。若被修改则自旋重试,通过不断重试最终修改共享变量。
- 悲观锁(如Synchronized):基于悲观锁思想,要防着其他线程修改共享变量,上锁后其他线程无法修改,直到解锁后其他线程才有机会操作。
volatile关键字
-
volatile关键字介绍及问题引出
- 关键字特性 :可修饰类的成员变量或静态成员变量,被修饰后具备保证线程间可见性 和禁止指令重排序两层含义。
-
volatile保证线程间可见性分析
- 代码示例逻辑:代码中有共享变量stop(初始值为false),线程一在100ms后将stop改为true并打印,线程二在200ms后获取stop的值并打印,线程三有一个以取反stop为条件的循环。
-
代码执行结果及问题分析
- 测试过程:先执行线程一和线程二,结果显示线程一将stop改为true后,线程二能获取到新值并打印true,证明线程一和线程二之间共享变量可见。然后放开线程三再次执行,发现线程一修改了stop为true,但线程三的循环未结束,说明线程三没有读到stop的新值。
- 原因分析:JVM中的即时编译器(JIT)对代码进行了优化,将取反stop的条件优化为固定的true,导致即使stop被修改,优化后的代码也无法读到新值。
- 解决方案介绍及测试
- 第一种方案:加vm参数禁用即时编译器(JIT),添加"-Xint"参数。但此方案不推荐,因为会影响其他程序使用即时编译器。
- 第二种方案 :在共享变量stop上加
volatile
修饰,告诉JIT不要对其做优化。给stop加上volatile修饰后再次测试,循环结束并打印循环次数,说明线程三能读到stop的新值。
- 总结与推荐
- 一般在项目开发中,推荐使用加volatile的方式来达到线程之间对共享变量的可见性,第一种方案了解即可,实际项目开发中使用较少。
- 禁止指令重排序特性介绍
- 含义:用Volatile修饰的共享变量在读写时加入屏障,防止其他读写操作越过屏障,阻止重排序。
- 指令重排序现象分析
-
多线程测试工具介绍及使用
- 工具及注解 :引入
jc stress test
工具,在方法上加@actor
注解保证方法内代码在同一线程执行,测试结果才正确;@outcome
注解可打印输出结果。
- 工具及注解 :引入
-
用Volatile关键字解决指令重排序问题
- 解决方法 :在共享变量上加
Volatile
关键字(value to
),在y
变量上加Volatile
关键字后再次测试,指令重排序问题解决。
- 解决方法 :在共享变量上加
-
加Volatile关键字解决指令重排序及原理说明
- 原理 :在共享变量上加入不同屏障,阻止其他读写操作越过屏障。加在
y
上时,写操作阻止上方其他写操作往下走,读操作阻止下方其他读操作往上走,将y
与其他读写操作隔离。 - 测试验证 :将
Volatile
关键字加到x
上,经过六轮测试10
的情况仍很多,说明加到x
上不能禁止指令重排序,因为写操作加的屏障只能阻止上方指令往下走,不能阻止下方指令往上走,读操作同理。
- 原理 :在共享变量上加入不同屏障,阻止其他读写操作越过屏障。加在
-
尝试在不同变量上加关键字及分析
- 变量都加关键字 :两个变量都加
Volatile
关键字能解决问题,但会影响性能,因为指令重排序对CPU
是一种优化,可减轻CPU
压力。 - 使用技巧 :写变量时修饰的变量放在代码最后位置,读变量时修饰的变量放在代码最上面,这样加载
Volatile
较少且能解决指令重排序问题。
- 变量都加关键字 :两个变量都加
-
课程总结
- Volatile关键字特性
- 保证线程间可见性,阻止编译器优化,使一个线程对共享变量的修改对其他线程可见。
- 禁止指令重排序,通过在读写共享变量时加入屏障来防止其他读写操作越过屏障,达到防止重排序的效果。
- Volatile关键字特性
文章目录
- synchronized
-
-
- [1. 基本概念与作用](#1. 基本概念与作用)
- [2. 使用方式](#2. 使用方式)
- [3. 底层原理(基于monitor)](#3. 底层原理(基于monitor))
- [4. 面试题回答要点](#4. 面试题回答要点)
- [5. 注意事项与进阶理解](#5. 注意事项与进阶理解)
-
- Java中synchronized锁相关知识总结
- Java内存模型(JMM)
- CAS概述及重要性
- volatile关键字
- AQS简介
AQS简介
- AQS简介与对比
- AQS定义:AQS是抽象队列同步器(AbstractQueuedSynchronizer),是JUC中提供的一种锁机制,作为其他组件(如ReentrantLock、Semaphore、CountDownLatch等)的基础框架,由Java语言实现。
- 对比Synchronized
- 实现方式:Synchronized是关键字,由C++语言实现;AQS是纯粹的Java API。
- 锁释放:Synchronized自动释放锁;AQS需要手动开启和关闭锁(因为是API)。
- 性能特点:在锁竞争激烈时,Synchronized会自动升级为重量级锁,性能较差;AQS提供多种解决方案,性能较好。
- AQS基本工作机制
- 状态管理 :内部有一个由volatile修饰的状态state,保证多个线程之间的可见性,0表示无锁,1表示有锁。线程获取锁时需修改state值,若当前state为0,线程可将其改为1并持有锁;若state为1,修改失败的线程进入队列等待。
- 队列结构:维护一个先进先出的双向队列,内部是双向链表,有指向队列头部(最早的线程)的head属性和指向队列最后一个元素(最新的线程)的tail属性。当持有锁的线程执行完,将state改为0(无锁状态)并唤醒队列中的头元素线程去持有锁。
- 多线程抢资源与原子性保证
- 多线程抢锁情况:多个线程同时抢资源时,例如线程0和线程四同时尝试修改state(初始为0),AQS使用CAS(Compare and Swap)操作来保证原子性,最终只有一个线程(如线程零)能抢到锁,将state改为1,其他线程进入队列等待,同时tail指向最新进来的线程。
- 公平锁与非公平锁实现
- 非公平锁 :当线程0持有锁并释放后(state由1改为0),会唤醒队列中的头元素线程(如线程一)去抢锁,此时若有新线程(如线程五)也来抢锁,(新线程5) 与队列中的 (线程1) 共同竞争资源,队列中的其他线程还在继续等待,这就是非公平锁。
- 公平锁 :新线程(如线程五)到来后,不能直接抢资源,而是直接到队列的最后一个元素等待。当state为0时,直接唤醒队列中的头元素线程(如线程一)去持有锁,遵循先来先得原则,类似生活中的排队,这就是公平锁。在AQS的不同实现类中,公平锁和非公平锁都可实现。
- 总结
- 回答AQS相关面试题时,应提及AQS是队列同步器和锁机制,作为基础框架被其他类使用;其内部维护先进先出双向队列存储排队线程;通过state控制锁,默认0为无锁,线程通过修改state获取锁,多个线程修改state时用CAS保证原子性。