【JUC系列-04】精通Synchronized底层的实现原理

这篇具有很好参考价值的文章主要介绍了【JUC系列-04】精通Synchronized底层的实现原理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

JUC系列整体栏目


内容 链接地址
【一】深入理解JMM内存模型的底层实现原理 https://zhenghuisheng.blog.csdn.net/article/details/132400429
【二】深入理解CAS底层原理和基本使用 https://blog.csdn.net/zhenghuishengq/article/details/132478786
【三】熟练掌握Atomic原子系列基本使用 https://blog.csdn.net/zhenghuishengq/article/details/132543379
【四】精通Synchronized底层的实现原理 https://blog.csdn.net/zhenghuishengq/article/details/132740980
【五】通过源码分析AQS和ReentrantLock的底层原理 https://blog.csdn.net/zhenghuishengq/article/details/132857564
【六】深入理解Semaphore底层原理和基本使用 https://blog.csdn.net/zhenghuishengq/article/details/132908068
【七】深入理解CountDownLatch底层原理和基本使用 https://blog.csdn.net/zhenghuishengq/article/details/133343440
【八】深入理解CyclicBarrier底层原理和基本使用 https://blog.csdn.net/zhenghuishengq/article/details/133378623

一,深入理解synchronized底层的实现原理

1,初识synchronized

在最前面的两篇文章中,谈了java的内存模型JMM,得知了为何会出现共享变量的不安全性,同时也谈到了通过无锁的方式实现共享变量安全的CAS,但是CAS本身也存在着一定的缺陷,不能适用于大规模并发的场景下,因此从这篇开始,讲解一个通过加锁的方式来实现共享变量的安全性,就是这篇的主角 synchronized

synchronized同步块是java内部提供的一个内置锁,又被称为监视器锁monitor,其实现是通过操作系统底层的互斥量来实现的。主要是针对一些临界区中的临界资源进行上锁的操作,其使用相对来说也比较简单,主要分为类锁和实例锁。接下来谈一下这个关键字是如何使用的。

1.1,类锁

顾名思义,就是将锁加在类方法或者静态代码块的上面,添加到类方法的方式如下

public static Integer data = 0;
public synchronized static void increment(){
    data ++;
}

除了上面的加在方法上之外,还可以直接添加到同步代码块里面

public static Integer data = 0;
public static void toIncrement(){
    synchronized (SynchronizedByClass.class){
        data ++ ;
    }
}

1.2,对象锁

对象锁,指的就是加在实例方法的上面,以及实例方法中的代码块上面,添加到实例方法的方式如下

public Integer decrementData = 0;
public synchronized  void decrement(){
    this.decrementData -- ;
}

除了加在方法上,也可以直接通过加在代码块上面的形式加在对象上面

public void decrementData1(){
    synchronized (this){
        this.decrementData --;
    }
}

private String lock = "";
public void decrementData2(){
    synchronized (lock){
        this.decrementData --;
    }
}

2,synchronized在jvm的字节码指令

由于synchronized是一个关键字,因此可以通过查看其字节码指令去了解底层是如何实现的。在分析之前,需要在idea中安装一个插件 jclasslib ,我在jvm系列中曾经讲过这个插件如何安装以及使用:https://zhenghuisheng.blog.csdn.net/article/details/129610963

接下来通过一段简单的代码来分析加在方法中其底层是如何实现的

public class SynchronizedJvmCode {
    public Integer data = 0;
    public synchronized void add(){
        this.data ++ ;
    }
}

在methods中找到这个add方法,在右边可以看到一个重要的标志:Access flags,其对应的值是0x0021

【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

在jdk关键字中,详细的描述了这个flag标志的信息,0x0021对应的就是这个ACC_SYNCHRONIAZED 这个指令,因此显而易见,在方法上加synchronized是通过这个 ACC_SYNCHRONIAZED 标志实现的

【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

接下来通过一段简单的代码来分析加在代码块其底层是如何实现的

public class SynchronizedJvmCode {
    public Integer data = 0;
    public void add(){
        synchronized (this){
            data++ ;
        }
    }
}

这个add方法对应的字节码的指令如下,可以发现在这里面多两个东西,分别是monitorentermonitorexit ,分别代表着加锁和解锁的意思,因此在代码块中就是通过这两个指令实现锁的操作的。

