synchronized实现原理及优化

一、概述

线程安全在并发编程中是重要关注点,造成线程安全问题的主要诱因有两个:一是存在共享数据(也称临界资源),二是存在多个线程共同操作共享数据。synchronized关键字能够保证在同一时刻只有一个线程可以执行某个方法或某个代码块。

1、synchronized作用

原子性:synchronized保证语句块内操作是原子的

可见性:synchronized保证可见性(通过“在执行unlock之前,必须先把此变量同步回主内存”实现)

有序性:synchronized保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”)

2、synchronized使用

修饰实例方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁
修饰静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

二、实现原理

synchronized在Java中实现原理主要是基于Java的内存模型(JMM)和对象锁机制。

1、Java内存模型(JMM)

依赖于JMM对一个变量unlock之前必须要同步到主内存,对变量进行lock操作将会清空本地内存中此变量的值,从新从主内存中load或assign初始化变量值。JMM定义了线程与主内存之间的抽象关系

1. 线程之间的共享变量存储在主内存中。

2. 每个线程都有一个私有的本地内存(也称为工作内存),本地内存中存储了该线程使用到的主内存副本的拷贝

3. 线程对变量的所有操作(读取、赋值等)都必须在自己的本地内存中进行,而不能直接读写主内存中的变量

多线程在JMM内存模型中运行机制

2、对象锁机制

synchronized通过对象锁机制来保证同一时刻只有一个线程可以访问被锁定的代码块或方法。在JVM中synchronized的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步的。所以谈synchronized的实现需要先说下对象在JVM中的存储:Java对象头、Monitor对象监听器。

2.1 对象结构

在HotSpot虚拟机中,对象在堆内存存储的布局主要分为三个区域:对象头、实例数据、填充数据。

实例数据:存放类的属性数据信息,包括父类的属性信息。若是数组的实例部分还包括数组的长度。这部分内存按4字节对齐。

填充数据:由于虚拟机要求对象的起始地址必须是8字节的整数倍。填充数据不是必须存在的。

对象头:对象头包括了分为两部分信息,第一部分存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态等信息被称为MarkWord。另一部分用于存储执行对象类型数据的指针被称为Klass Pointer。若是数组对象还会有一个额外部分存储数组长度。

2.2 对象头组成

对象头包括了MarkWord和KlassPointer两部分,若是数据对象还有一个额外部分存储数组长度。

MarkWord

MarkWord主要用来存储对象自身运行时的数据信息,如hashCode、GC分代年龄等。MarkWord的位长度为JVM的一个Word大小,32位JVM的MarkWord是32位,64位JVM的MarkWord是64位。

其中各部的含义如下:

锁标志位(lock):用低两位表示锁标志位,该标记位的数据不同代表MarkWord的含义不同。

偏向锁位(1bit)锁标记位(2bit)锁状态
001无锁状态
101偏向锁
000轻量级锁
010重量级锁
011GC标记信息

偏向锁位(bias_lock):对象是否启动偏向锁标记,只占1个二进制位。为1时表示对象启动偏向锁,为0时表示对象没有偏向锁。

分代年龄(age):4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

hashCode:31位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。

JavaThread:持有偏向锁的线程ID。

Epoch:偏向时间戳。

lock record(ptr_to_lock_record):指向栈中锁记录的指针。

互斥量(ptr_to_heavyweight_monitor):指向monitor对象(也称为管程或监视器锁)的起始地址,每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor对象可以与对象一起创建销毁或当前线程试图获取对象锁时自动生,但当一个monitor被某个线程持有后,它便处于锁定状态。

KlassPointer

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

  • 每个Class的属性指针(即静态变量)
  • 每个对象的属性指针(即对象变量)
  • 普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等

2.3 Monitor监视器锁

Monitor介绍

在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程。

当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。执行过程如下:

由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。

对象Object的Monitor机制

Java虚拟机给每个对象和class字节码都设置了一个监听器Monitor,用于检测并发代码的重入,同时在Object类中还提供了notify和wait方法来对线程进行控制。

Object类中的代码如下:

public class Object {
…
private transient int shadow$monitor;
public final native void notify();
public final native void notifyAll();
public final native void wait() throws InterruptedException;
public final void wait(long millis) throws InterruptedException {
wait(millis, 0);
}
public final native void wait(long millis, int nanos) throws InterruptedException;
…
}

Monitor可以类比为一个特殊的房间,这个房间中有一些被保护的数据,Monitor保证每次只能有一个线程能进入这个房间进行访问被保护的数据,进入房间即为持有Monitor,退出房间即为释放Monitor。

