当前位置: 首页 > 科技观察

采访者:你听说过Synchronized吗?说说吧_0

时间:2023-03-23 11:16:39 科技观察

前言相信很多同学都熟悉synchronized的用法,之前也给大家讲解过它的用法。本文主要带你深入了解它。你也可以试着自己总结一下。这也是面试中经常被问到的。简单的回答它的基本使用不会让面试官吃惊~。synchronized的引入,字面上翻译就是同步的意思,所以也叫同步锁。我们通常会在一个方法或者一段代码中加一个Synchronized锁来解决多线程中并发带来的问题。它也是最常用的。最简单的方法。在Java中,锁基本上是基于对象的,所以也叫对象锁。一个类通常只有一个类对象和n个实例对象,它们共享类对象,我们有时会对类对象加锁,所以也叫类对象锁。这里大家要注意的是对象需要是非空对象,我们通常称之为对象监视器。重量级锁在JDK1.5之前是重量级锁,我们通常用它来保证线程同步。在1.5中还提供了一个Lock接口来实现同步锁的功能。我们只需要显式地获取和释放锁。有什么意义?在1.5中,Synchronized依赖于操作系统底层的MutexLock实现。每次释放和获取锁都会引起用户态和内核态的切换,从而增加系统性能的开销。一般情况下,锁竞争会很激烈,性能会很差,所以被称为重量级锁,所以大家往往会选择Lock锁。锁优化,但是Synchronized这么简单好用,而且官方自带,怎么舍得下呢?所以在1.6之后引入了大量的锁优化,比如自旋锁、轻量级锁、偏向锁等等,下面我们一一来看。同步实现原理在了解解锁优化之前,我们先来看看它的实现原理。首先来看同步块,因为是关键字,看不到源码实现,只能反编译,使用javap-v**.class。publicstaticvoidmain(String[]args){synchronized(Demo.class){System.out.println("hello");}}publicstaticvoidmain(java.lang.String[]);描述符:([Ljava/lang/String;)Vflags:ACC_PUBLIC,ACC_STATICCode:stack=2,locals=3,args_size=10:ldc#2//classcom/thread/base/Demo2:dup3:astore_14:monitorenter5:getstatic#3//字段java/lang/System.out:Ljava/io/PrintStream;8:ldc#4//Stringhello10:invokevirtual#5//Methodjava/io/PrintStream.println:(Ljava/lang/String;)V13:aload_114:monitorexit15:goto2318:astore_219:aload_120:monitorexit21:aload_222:throw23:return我们重点关注monitorenter和monitorexit,那么它们是什么意思呢?monitorenter,如果当前monitor入口号为0,线程它会进入monitor,并且进入次数+1,那么线程就是monitor的所有者(owner)。如果线程已经是monitor的所有者重新进入,则进入次数会再次+1。也就是可重入。monitorexit,执行monitorexit的线程必须是monitor的owner。指令执行后,监视器中的条目数减1,如果条目数减1后为0,则线程退出监视器。然后其他被阻塞的线程可以尝试获取监视器的所有权。该指令出现两次,第一次是同步正常退出释放锁;第二次是异步退出释放锁。我们来看看修改实例方法的性能:classDemo{publicsynchronizedvoidhello(){System.out.println("hello");}}publicsynchronizedvoidhello();descriptor:()Vflags:ACC_PUBLIC,ACC_SYNCHRONIZEDCode:stack=2,locals=1,args_size=10:getstatic#2//字段java/lang/System.out:Ljava/io/PrintStream;3:ldc#3//Stringhello5:invokevirtual#4//方法java/io/PrintStream.println:(Ljava/lang/String;)V8:returnLineNumberTable:line25:0line26:8LocalVariableTable:StartLengthSlotNameSignature090thisLcom/thread/base/Demo;}我们关注ACC_SYNCHRONIZED。它的作用是这个方法一旦执行,会先判断是否有flag。如果是这样,它将首先尝试获取监视器。只有方法执行成功后才能执行该方法。方法执行完后,就会被释放。监视器。在方法执行期间,没有其他线程可以获取同一个监视器。归根结底还是对monitor对象的竞争,只是隐式实现了同步方法。synchronized在JVM中的实现是基于进入和退出monitor,底层是通过成对的MonitorEnter和MonitorExit指令来实现的。有了上面的认识,我们再来看看锁的优化。SynchronizedAdaptivespinlock中的锁优化Spinlock,我们之前讲FutureTask源码的时候,有一个内部方法awaitDone(),给大家介绍过,就是基于它实现的。今天小编就为大家总结一下。其目的是避免在阻塞和唤醒之间切换。未获取到锁时不进入阻塞,不断循环检查锁是否释放。然而,它也有缺点。一般来说,一个线程占用锁的时间比较短,但是如果占用时间比较长呢?这样会占用大量的cpu时间,从而导致性能不佳,所以1.6引入了自适应自旋锁来满足这样的场景。那么什么是自适应自旋锁呢?自旋的次数不是固定的,而是由同一把锁上一次自旋的时间和锁拥有者的状态决定的。如果这次旋转成功,那么下一次旋转成功的可能性很大,所以允许旋转的次数会更多。反之,如果很少有线程能够自旋成功,很可能下一次也会失败。旋转次数较少。这样可以更好的利用系统资源。锁消除锁消除是一种锁优化策略。这个优化更彻底。当JVM编译时,它会扫描运行上下文以移除不太可能发生共享资源竞争的锁。这种优化策略可以消除不必要的锁,消除获取锁的时间。锁粗化如果一系列连续的加锁和解锁操作可能会造成不必要的性能损失,所以引入锁粗化语言的概念。意思是将多个连续的加锁和解锁操作连在一起,展开成一把更大的锁,这个应该很好理解。偏向锁偏向锁是JDK1.6引入的。它解决的场景是什么?我们大多数人在多线程场景下使用锁来解决问题,但有时候一个线程也会有这样的问题。偏向锁是在单线程中执行代码块时使用的机制。锁的争用实际上是对Monitor对象的争用,每个对象都有一个对象头,由MarkWord和Klass指针组成。一旦某个线程持有了锁对象,并且flag变为1,就会进入偏向模式。同时线程的ID会记录在对象的MarkWord中。当同一个线程再次进入时,将不再执行同步操作,大大减少了获取锁的时间,从而提高了性能。轻量级锁我们上面提到的偏向锁,在多线程的情况下,如果偏向锁失效,就会升级为轻量级锁,而MarkWord的结构也会变成轻量级锁结构。在执行同步代码块之前,JVM会在线程的栈帧中创建一个锁记录(LockRecord),并将MarkWord副本复制到锁记录中。然后尝试通过CAS操作将MarkWord中锁记录的指针指向创建的锁记录。如果成功则说明获取锁状态成功,如果失败则进入自旋获取锁状态。如果自旋锁失效了,它就会升级为重量级锁,也就是我们之前说的,它会阻塞线程,等待它被唤醒。重量级锁也称为悲观锁。升级到这种情况后,锁的竞争更加激烈,占用时间更长。为了减少CPU消耗,线程会被阻塞,进入阻塞队列。synchronized就是通过锁升级策略来适应不同的场景,所以现在synchronized优化的很好,这也是我们项目中经常使用它的原因。结语本节内容比较多,请大家好好理解,尤其是锁的升级攻略。在本节中,我们提到了Lock锁。下一节,我们将带大家深入了解Java的Lock。