并且在这个字节码指令中,存在两个 monitorexit ,根据下面31行已经解锁了跳到后面41return了,但是后面还有字节码操作,通过第40行可以发现,就是为了防止出现异常导致死锁,类似于在try中有解锁操作,在catch中也有解锁的操作,这样不管有没有异常,都能正常解锁

 0 aload_0
 1 dup
 2 astore_1
 3 monitorenter  //加锁
 4 aload_0
 5 getfield #3 <com/zhs/study/juc/synchronize/SynchronizedJvmCode.data>
 8 astore_2
 9 aload_0
10 aload_0
11 getfield #3 <com/zhs/study/juc/synchronize/SynchronizedJvmCode.data>
14 invokevirtual #4 <java/lang/Integer.intValue>
17 iconst_1
18 iadd
19 invokestatic #2 <java/lang/Integer.valueOf>
22 dup_x1
23 putfield #3 <com/zhs/study/juc/synchronize/SynchronizedJvmCode.data>
26 astore_3
27 aload_2
28 pop
29 aload_1
30 monitorexit  //解锁
31 goto 41 (+10)
34 astore 4
36 aload_1
37 monitorexit  //解锁
38 aload 4
40 athrow
41 return

也就是说,这个加锁和解锁是jvm内部帮我们实现的,不需要我们手动去加锁解锁,相对于Lock这种显示锁,synchronized就是一把隐式锁。

总结来说就是:如果在方法上加这个synchronized,其底层是通过ACC_SYNCHRONIAZED标志实现的,如果是在同步块上synchronized,其底层是通过monitorEnter和monitorExit实现的。但是这两种方式都是通过jvm去调用操作系统来实现的,这样就会涉及到用户态到内核态之间的来回切换,以及会涉及到阻塞等等问题,因此这个关键字的使用也是挺耗性能的,相对于volatile来说,这个synchronized就是一把重锁。

3,Monitor监视器

在操作系统中,monitor又可以被称为这个管程,主要是帮助共享变量在并发场景下可以保证数据的安全性。在java中实现管程的方式是由synchronized关键字和wait,notify和notifyAll这三个方法共同实现的。其底层的模型架构如下
【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

在hotspot虚拟机中,有关Monitor的底层实现的部分源码如下