当一个线程需获取锁对象的Monitor时,它会首先在entryList入口队列中排队,若无线程正在持有锁对象的Monitor,那么该线程会和entryList队列、waitSet队列中其他线程进行竞争(即通过CPU调度),选出一个线程来获取锁对象的Monitor,执行受保护的方法或代码块。执行完毕后释放锁对象的Monitor。若已经有线程持有锁对象的Monitor,那么需要等待其释放Monitor后再进行竞争。

waitSet队列:当一个线程拥有Monitor后,经过某些条件的判断(比如用户取钱发现账户没钱),这个时候需要调用Object的wait方法,线程就释放了Monitor,进入waitSet队列,等待Object的notify方法(比如用户向账户里面存钱)。当该对象调用了notify方法或者notifyAll方法后,waitSet中的线程就会被唤醒,然后在waitSet队列中被唤醒的线程和entryList队列中的线程一起通过CPU调度来竞争对象的Monitor,最终只有一个线程能获取对象的Monitor。

三、JDK1.6对synchronized的优化

synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。JDK团队在JDK1.6中对synchronized进行了大量优化,主要包括锁消除、锁膨胀、锁升级三部分。

1、锁消除

当JVM在JIT(Just-In-Time)编译时,在Synchronized修饰的代码中的锁不可能被其他线程访问到即不存在操作临界资源竞争情况,那么JVM在编译时会将这个锁清除。这样可以避免无谓的锁操作,提升执行效率。即便写了Synchronized也不会触发。

2、锁膨胀

在JDK1.6中引入了锁膨胀的优化技术。当JVM检测到一段代码中有多次加锁和解锁的操作时,若这些锁针对同一个对象,并且加锁和解锁操作频繁但加锁范围小,JVM会将锁的范围扩展到更大区域,从而减少锁的获取和释放次数,降低锁的开销。

如在for循环中频繁的获取和释放锁资源,这样带来的消耗很大,通过锁膨胀将锁范围扩大,减少资源消耗。代码如下:

		public void method(){
			for(int i = 0; i< 999999; i++){
				synchronized(对象){
					
				}
			}
		}

JVM对上面代码编译时会触达锁膨胀,编译后代码

		public void method(){
			synchronized(对象){
				for(int i = 0; i< 999999; i++){
				
				}
			}	

        }

3、锁升级

ReentrantLock的实现是先基于乐观锁的CAS尝试获取锁资源,若拿不到资源才会挂起线程。在JDK1.6中synchronized引入了无锁偏向锁、轻量级锁和重量级锁等多种锁状态。锁的升级是

为了提高获取锁和释放锁的效率,但升级后的锁会带来更好的性能开销。锁升级是单向的,从偏量锁开始根据竞争程度逐步升级为轻量级锁和重量级锁。

锁升级状态转变

3.1 无锁

当一个线程访问一个没有被锁定的代码块时,这个代码块处于无锁状态。此时,任何线程都可以访问这个代码块,不需要进行任何锁的竞争。

3.2 偏向锁

偏向锁的目的是共享数据在无竞争的情况下提高获取锁的性能。

获取偏向锁

1. 判断锁对象的锁状态否为“101”-可偏向状态
2. 若为可偏向状态,则判断锁对象头中的线程ID是否是当前线程,若是进入同步块;
3. 若线程ID并未指向当前线程,会基于CAS方式尝试将偏向锁指向当前线程。若操作成功,将Mark Word中线程ID更新为当前线程ID,进入同步块
4. 若操作失败,代表出现了锁的竞争,准备撤销偏向锁。会根据线程是否处于活动状态,决定是转换为无锁状态还是升级为轻量级锁。

当一个线程第一次访问一个被synchronized锁保护的代码块时,这个代码块会尝试进入“101”-偏向锁状态。此时,JVM会将锁对象头中的标记位设为“偏向模式”,并记录持有锁的线程ID。此后,该线程再次访问这个代码块时,可以直接获取锁,不需要进行任何锁的竞争。偏向锁可以提高无竞争情况下的性能。

释放偏向锁

偏向锁使用了遇到竞争才释放锁的机制。偏向锁的撤销需要等待全局安全点,然后它会首先暂停拥有偏向锁的线程,然后判断线程是否还活着,如果线程还活着,则升级为轻量级锁,否则,将锁设置为无锁状态。

3.3 轻量级锁

轻量级锁也是JDK1.6引入的新型锁机制。它不是用来替换重量级锁的,它的本意是在没有多线程竞争的情况下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

