synchronized锁
synchronized底层原理
当使用synchronized时,不需要自己编写代码进行上锁和上锁的操作,因为JVM帮我们把相关操作完成了。
JVM采用了monitorenter和monitorexit指令进行同步的,前者指向同步代码开始的位置,后者指向同步代码结束的位置,并通过ACC_SYNCHRONIZED标识符实现同步的,这个标识代表该方法是同步方法。monitorenter、monitorexit、ACC_SYNCHRONIZED都是基于Moniter实现的,而Moniter由ObjectMoniter实现。即synchronized的底层通过Moniter实现的,Moniter是JVM级对象,通过C++实现,线程获取锁需要对象锁关联Moniter。
ObjectMoniter包含了WaitSet、Owner、EntryList三部分组成。Owner存储获取了锁的线程,只能有一个线程获取;WaitSet用于存储处于等待状态的线程;EntryList用于存储处于阻塞状态的线程。
当代码进入synchronized时,首先根据对象锁关联的Moniter,判断Owner是否已存储了线程,若未存储,则当前线程获取锁成功,并进入到Owner中;若已存储,则进入EntryList队列等待,当Owner释放锁时,EntryList队列中的线程竞争锁的使用权(非公平的方式);当代码调用了wait()方法,则线程进入WaitSet队列中,进行等待。
synchronized可见性、有序性、可重入性实现
可见性
线程加锁前,清空工作变量中共享变量的值,需要使用共享变量时从主内存中读取共享变量的值。
线程加锁后,其他线程无法读取共享变量的值;在线程释放锁后,需要把共享变量的值刷新到主内存中。
有序性
synchronized同步的代码块,使得一次只能由一个线程执行,保证每一时刻都是单线程执行,同时as-if-serial语义的存在,单线程程序能保证最终的结果是有序的(as-if-serial语义:指令执行的顺序无论怎么重排序,执行的结果都不会被改变)。
可重入
是通过锁对象的计数器recursions记录线程获取锁的次数,当线程获取锁后,计数器加1,线程执行完后,计数器减1,直到计数器减为0。
锁升级过程
Java的对象头中,通过Mark Word结构,记录锁的状态。当后三位为001时,为无锁状态;后三位为101时为偏向锁状态;后两位为00时为轻量级锁;后两位为01时为重量级锁;后两位为11时对象标记为GC。
在无锁状态时,没有线程试图获取锁。
当第一个线程访问同步块时,且对象中处于无锁状态且偏向锁未被禁用,同步锁的对象头中存储该线程的id,并将对象头中的锁标记设置为偏向锁。
此时同一线程退出或进入这个同步块时,不需要通过CAS进行加锁或解锁。当其他线程进入这个同步块时,使用CAS操作将Mark Word中的id替换为新线程的id,若成功,则原线程不活跃,则将锁偏向至新的线程,Mark Word中的id也替换为新线程的id,锁仍为偏向锁;若失败,则原线程活跃,则撤销偏向锁,当偏向锁撤销时,遍历堆栈中所有锁记录,暂停偏向锁的线程,并检查锁对象,此时若有其他线程试图获取这个锁,则JVM撤销偏向锁,锁状态变为轻量级锁,此时偏向锁标志位设为0,锁标志位设为00。当多个线程在不同时段获取同一把锁时,JVM采用轻量级锁避免线程被阻塞和唤醒。
当一个线程试图获取轻量级锁时,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,当线程发现获取的锁为轻量级锁时,会将该锁对象的Mark Word复制到存储锁记录的空间中,然后线程试图通过CAS将锁的Mark Word修改为指向锁记录的指针,若成功,则该线程获取锁成功,若失败,则说明Mark Word已被替换为其他线程的锁记录,存在竞争,此时通过自旋的方式不断尝试获取锁,当达到一定次数未成功时,锁会升级为重量级锁。
当锁被升级到重量级锁时,JVM会在操作系统层面创建一个mutex互斥锁,所有尝试获取该锁的线程都会被阻塞,直至锁被释放。
锁优化
在JDK1.6前,synchronized锁为重量级锁,在JDK1.6后,HotSpot对Java锁进行了优化,除了偏向锁、轻量级锁、自旋锁外,还有一下两种:
1、锁粗化:若JVM检测到多个连续的锁操作在一个线程中时,会将多个锁操作合并为一个更大的锁操作,可减少锁的操作次数。锁粗化主要针对循环内连续加锁和解锁的情况。
2、锁消除:Java即时编译器即(JIT)可在代码运行时对代码进行分析,若发现某些锁操作不可能被多个线程同时访问,这些锁就会被消除,减小了很多不必要的开销。