ObjectMonitor() {
    _recursions   = 0;   // 锁的重入次数 
    _object       = NULL;  //存储锁对象
    _owner        = NULL;  // 标识拥有该monitor的线程(当前获取锁的线程) 
    _WaitSet      = NULL;  // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
    _cxq          = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
    _EntryList    = NULL ; //存放在进入或重新进入时被阻塞的线程 (也是存竞争锁失败的线程)

里面有一个锁的重入次数,表示synchronized是一把可重入锁;里面主要有三个队列,一个是双向循环列表实现的waitSet队列,里面存储的是调用wait方法释放锁之后阻塞的线程;从外面进来的大量线程在没有拿到锁的情况下,会进入这个cxq的队列里面,而cxq的数据结构是栈的方式实现,就是先进后出,表示这是一把非公平锁,并且不能保证有序性;entryList存储的是被阻塞的线程,会和cxq中的线程一起去抢锁

接下来用一段代码来演示一下内部的整个流程,如下面这段代码,首先三个方法同时抢一把锁,此时模拟为三个线程,由于代码从上往下执行,因此这个thread1先进入cxq队列中,随后是2,3。然后3先拿到锁,有一个wait方法,会释放资源和释放锁,此时thread3就进入这个waitSet的队列里面,thread2和thread3一样,此时就剩thread1线程,就会拿到锁。

private String lock = "";
public void thread1() throws InterruptedException {
    synchronized (lock){
        ...    
    }
}
public void thread2() throws InterruptedException {
    synchronized (lock){
        wait(100);
    }
}
public void thread3() throws InterruptedException {
    synchronized (lock){
        wait(300);    
    }
}

如果此时thread2和thread3被notifyAll给唤醒,此时这两个线程会从waitSet队列中进入entryList队列或者cxq队列,主要是根据不同策略实现的,随后这两个队列的线程再次一起去抢锁。如果entryList队列的数据为空,则直接将cxq的队列数据全部存储到entryList里面,如果entryList的数据不为空,则优先唤醒entryList里面的线程。

【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

总而言之:CXQ队列是线程刚从外面进来的队列,由于内部采用的是栈结构,先进后出,所以整体是一个非公平锁的操作,waitSet队列存储的是加了wait被阻塞的线程,wait是会释放资源的,当被唤醒后,会重新进入EntryList或者CXQ队列中,这取决于不同的策略实现,EntryList中线程被唤醒的优先级高于CXQ队列

CXQ队列是一个同步等待队列,waitSet队列是一个条件等待队列。synchronized在多线程抢占锁时,采用的是cas的方式实现的。

4,对象的内存布局

上面有提到monitor监视器是将锁加在对象上面的,那么一个对象上面是否加锁,那就得了解一下这个java中对象的内存布局,其主要可以分为三个部分:对象头,实例数据和对齐填充。以下所有例子都是用64位的虚拟机

  • 对象头:里面主要是会记录一些对象的hashcode,年龄,线程id,锁的标志和锁的状态等
  • 实例数据:类中的一些属性信息等
  • 对齐填充:每个对象所占的字节数必须是8的整数倍,否则补齐

【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

在对象头中,又可以分为三个部分,分别是:Mark Word,MetaData压缩指针和数组长度。

  • Mark word主要存储一些对象的hashcode,年龄,线程id,锁的标志和锁的状态等,一般占8个字节
  • Klass Pointer指的是对象的压缩指针,在jdk8中默认是开启压缩指针的,一般占用4个字节,如果没有开启,则占8个字节。虚拟机通过这个指针来确定这个对象是属于哪个实例的
  • 如果一个对象中存在数组,那么这个数组默认占用4个字节

因此看下面这个类,如果new一个Data这个类,那么占用的字节数如下:对象头中的markWord占8个字节,压缩指针占4个字节,数组占4个字节,实例数据age占4个字节,总共占20个字节,但是对齐填充中需要满足是8的整数倍,因此总共占24个字节。

class Data{
    private int age;
    private int code[];
}

5,锁的几种状态

在这个markword中,会储存关于锁的信息,以jvm64位的虚拟机为代表,如下图所示。在synchronized中,主要可以分为无锁状态、偏向锁状态、轻量级锁状态和重量级状态,无锁通过001表示,偏向锁通过101表示,轻量级锁通过00表示,重量级锁通过10表示。

【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

在这几种锁中,会随着锁的竞争激烈程度不断的变强,会从当没有线程时,处于一个无锁状态,当有一个线程时,会处于偏向锁状态,随后会随着并发的强度不断的上升锁的强度,从轻量级锁再到重量级锁,并且这是一个不可逆的过程。

5.1,偏向锁

但是在jdk6开始,默认这个偏向锁是延迟开启的,因为在jvm进行类初始化等操作的时候,会使用大量的synchronized关键字,也就是说在加载阶段我们可以明确是可能存在多个线程并发的,如果还按先偏向锁再到轻量级锁,这样就可能会有部分性能问题,因此为了解决这个问题,干脆就直接从无锁到轻量级锁了,从而将这个偏向锁省略或者延迟加载。jvm默认采用的是延迟加载的,默认是在jvm虚拟机启动4s之后开始加载,也就是说,如果没有任何操作,只有在jvm启动4s后加载的对象,才有可能出现偏向锁。以下是关于jvm操作偏向锁的一些参数。

//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
//禁止偏向锁
-XX:-UseBiasedLocking 
//启用偏向锁
-XX:+UseBiasedLocking 

当然也可以直接通过强行睡眠的方式,来解决这个偏向锁问题

Thread.sleep(4000);

但是根据下图可以发现在偏向锁中,并没有存储这个对象hashcode的地方,因此如果在睡眠4s之后再调用这个hashcode方法,就会出现这个偏向锁撤销的情况,又由于这几种锁的状态不可逆,所以会直接从偏向锁状态升级为轻量级锁的状态,也可能会升级成重量级锁。
【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

除了调用这个hashcode之外,也可能调用wait方法或者notifyAll方法等锁出现偏向锁失败的场景。

5.2,轻量级锁

在无锁或者偏向锁中,都可能升级为轻量级锁。轻量级锁,顾名思义就是此时争取锁的线程不多,没那么激烈,或者说线程与线程之间交替执行。由于synchronized底层抢锁是过cas的方式实现,轻量级锁并不需要cas就能拿到锁,如果需要长时间cas,那么就会进行一个锁膨胀的操作,最后去获取一个monitor对象,变成重量级锁。

由于延迟偏向锁是4s后开始的,因此开启一个延迟偏向锁,随后创建一个Object对象,并且创建两个线程

public static void main(String[] args) throws InterruptedException {
    //开启延迟偏向
    Thread.sleep(5000);
    //延迟4s后才开始加载的对象
    Object lock = new Object();
    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
	//创建线程1
    new Thread(()->{
        synchronized (lock){
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    },"thread1").start();
	//创建线程2
    new Thread(()->{
        synchronized (lock){
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    },"thread2").start();
    
}

其输出打印的结果如下,由于延迟偏向锁的开启,此时状态为101,但是此时并没有偏向哪个线程;随后第二个线程打印出来的也是101,还是延迟偏向锁,代表刚刚那个偏向锁现在已经有执向的线程了;又有了第二个线程来抢锁,随后随着锁竞争的激烈程度锁就行了升级,变成了00,就是轻量级锁。

【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

在轻量级锁中,拿到锁的线程会将对象锁的markword存储在当前栈帧中, 而markword中存储的线程id也是当前线程的id,当有别的线程来抢锁时,需要通过cas操作,就是看是否携带这个markword以及线程的id是否匹配,如果不匹配,则需要继续自旋。而当前线程执行完成之后,需要将轻量级锁变成无锁状态,别的线程才能获取到锁,锁的不可逆指的是重量级锁到轻量级锁的不可逆,以及轻量级锁到偏向锁的不可逆。

5.3,重量级锁

偏向锁和轻量级锁都是通过操作mark word来修改对象锁的状态的,但是重量级锁不一样,需要切换到内核态进行锁状态的修改,需要调用底层的moniter机制来实现。也就是说前面两个不需要加锁或者cas就能操作,后者需要用户态到内核态之间的来回切换。重量级锁就是在cas时,经过长时间轮询还是不能获取到锁,那么这个锁就会升级膨胀,随后会去获取操作系统底层的monitor对象,此时轻量级锁升级为重量级锁,并且期间需要不断的cas自旋。只有在重量级锁需要长时间自旋,轻量级锁和偏向锁是不需要自旋的

依旧是采用下面这段代码,再在轻量级锁那段代码上面再加一个线程thread3

new Thread(()->{
    synchronized (lock){
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
},"thread3").start();

上面的代码结果如下,可以发现前面两步开始延迟偏向锁,但是第三步开始就不一样了,因为随着锁的竞争强度的增加,从原来的00轻量级锁变成了现在的10重量级锁

【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

重量级锁到轻量级锁的是不可逆的,但是重量级锁可以直接到无锁状态。并且根据轻量级锁和重量级锁的两段代码,可以发现并不存在无锁到偏向锁的过程,要么就是无锁,要么就是偏向锁,而且都是用01表示,表明其实这两个是互斥的。

总而言之:如果线程没有开启延迟偏向锁,那么对象刚加锁后会由无锁变成轻量级锁的状态,轻量级锁在获取锁失败的情况下就会膨胀,获取到monitor对象,随后由轻量级锁变成重量级锁,内部通过cas的方式竞争锁;如果线程开启了延迟偏向锁,那么对象会自动进入一个匿名偏向锁的状态,随后在拿到一把锁之后,对象会进入一个有指向线程id的偏向锁状态,随后通过一些列的偏向锁锁撤销等操作,随着偏向锁撤销等操作,进入无锁,轻量级锁或者重量级锁。

6,锁的升降级方式

上面讲了几种synchronized锁的状态,有无锁、偏向锁、轻量级锁和重量级锁这几种锁,接下来详细谈一下底层是如何进行锁升降级的。接下来以下图为主要核心讲解这几个锁之间的关系。

【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

6.1,匿名偏向锁和偏向锁关系

假设此时延迟偏向锁没有关闭,那么在4s后的延迟偏向锁开启之后创建一个锁对象,因此这个锁对象中,会有一个markword,此时该对象是处于一个偏向锁的状态,但是由于并没有线程来获取这把锁,此时执行的线程id为0,锁标志位101记录在markword中,此时的锁为一个匿名偏向锁的一个状态。很多人会觉得匿名偏向锁是一个无锁状态,其实不是,通过标志位就可以知道101,是一个偏向锁的状态,001才是无锁状态。

假设此时有一个线程进来拿这把锁(可以看5里面的例子),那么此时还是一把偏向锁,此时对象锁obj中的markword中的线程id会指向偏向抢这把锁的线程id,该线程id为操作系统底层的id值。并且在偏向锁解锁后,不会变成无锁状态,还是一把偏向锁状态。

Object obj = "";

【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

6.2,无锁,轻量级锁和重量级锁之间关系

1,假设在不考虑偏向锁的情况下,此时无锁、轻量级锁和重量级锁的升级关系是这样的:

  • 首先在没有线程来竞争这把对象锁时,此时的对象锁中的markword的锁标志是001,是一个无锁状态
  • 当有一个线程或者线程交替执行的时候,此时对象锁会有指针指向拿到这把锁的线程,并且将markword中的值改成00,拿到锁的线程也会将无锁时的markword保存在栈帧内部,此时无锁状态升级成轻量级锁状态;
  • 在轻量级锁中,会随着cas长时间拿不到锁而膨胀,当拿到monitor对象之后,会升级成一把重量级锁,此时对象锁中的markword的锁标志位10。

上面三种情况是随着线程抢锁的激烈程度增加而增加的,也有可能直接出现从无锁到重量级锁的情况,如某一时刻的并发量大,需要大量的长时间的cas,那么此时会从无锁直接升级成重量级锁。

2,既然存在锁升级的情况,那么也肯定存在锁降级的情况,其关系如下

  • 轻量级锁状态在释放锁的时候,如果此时没有其他线程来竞争锁,那么此时会将锁释放,并且将当前线程中保存的markword还原给初始的无锁状态。
  • 重量级锁和轻量级锁一样,在释放锁时,也会将锁从重量级锁降级成无锁状态。

不存在重量级锁到轻量级锁之间的降级,这两个是不可逆的,因为有monitor对象会优先使用monitor对象。在锁降级时,当前线程会将一开始保存的初始markword还原回去,这样不管过程如何修改,最终都可以还原锁对象最初的无锁状态。

6.3,偏向锁的撤销

通过上述5中的例子可以发现,当偏向锁解锁之后,还是处于偏向锁的状态,而不是无锁,因此就引入了这个偏向锁撤销的概念。还是得看着下面的这个图来解释,假设此时对象锁处于偏向锁状态,然后在内部调用hashcode方法,而此时偏向锁中并没有存储hashcode值的地方,那么就会出现三种情况

【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

1,假设此时还是一个匿名偏向锁,如下面的lock锁,此时是一把匿名偏向锁,随后调用hashcode方法

public static void main(String[] args) throws InterruptedException {
    //开启延迟偏向
    Thread.sleep(5000);
    //延迟4s后才开始加载的对象
    Object lock = new Object();
    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    lock.hashCode();
    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}

接下来查看他的打印日志,有图有真相,从101状态变成了001状态,这就是锁撤销成了无锁状态

【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

2,假设此时是一把有偏向线程的偏向锁,随后定义两个线程,随后也是调用这个hashcode对象

public static void main(String[] args) throws InterruptedException {
    //开启延迟偏向
    Thread.sleep(5000);
    //延迟4s后才开始加载的对象
    Object lock = new Object();
    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    //两个线程竞争,成为轻量级锁
    new Thread(()->{
        synchronized (lock){
            //System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    },"thread1").start();
    new Thread(()->{
        synchronized (lock){
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    },"thread2").start();
    Thread.sleep(500);
    lock.hashCode();
    System.out.println(ClassLayout.parseInstance(lock).toPrintable());

}

其结果很明显,第一个打印出来的是匿名偏向锁,此时还没有线程来拿锁,第二步是直接成为了00轻量级锁,随后轻量级锁释放锁成为001无锁。

【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

3,依旧是有偏向线程id的偏向锁,在一个线程中,休眠一会再调用这个hashcode方法,最后打印日志

public static void main(String[] args) throws InterruptedException {
    //开启延迟偏向
    Thread.sleep(5000);
    //延迟4s后才开始加载的对象
    Object lock = new Object();
    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    //两个线程竞争,成为轻量级锁
    new Thread(()->{
        synchronized (lock){
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
            lock.hashCode();
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    },"thread1").start();
    Thread.sleep(500);
}

这样就实现了从偏向锁101,撤销到重量级锁10了。

【JUC系列-04】精通Synchronized底层的实现原理,JUC系列,java,synchronized,java锁,对象内存布局,synchronized锁升级,synchronized锁优化,synchronized原理

锁的撤销一般是在程序的安全点进行操作,如触发GC时,程序异常时等。

7,jvm对synchronized锁的优化

7.1,偏向锁批量重偏向优化

在markword的偏向锁中,有一个Epoch字段,该字段主要是记录同一个对象偏向锁撤销的次数,在多线程的条件下,如果Epoch存储的值达到一定的阈值的时候,就会触发这个批量重偏向的优化操作,因为偏向锁的撤销是需要花费一定的性能的,而大量线程一直去撤销同一个偏向锁对象,因此这里就做了重偏向的优化

重偏向,简而言之就是复用的意思,原先在一个偏向锁中,其对应指向的线程id是不变的,后面在jvm内部是做了优化的,假设第一个线程里面有50个对象锁存放在list里面,第二个线程还是用list里面的这50个对象,那么当第二个线程撤销重偏向的次数达到20的时候,后面的对象会直接进行重偏向操作,就是复用第一个线程的偏向锁,从而减少偏向锁撤销所带来的性能影响。

主要是jvm会认为当前锁对象是不是重偏向错了,于是会重置锁对象的线程ThreadId

intx BiasedLockingBulkRebiasThreshold   = 20   //默认偏向锁批量重偏向阈值

这个就有点类似于线程池中线程复用的原理,但是偏向锁在重偏向时会有对应的阈值,主要是通过jvm内部优化

7.2,偏向锁批量撤销优化

这个批量撤销相对而言更好理解,因为偏向锁撤销肯定会影响性能,因此也会对这个Epoch的统计做一个阈值处理,当达到40时,JVM就会觉得这个类干脆就不用偏向锁的状态,直接进入无锁状态,从而省去锁撤销锁带来的性能问题。

intx BiasedLockingBulkRevokeThreshold   = 40   //默认偏向锁批量撤销阈值

批量重偏向和批量撤销主要是针对锁的优化,并且偏向锁只能重偏向一次

7.3,重量级锁自旋优化

在这几种锁中,轻量级锁和偏向锁都不存在自旋操作,只有这个重量级锁存在自旋。在自旋之前,如果直接使用阻塞的方式抢锁,那么需要不断的用户态切换到内核态去抢占,那么jvm就直接在用户态通过cas的方式进行一个锁的竞争,在用户态选出获取拿到锁的线程,随后再去调用内核态进行操作,从而避免大量线程阻塞问题。

  • 在jdk6之后,可以通过参数设置来决定是否开启自旋,以及设置自旋的次数。

  • 在jdk7之后,不能对这个自旋的参数就行控制,这个功能交给了jvm底层去自适应。

7.4,锁粗化和锁消除

锁粗化指的是对同一个对象重复加锁,jvm在编译期间会进行优化操作,将多个锁变成一个锁。由于每个append内部都有一个synchronized锁,因此内部会做一个合并,将多个锁拆成一个锁

StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("first").append("second").append("three);

在jvm中,对象有可能并不是垃圾回收器回收的,而是随着入栈出栈被销毁的,这种技术叫逃逸分析。逃逸分析主要有三种情况,一种是标量替换,一种是栈上分配,还有一种是同步省略。这里主要讲的就是同步省略,同步省略又被称为锁消除,指的就是jit即时编译器发现每次调用的方法锁的都不是同一个对象,锁了跟没锁一样,而且效率还更慢,那么就直接会将这把锁给消除。

标量替换和栈上分配可以看本人的jvm的博客https://zhenghuisheng.blog.csdn.net/article/details/129796509

for(int i = 0; i < 100 ; i++){
    Student stu = new Student();
	//发现每次调用该方法锁的根本不是同一个对象,因此会将这个锁消除
	synchronized(stu){
    	System.out.println("helloi stu");
	}
}

8,synchronized锁误区

详情可以查看c++底层源码

1,锁的不可逆指的是轻量级锁到重量级锁是不可逆的,但是也存在轻量级锁到无锁或者重量级锁到无锁的状态

2,不存在无锁到偏向锁的过程,这两把锁相对独立,但是偏向锁可以撤销成无锁

3,轻量级锁中,不存在cas自旋,里面是属于线程交互执行,一旦没拿到锁,则立马升级膨胀,最后拿到monitor对象之后,直接升级成重量级锁

如有转载,请标明出处:https://zhenghuisheng.blog.csdn.net/article/details/132740980文章来源地址https://www.toymoban.com/news/detail-698342.html

到了这里,关于【JUC系列-04】精通Synchronized底层的实现原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 「JUC并发编程」初识CAS锁(概述、底层原理、原子引用、自旋锁、缺点)

    概述 CAS的全称为 Compare-And-Swap ,直译就是 对比交换 。是一条 CPU的原子指令 ,其作用是让CPU 先 进行比较两个值是否相等, 然后 原子地更新某个位置的值。经过调查发现,其实现方式是 基于硬件平台的汇编指令 ,就是说CAS是靠硬件实现的, JVM只是封装了汇编调 用,那些

    2024年02月16日
    浏览(37)
  • Java中的Iterator底层原理实现

    Iterator主要有 两个抽象方法 ,让子类实现。 hasNext ()用来判断还有没有数据可供访问。 next ()方法用于访问集合的下一个数据。 这两个方法不像List的get()那样依赖索引获取数据,也不像Queue的poll方法那样依赖特定规则获取数据。 迭代器的方法将通用性做到了极致, 可以访问

    2023年04月12日
    浏览(22)
  • 【JUC基础】05. Synchronized和ReentrantLock

    前面两篇中分别讲了Synchronized和ReentrantLock。两种方式都能实现同步锁,且也都能解决多线程的并发问题。那么这两个有什么区别呢? 这个也是一个高频的面经题。 什么是可重入锁? 可重入锁,也称为递归锁,是指同一线程在外层方法获取锁的时候,在进入内层方法会自动获

    2024年02月08日
    浏览(64)
  • 多线程JUC 第2季 synchronized锁升级过程

    用锁能够实现数据的安全,但是会带来性能下降。Synchronized是一个重量级锁,锁的升级过程: 无锁-偏向锁-轻量级锁-重量级锁。 高并发时,同步调用应尽量考虑锁的性能损耗,能用无锁数据结构,就不要用锁,能用区块不要用锁住整个方法体;能有对象锁,就不要用类锁。

    2024年02月09日
    浏览(25)
  • Java——》synchronized的原理

    推荐链接:     总结——》【Java】     总结——》【Mysql】     总结——》【Redis】     总结——》【Kafka】     总结——》【Spring】     总结——》【SpringBoot】     总结——》【MyBatis、MyBatis-Plus】     总结——》【Linux】     总结——》【MongoDB】    

    2024年02月09日
    浏览(24)
  • Java LongAdder类介绍、代码示例、底层实现原理及与分段锁的区别

    LongAdder是Java并发包(java.util.concurrent)中的一个类,用于高效地实现多线程环境下的加法操作。 在多线程环境中,如果多个线程同时对同一个变量进行加法操作,会存在竞争条件(race condition)。传统的加法操作使用synchronized或者锁来保证线程安全,但是在高并发情况下

    2024年02月12日
    浏览(25)
  • 剑指JUC原理-8.Java内存模型

    👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家 📕系列专栏:Spring源码、JUC源码 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦 🍂博主正在努力完成2023计划中:源码溯源,一探究竟 📝联系方式:nhs19990716,加我进群

    2024年02月06日
    浏览(33)
  • JUC并发编程-线程和进程、Synchronized 和 Lock、生产者和消费者问题

    源码 + 官方文档 面试高频问! java.util 工具包、包、分类 业务:普通的线程代码 Thread Runnable Runnable 没有返回值、效率相比入 Callable 相对较低! 线程、进程,如果不能使用一句话说出来的技术,不扎实! 进程:一个程序,QQ.exe Music.exe 程序的集合; 一个进程往往可以包含多

    2024年01月20日
    浏览(39)
  • Java并发编程挑战与解决方案:上下文切换、死锁、资源限制及底层实现原理

    深入探讨Java并发编程中的挑战,包括上下文切换、死锁、资源限制,并介绍解决方案如减少上下文切换、避免死锁等。了解Java并发机制的底层实现原理和线程间通信方法。

    2024年02月01日
    浏览(32)
  • Java 八股文面试过程系列之synchronized关键字

    本文通过一场虚拟的面试对话,详细解析了Java中的synchronized关键字。从基本用法到底层实现原理,再到性能优化策略,全面提升您对同步机制的理解。

    2024年02月07日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包