轻量级锁是一种基于CAS自适应自旋锁的方式尝试获取锁资源。使用CAS尝试将线程ID存储到锁对象的对象头中。若操作成功,则表示当前线程成功获取锁。若操作失败,则表示锁已被其他线程持有,当前线程需要等待或其他同步操作。

获取轻量级锁

1. 当线程尝试进入同步代码或方式时,JVM会检查锁对象的状态。JVM首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前Mark Word的拷贝。

2. 若锁对象的Mark Word表明锁处于无锁状态(即未被任何线程持有),JVM会尝试将锁膨胀为轻量级锁。膨胀过程中JVM会将锁对象的Mark Word更新为指向当前线程的栈帧中的锁记录(Lock Record)的指针。

3. 为确保轻量级锁获取是原子的,JVM会利用CAS自适应自旋锁方式尝试将锁对象的Mark Word更新为指向当前线程的锁记录。

4. 若更新成功,那么该线程就拥有了该对象的锁,并且锁对象的Mark Word锁标志位转变为“00”,即表示此对象处于轻量级锁定状态;

5. 若更新失败,JVM首先会检查锁对象的Mark Word是否指向当前线程的栈帧,若锁对象被其他线程持有,则当前线程会自旋等待(在一定时间被循环尝试重新获取锁)。

6. 若自旋次数超过预设的阈值,或JVM检测到有多个(两个以上)线程在竞争同一个锁,JVM会将锁升级为重量级锁。

自适应自旋锁:是基于CAS操作实现,线程如果自旋成功了,那么下次自旋的次数会更加多;反之会减少自旋次数甚至取消。

释放轻量级锁

如果锁对象的Mark Word仍然指向线程的锁记录,那就用CAS操作将锁对象当前的Mark Word与线程栈帧中的Displaced Mark Word交换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。

3.4 重量级锁

Synchronized的重量级锁是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。

3.5 偏向锁、轻量级锁和重量级锁对比

1. 如果是单线程使用,偏向锁的代价最小,每次判断当前线程是否独占该锁即可

2. 如果出现了其他线程竞争,则偏向锁就会升级为轻量级锁

3. 线程达到最大自旋次数,升级为重量级锁

四、与ReentrantLock区别

1、底层实现

synchronized:是JVM层面的锁,通过对象头中的Mark Word来实现锁的获取和释放。底层是基于ObjectMonitor实现。它支持偏向锁、轻量级锁和重量级锁等多种锁状态,以优化性能。

ReentrantLock:是API层面的锁,基于AbstractQueuedSynchronizer(AQS)框架实现。AQS是一个用于构建锁和其他同步类的框架,提供了丰富的同步原语。

2、锁的获取和释放

synchronized:隐式地获取和释放锁。当线程进入synchronized修饰的方法或代码块时,会自动获取锁,并在方法或代码块执行完毕后自动释放锁。

ReentrantLock:显式地获取和释放锁。线程需要调用lock()方法来获取锁,并在使用完毕后调用unlock()方法来释放锁,也可以通过tryLock()方法尝试获取锁。这种显式的方式提供了更高的灵活性,但也要求程序员必须确保在finally块中释放锁,以避免死锁。

3、锁的公平性

synchronized:非公平锁。synchronized无法保证等待时间最长的线程会最先获得锁。

ReentrantLock:可以是公平锁,也可以是非公平锁。通过构造函数中的布尔值参数可以指定锁的公平性。公平锁会按照线程请求锁的顺序来分配锁,而非公平锁则允许插队。

4、锁的灵活性

synchronized:是Java语言的关键字,使用起来相对简单,但灵活性较低。它无法设置超时时间,也无法尝试获取锁而不阻塞当前线程。

ReentrantLock:提供了更高的灵活性。可以设置超时时间或tryLock()方法,尝试获取锁而不阻塞当前线程,以及检查锁是否被某个线程持有等。

5、响应中断

synchronized:无法响应中断。当线程在synchronized代码块中等待锁时,如果线程被中断,它会继续等待直到获取到锁,然后才能抛出InterruptedException。

ReentrantLock:可以响应中断。当线程在ReentrantLock的lock()或lockInterruptibly()方法中等待锁时,如果线程被中断,它会立即抛出InterruptedException,并释放已经占有的锁(如果是lockInterruptibly())。

6、锁的绑定条件

synchronized:不支持绑定多个条件。synchronized只能用于控制对共享资源的访问,而无法提供复杂的线程间通信机制。

ReentrantLock:支持绑定多个Condition条件。ReentrantLock可以与多个Condition对象关联,以实现更复杂的线程间通信和协作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值