Synchronized和ReentrantLock锁的区别
Synchronized和ReentrantLock锁的区别
在Java并发编程中,锁机制是确保线程安全的重要手段。本文将深入探讨synchronized和ReentrantLock这两种常用的锁机制,分析它们的实现原理、优缺点以及适用场景。
1. 锁机制概述
1.1 如何实现线程安全?
实现线程安全的主要方式包括:
- 加锁:包括悲观锁和乐观锁
- 原子类(CAS+自旋)
- ThreadLocal
其中,CAS(Compare and Swap)是“比较并交换”的缩写,AQS(AbstractQueuedSynchronizer)是“排队同步器”的缩写。
1.2 synchronized
synchronized可以用于实例方法、静态方法和代码块,具有可重入性和互斥性。
1.2.1 synchronized 锁实现
Java对象的锁机制是通过monitor对象实现的。每个Java对象都有一个与之关联的monitor对象,它在JVM中用C++实现,负责锁机制和CPU硬件的交互。monitor对象包含以下组件:
- owner:标识当前持有对象锁的线程ID,如果没有线程持有锁,则值为null。
- entryList:记录正在请求锁但尚未获得锁的线程。
- waitSet:记录通过
wait
方法等待的线程。
当线程获取锁后,计数器会+1,重复获取锁时计数器会持续增加。释放锁时计数器会-1,直到为0时其他线程才能获取锁。
1.2.2 synchronized 锁升级演变
为了解决synchronized的性能问题,JDK1.6引入了锁升级机制,根据线程竞争程度设计了不同类型的锁:
1.2.3 Java 对象结构
Java对象的内存布局主要包括:
- 对象头:
- mark word:存储对象的运行时数据,如哈希码、GC分代年龄、锁状态标志等。
- class pointer:指向对象的类元数据。
- 对象中的实际数据:存储对象的变量和内部对象。
- 对齐填充:用于内存对齐,提高访问速度。
1.2.4 mark word
mark word在不同锁状态下的存储信息如下:
- 无锁:对象创建后,没有线程访问时的状态。
- 偏向锁:当一个线程首次访问对象时,会将mark word标记为偏向锁,并记录线程ID。
- 轻量级锁:当另一个线程尝试获取锁时,会通过CAS操作尝试获取锁。
- 重量级锁:当多个线程竞争锁时,会升级为重量级锁,使用操作系统提供的互斥量进行同步。
1.2.5 锁升级过程
锁升级过程如下:
- 无锁:对象创建后,没有线程访问时的状态。
- 偏向锁:当一个线程首次访问对象时,会将mark word标记为偏向锁,并记录线程ID。
- 轻量级锁:当另一个线程尝试获取锁时,会通过CAS操作尝试获取锁。
- 重量级锁:当多个线程竞争锁时,会升级为重量级锁,使用操作系统提供的互斥量进行同步。
锁升级是JVM自动管理的,且升级过程不可逆。
1.3 ReentrantLock
ReentrantLock的内部结构如下:
- Sync:同步锁的基础类。
- FairSync:公平同步锁。
- NonfairSync:非公平同步锁。
ReentrantLock默认是非公平锁,可以通过构造函数指定公平锁。ReentrantLock底层使用CAS操作实现,但整体设计上更偏向于悲观锁,提供了更多的功能和灵活性,如可重入性、可中断性和公平性选择。
1.4 synchronized 和 ReentrantLock 对比
相同点:
都是悲观锁
都具有可重入性
区别:
加锁方式:synchronized通过操作系统层面的互斥变量mutex实现,而ReentrantLock基于CAS操作实现。
可中断性:ReentrantLock支持可中断的锁获取,而synchronized不支持。
公平性:ReentrantLock可以选择公平或非公平锁,而synchronized默认是非公平锁。
1.5 为什么synchronized比较慢?AQS是如何解决的?
synchronized的性能问题主要在于:
- 性能开销:每次获取和释放锁都需要通过操作系统内核态进行,导致上下文切换和系统调用开销。
- 缺乏灵活性:synchronized是一种悲观锁,一旦有线程进入临界区,其他线程都要被阻塞。
AQS(AbstractQueuedSynchronizer)通过以下方式优化了锁的实现:
- 队列管理:使用FIFO队列管理等待获取同步状态的线程。
- CAS操作:减少对操作系统层面锁操作的依赖,降低性能开销。
- 灵活性:支持独占锁、共享锁等不同类型的同步器实现。
1.6 tomcat 和 spring 线程池为什么要分开
Tomcat和Spring的线程池分开主要是为了解耦:
- Tomcat线程池:用于处理客户端请求。
- Spring线程池:用于处理业务逻辑。
这种分离使得系统架构更加清晰,便于管理和优化。
2. 原子类
2.1 AtomicLong 和 LongAdder
AtomicLong和LongAdder都是用于原子更新long类型值的工具类,但它们有一些区别:
- 竞争热点:AtomicLong存在单一的竞争点,所有线程都竞争同一把锁。而LongAdder通过分段存储减少竞争。
- 内部结构:AtomicLong使用CAS操作实现单个long值的原子更新,而LongAdder使用线程本地的long值数组。
- 空间占用:AtomicLong占用固定内存,而LongAdder在高并发场景下可能占用更多内存。
示例代码:
// 使用 AtomicLong 进行原子操作
AtomicLong atomicLong = new AtomicLong();
atomicLong.incrementAndGet();
// 使用 LongAdder 进行原子操作
LongAdder longAdder = new LongAdder();
longAdder.increment();
总结来说,AtomicLong适合低竞争场景,而LongAdder适合高并发场景。