Synchronized和ReentrantLock锁的区别
Synchronized和ReentrantLock锁的区别
锁
如何实现线程安全?
- 加锁:悲观锁、乐观锁
- 原子类(CAS+自旋)
- ThreadLocal
CAS 是 Compare and Swap(比较并交换)的缩写
AQS 是 AbstractQueuedSynchronizer (排队同步器)的缩写
synchronized
可以用于 实例方法、静态方法、代码块,具有可重入性、互斥性
synchronized 锁实现
Java是上层语言,不与操作系统打交道,也不支持锁功能,而是通过里每个对象都关联了一个
monitor(监视器)对象。JVM规定了每一个java对象都有一个monitor对象与之对应,它是JVM帮我们创建的,在底层使用C++实现,实现了锁机制和cpu硬件的交互。
monitor对象中包括了owner、entryList 和waitSet,功能如下:
- owner:标识当前拥有该对象锁的线程ID,如果没有线程拥有该对象的锁,owner的值为null。
- EntryList:用于记录那些正在请求该对象的锁的线程。(进来却还没有获得锁的线程)
- WaitSet:记录了那些正在等待该对象锁的线程。(通过 wait 方法实现等待)
monitor对象源码如下:
当线程获取锁后count会+1,当前线程重复拿到锁count会持续增加。当锁释放后count会-1,直到为0后才可被其他线程获取锁。
mutex(互斥变量)
同时多个线程拿锁如何保证只有一个线程拿到锁呢?就是通过mutex变量来实现的,monitor会维护一个
mutex(互斥变量),由操作系统内部的线程库维护的,上锁需要通过JVM从用户态切换到内核态,来调用底层操作系统的指令,性能极差,并且没拿到锁的会阻塞睡眠,引起cpu上下文切换,
synchronized 锁升级演变
JDK1.6版本为了弥补Synchronized性能缺陷,设计了Synchronized锁的升级膨胀,也就是根据当前线程的竞争激烈程度。设计了不同效果的锁。
java 对象结构
对象头:
- mark word:用于存储对象自身的运行时数据,比如哈希码、GC分代年龄、锁状态标志等。在32位JVM中,Mark Word通常占据8字节空间,而在64位JVM中通常占据16字节。
- class pointer:指向对象的类元数据,也就是对象所属类的元信息(比如方法表,字段表等)。
对象中的实际数据:
- instance data:存储对象中的实际数据,如变量、内部对象。
对齐填充:
- padding:用于填充对齐以保证对象在内存中的存储地址是一致的。对齐填充通常是为了满足处理器的对齐要求,提高访问速度。比如6kb对其填充后就是8kb,会多出来2kb
mark word
mark word在无锁、偏向锁、轻量级锁、重量级锁时存储信息如下
锁升级过程
1.6 之前,synchronized 锁仅采用一种简单的互斥锁,在1.6之后引入了锁升级概念:偏向锁、轻量级锁和重量级锁 。
Java 1.6相当于低版本来说,在处理低竞争情况下的锁性能方面有了显著的改进,但是高并发可能差距不大。
锁升级是JVM自动进行管理的。
- 无锁:对象创建后,没有线程进来的时候
- 偏向锁:当一个线程访问带有synchronized方法时,如果发现该对象未加锁,则在修改Mark Word标志位为1,标记为偏向锁,并将线程ID记录到对象的Mark Word中,并执行同步方法。后续这个线程再进来的时候便不用加锁直接执行同步方法。适用于处理只有一个线程访问同步块的情况,减少不必要的竞争和加锁。
- 轻量级锁:当另一个线程进来的时候。在这种情况下,不同的线程会通过 CAS 操作尝试获取锁,如果获取成功则表示升级为轻量级锁。
轻量级锁会在当前栈帧中建立一个Lock Record(锁记录)的空间,然后用来存放对象中Mark Work拷贝,然后把Lock Record中的owner属性指向当前对象。
接下来JVM会利用CAS尝试把对象Mark Word更新到Lock Record指针,成功则说明加锁成功。失败会判断当前对象是否指向栈帧。如果是说明线程已经持有对象,不是则通过自旋进行CAS操作
- 重量级锁:当锁两个或以上线程同时获取锁的时候,则升级为重量级锁。这种情况下,锁对象会使用操作系统提供的互斥量进行同步,确保并发访问的安全性。即进来便加锁、其他等待被唤醒。
注意:锁的膨胀只能升级不能降级,也就是说升级过程不可逆。
ReenTrantLock
网上借鉴一张图,ReentrantLock中有三个内部类Sync(同步锁)、FairSync(公平同步锁)、NonfairSync(非公平同步锁)。FairSync 和 NonfairSync 基础于Sync,而 Sync 基础于AbstractQueuedSynchronizer(AQS),AQS中还包括两个内部类 Node( AQS 的等待队列) 和 ConditionObject(实现条件等待)
ReenTrantLock 默认是非公平锁,可以通过 new ReentrantLock(true) 来指定公平锁。
默认执行的是 NonfairSync 类的 lock 方法,指定为公平锁则是执行的 FairSync 类中的 lock
ReentrantLock 底层是使用 CAS 操作来实现的,CAS 是乐观锁的一种实现方式,它通过比较并交换的方式来实现对共享变量的原子操作。但ReentrantLock 本身是一种悲观锁,这是因为它在获取锁时会使用“悲观策略”,即默认情况下假定会有线程竞争,因此会直接将线程阻塞等待获取锁。
这里存在一些理论上的区分和实际应用的考量:
CAS 与 ReentrantLock:虽然 ReentrantLock 的底层使用 CAS 操作来实现,这符合乐观锁的特点,但 ReentrantLock 本身的设计是为了提供更多的灵活性和功能,如可重入性、可中断性、公平性等。这些特性决定了它更适合作为一种悲观锁来使用。
应用场景:悲观锁和乐观锁都有各自的适用场景。悲观锁适合于写操作远多于读操作的场景,或者对数据更新频繁的情况;而乐观锁适合于读操作远多于写操作的场景,或者对数据更新不频繁的情况。
综上所述,尽管 ReentrantLock 底层使用 CAS 操作,但它设计为悲观锁,这是出于对更多功能和应用场景的考虑。在实际应用中,需要根据具体的场景和需求来选择使用悲观锁还是乐观锁。
synchronized 和 ReenTrantLock 对比
当使用1.6版本之前的 synchronized 时,锁的获取和释放通常会导致线程阻塞和唤醒,这涉及到了线程的上下文切换,因为当一个线程无法获取锁时,它会被阻塞,需要操作系统保存当前线程的状态并加载下一个线程的状态。这个过程包括保存和恢复线程的程序计数器、寄存器和堆栈等信息。
相比之下,ReentrantLock 的实现中采用了更为灵活的 AQS(AbstractQueuedSynchronizer)框架,底层使用 CAS 操作来控制锁的获取和释放。在 ReentrantLock 中,当一个线程无法获取锁时,它可以选择进行自旋等待而不是立即阻塞,这可以减少线程阻塞和唤醒的开销,减少了线程上下文切换的次数,提高了性能。
相同点:
- 都是悲观锁、具有可重入性
其他区别:
- 加锁方式:synchronized 是通过操作系统层面的互斥变量mutex进行加锁。而ReentrantLock的底层实现主要基于自旋锁和CAS(Compare-and-Swap)操作,与操作系统的加锁机制没有直接关系。(CAS操作是一种无锁技术,通过比较并交换当前值与预期值来实现原子性的操作。ReentrantLock在加锁时,会尝试使用CAS操作将锁的状态从未锁定状态修改为锁定状态。如果修改成功,则表示获取锁成功;否则,需要重试或采取其他策略。这种无锁技术避免了与操作系统的交互,从而减少了线程阻塞和唤醒的开销,提高了并发性能。)
- 可中断性:ReentrantLock 提供了可中断的锁获取方式,而 synchronized 不支持线程在等待锁时被中断。
- 公平性:ReentrantLock 可以选择公平或非公平的锁获取方式,而 synchronized 采用的是非公平锁获取方式。
synchronized 关键字在 JDK1.6 版本之前,是通过操作系统的 Mutex Lock 来实现的,这种方式效率较低。在 JDK1.6 及以后的版本中,synchronized 关键字引入了偏向锁、轻量级锁和重量级锁等优化措施,以提高其性能。但是在使用灵活度上面还是不如 ReentrantLock,因为 ReentrantLock 可以设置过期时间,锁中断,公平锁等。
为什么synchronized比较慢?AQS是如何解决的?
synchronized比较慢的原因:
- 性能开销:synchronized性能开销主要是来自于操作系统层面上锁实现,每次进入synchronized区块都需要通过操作系统内核态来进行锁的获取和释放,这会导致上下文切换和系统调用等额外开销,降低性能
- 缺乏灵活性:synchronized锁是一种悲观锁,一旦有线程进入临界区。其他线程都要被阻塞,这种情况会导致性能下降。
AQS性能好的原因:
AQS通过使用内部的FIFO队列管理等待获取同步状态的线程,并实现了自旋、CAS操作等手段来减少操作系统层面上的锁操作,从而降低性能开销。
AQS的灵活性也使得它可以根据具体的需求,实现不同类型的同步器,比如独占锁、共享锁等,例如, ReentrantLock使用AQS实现,通过AQS提供的底屋支持,它能够高效地实现锁的获取与释放,同时还提供了可中断锁等高级功能,进一步提高了灵活性和性能。
总的来说,AQS通过在Java层面上实现了一些同步操作,使用了更先进的算法和数据结构,来减少对操作系统层面上锁操作的依赖,从而提高了同步器的性能和灵活性。
tomcat 和 spring 线程池为什么要分开,不用一个?
解耦。tomcat 用于处理客户端请求,spring用于处理业务逻辑
redis 为什么没有事务?
作者不想搞,本身就是基于内存很快,不是关系型数据库,只是一个缓存框架,没必要。
原子类
LongAdder 和 AtomicLong 都是 Java 中用于原子更新 long 类型值的工具类,但它们有一些区别:
- 竞争热点:AtomicLong 存在一个单一的竞争点,即所有线程都竞争同一把锁。这意味着在高并发场景下,大量线程的竞争可能导致性能瓶颈。而 LongAdder 通过分散计数值来减少竞争,采用了分段的概念,每个段有自己的计数值,最后结果是对所有段的求和。
- 内部结构:AtomicLong 内部使用 CAS 操作来实现对单个 long 值的原子更新。而 LongAdder 内部存储的是一个 base 线程本地的 long 值数组,通过 CAS 操作对这些数组进行原子更新。
- 空间占用:AtomicLong 占用的内存空间是固定的,由一个 long 值和一些额外的控制字段组成。而 LongAdder 在多线程高并发的情况下,由于分段,可能需要存储多个 long 值(每个线程一个),因此可能会占用更多内存。
举例说明,下面是简单的示例代码:
// 使用 AtomicLong 进行原子操作
AtomicLong atomicLong = new AtomicLong();
atomicLong.incrementAndGet();
// 使用 LongAdder 进行原子操作
LongAdder longAdder = new LongAdder();
longAdder.increment();
综上所述,AtomicLong 适合于低竞争的场景,而 LongAdder 适合于高并发的场景,因为它能够分散并发访问,从而减少竞争。