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

面试题:再说说Synchronized的实现原理!

时间:2023-03-19 20:43:16 科技观察

前言线程安全是并发编程中的一个重要问题。造成线程安全问题的主要原因有两个,一是共享数据(也称为临界资源)的存在,二是多个线程共同操作共享数据的存在。为了解决这个问题,我们可能需要这样的解决方案。当有多个线程操作共享数据时,需要保证只有一个线程同时操作共享数据,其他线程必须等到该线程处理完数据后才能进行。在Java中,关键字Synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块(主要是对方法或代码块中共享数据的操作)。下面我们一起来探讨一下Synchronized的基本用法和实现机制。面试题来自:社招一年半的分享(包括阿里美团今日头条京东滴滴)用法两种锁:对象锁:包括方法锁(默认锁对象是this的当前实例对象)和同步代码块锁(自己指定锁对象)。类锁:指Synchronized修饰的静态方法或指定的锁作为Class对象。当一个线程试图访问同步代码块时,它必须先获取锁,并在退出或抛出异常时释放锁。普通方法加锁时,加锁的对象是this;锁定静态方法时,锁定的对象是类对象;锁定代码块时,可以指定特定对象作为锁定。代码示例如下:publicclassSynchronizedTest{/***修饰的静态方法,相当于下面注释的方法*/publicsynchronizedstaticvoidtest1(){System.out.println("Moonwithflyingfish");}//publicstaticvoidtest1(){//synchronized(SynchronizedTest.class){//System.out.println("飞鱼伴月");/////***修改实例方法,相当于下面注释的方法*/publicsynchronizedvoidtest2(){System.out.println("MoonwithFlyingFish");}//publicvoidtest2(){//synchronized(this){//System.out.println("MoonwithFlyingFish");/////***修改代码块*/publicvoidtest3(){synchronized(this){System.out.println("MoonwithFlyingFish");}}}多线程访问同步方法的几种情况:两个线程访问同步方法同时一个对象。由于同步方法lock使用的是this对象锁,同一个对象只有一个this锁,两个线程中只有一个可以同时持有锁,所以方法会串行运行。两个线程访问两个对象的同步方法。由于两个对象的this锁互不影响,Synchronized不会起作用,所以方法会并行运行。两个线程访问Synchronized的静态方法。Synchronized修饰的静态方法获取当前类模板对象的锁。只有一把锁。无论访问该类对象的多少个方法,都会依次执行。同时访问同步和非同步方法非同步方法不受影响。访问同一对象的不同普通同步方法。由于只有一个this对象锁,不同线程访问的多个公共同步方法会串行运行。同时访问静态Synchronized和非静态Synchronized方法。静态Synchronized方法的锁是类对象的锁,非静态Synchronized方法的锁是this的锁。它们不是同一个锁,所以它们会并行运行。使用优化当你使用synchronized关键字的时候,你可能经常这样写:synchronized(this){...}它的作用域是当前对象,锁是当前对象。谁拿到锁就可以运行受控代码。当有明确的对象作为锁时,可以这样写,但是当没有明确的对象作为锁,只想同步一段代码时,可以创建一个专门的变量(对象)来充当锁:publicclassDemo{privatefinalObjectlock=newObject();publicvoidmethonA(){synchronized(lock){...}}}这样写是没有问题的。但是使用newObject()作为锁对象是不是最好的选择呢?在StackOverFlow上看到这样一篇文章:object-vs-byte0-as-lock大意是说用newbyte[0]作为锁对象比较好。它将减少字节码操作的数量。publicclassDemo{privatefinalbyte[]lock=newbyte[0];}具体细节可以看这篇文章提供思路!实现原理是因为Synchronized锁住了对象。在讲解原理之前,先介绍一下对象结构。知识。在HotSpot虚拟机中,对象在内存中的布局可以分为三个区域:对象头、实例数据和对齐填充。对象头对象头包括两部分信息:运行时数据MarkWord和类型指针。如果对象是数组对象,则对象头占用3个字(Word)(需要记录数组的长度)。如果对象是非数组对象,对象头占用2个字宽(1word=2Byte=16bit),对象头的类型指针指向对象的类元数据,虚拟机通过它可以判断是哪个类该对象是一个实例。MarkWord用于存储对象本身的运行时数据,比如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,是关键实现轻量级锁和偏向锁。这部分数据的长度在32位和64位虚拟机中分别为32位和64位(不考虑启用压缩指针的场景)。Synchronizedlock对象存储在MarkWord中,MarkWord的布局如下:32位虚拟机实例数据Instancedata是程序代码中定义的各种类型的字段,包括HotSpot自动继承自父类的alignmentpadding内存管理要求对象的起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍,而对象头中的数据恰好是8的整数倍,所以当实例数据不够8字节的整数倍时,需要通过alignmentpadding来完成,也就是说每次分配的内存大小必须是8的倍数。如果对象头的值+instancedata不是8的倍数,则重新计算一个较大的值进行分配。底层实现如下代码,在命令行执行javac,然后执行javap-v-p,可以看到它的具体字节码。可以看出,在字节码的实施例中,只是给方法增加了一个标志:ACC_SYNCHRONIZED。synchronizedvoidsyncMethod(){System.out.println("syncMethod");}synchronizedvoidsyncMethod();描述符:()Vflags:ACC_SYNCHRONIZEDCode:stack=2,locals=1,args_size=10:getstatic#43:ldc#55:invokevirtual#68:return我们再来看一下同步代码块的字节码。可以看出字节码是由monitorenter和monitorexit两条指令控制的。voidsyncBlock(){synchronized(Test.class){}}voidsyncBlock();descriptor:()Vflags:Code:stack=2,locals=3,args_size=10:ldc#22:dup3:astore_14:monitorenter5:aload_16:monitorexit//两个monitorexit分别是正常退出和异常退出的场景7:goto1510:astore_211:aload_112:monitorexit13:aload_214:athrow15:returnExceptiontable:fromtotargettype5710any101310any这两个虽然显示效果不一样,但是都是通过monitor同步的。其中,在Java虚拟机(HotSpot)中,Monitor由ObjectMonitor实现,其主要数据结构如下(位于HotSpot虚拟机源码的ObjectMonitor.hpp文件中,用C++实现):ObjectMonitor(){_header=NULL;_count=0;//用来记录对象被线程锁定的次数,这也说明synchronized是可重入的_waiters=0,_recursions=0;_object=NULL;_owner=NULL;//指向持有ObjectMonitor对象的线程_WaitSet=NULL;//等待状态的线程会加入到_WaitSet中,调用wait方法后会进入这里_WaitSetLock=0;_Responsible=NULL;_succ=NULL;_cxq=NULL;FreeNext=NULL;_EntryList=NULL;//处于等待锁块状态的线程会加入list_SpinFreq=0;_SpinClock=0;OwnerIsThread=0;}每个Java对象在JVM对等对象头部保存锁状态,指向ObjectMonitor。ObjectMonitor保存当前持有锁的线程引用,EntryList保存当前等待获取锁的线程,WaitSet保存等待线程。还有一个计数器计数。每当线程获取监视器锁时,计数器为+1。当线程重新进入锁时,计数器会+1。当计数器不为0时,其他试图获取监视器锁的线程将被存储在EntryList中并被阻塞。当持有锁的线程释放监视器锁时,计数器为-1。当计数器归0时,EntryList中的所有线程都会尝试获取锁,但只有一个线程会成功,不成功的线程仍然保存在EntryList中。详细过程:加锁时,即遇到Synchronized关键字时,线程会先进入monitor的_EntryList队列阻塞等待。如果monitor的_owner为空,则从队列中移除,分配给_owner。如果程序中调用了wait()方法,则线程进入_WaitSet队列。我们都知道wait方法会释放monitor锁,也就是将_owner赋值为null,进入_WaitSet队列阻塞等待。这时候_EntryList中的其他线程就可以获取锁了。当程序中的其他线程调用notify/notifyAll方法时,_WaitSet中的一个线程会被唤醒,这个线程会再次尝试获取监听锁。如果成功,您将成为显示器的所有者。当程序遇到Synchronized关键字的范围结束时,它会将监视器的所有者设置为null并退出。JavaObjects和Monitor关联锁优化与JDK1.5相比,JDK1.6中的HotSopt虚拟机对Synchronized内置锁的性能进行了优化,包括自适应自旋、锁淘汰、锁粗化、偏向锁、轻量级锁等。量化锁等AdaptivespinlockAdaptivespinlock是在JDK1.6中引入的,用来解决长自旋的问题。例如,如果最近一次尝试自旋获取某个锁成功,那么下一次可能会继续使用自旋,可能会允许更长时间的自旋;但是如果最近一次获取某个锁的自旋失败了,那么可能会省略自旋的过程,以减少无用的自旋,提高效率。经过锁排除和逃逸分析,如果发现有些对象不能被其他线程访问,那么就可以认为是栈上的数据。由于栈上的数据只能被本线程访问,自然是线程安全的,所以不需要加锁,所以这样的锁会自动解除。锁粗化从逻辑上讲,同步块的范围应该越小越好,只在共享数据的实际范围内进行同步。这样做的目的是尽量减少需要同步的操作数,缩短阻塞时间。如果存在锁竞争,等待锁的线程也能尽快拿到锁。但是加锁和解锁也会消耗资源。如果有一系列连续的加锁和解锁操作,可能会造成不必要的性能损失。锁粗化是将多个连续的加锁和解锁操作连接在一起,扩展成一个更大的锁,避免频繁的加锁和解锁操作。偏向锁/轻量级锁/重量级锁JVM默认会优先使用偏向锁,必要时逐步升级,大大提高了锁的性能。锁升级锁状态有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。但是锁升级是单向的,也就是说只能从低升级到高,不会出现锁降级。从JDK1.6开始,偏向锁默认开启,可以通过-XX:-UseBiasedLocking禁用偏向锁。当只有一个线程使用锁时,偏向锁可以保证更高的效率。具体过程如下:当第一个线程第一次访问同步块时,会先检查对象头MarkWord中的标志位Tag是否为01,从而判断对象锁是否在锁中-此时处于自由状态或偏向锁。状态。一旦线程获得了锁,它就会将自己的线程ID写入MarkWord。在其他线程获得锁之前,锁处于偏向锁状态。当下一个线程参与偏向锁竞争时,会先判断MarkWord中保存的线程ID是否与本线程ID相等。如果不是,则立即撤销偏向锁并升级为轻量级锁。轻量级锁当锁处于轻量级锁状态时,已经不能简单的通过比较标志位Tag的值来判断了。每次获取锁时,都需要自旋。当然自旋也是针对没有锁竞争的场景。例如,一个线程运行完毕后,另一个线程获取了锁;但是如果自旋失败一定次数,锁就会膨胀成重量级锁。重量级锁重量级锁,这种情况下,线程会挂起,进入操作系统的内核态,等待操作系统的调度,再映射回用户态。系统调用是昂贵的,因此得名重量级锁。如果系统的共享变量竞争激烈,锁会迅速扩展为重量级锁,这些优化将形同虚设。如果并发很严重,可以通过参数-XX:-UseBiasedLocking禁用偏向锁。理论上会有一定的性能提升,但在实践中并不确定。面试常见问题Synchronized和Lock的区别:Synchronized属于JVM层面,底层由monitorenter和monitorexit两条指令实现。锁是API级别的东西。JUC提供的具体类Synchronized不需要用户手动释放锁。Synchronized代码执行完后会自动让线程释放持有的锁。Lock需要使用try-finally方式手动释放锁。synchronized是不可中断的,除非抛出异常或者程序正常退出。锁定可以被中断。使用lockInterruptibly并调用中断方法中断Synchronized是非公平锁。Lock默认是非公平锁,但是可以通过构造函数传入一个boolean类型的值来改变是否是公平锁。lock是否可以绑定多个condition,Synchronized没有condition语句,要么唤醒所有线程,要么随机唤醒一个线程,Lock可以使用condition唤醒需要分组唤醒的线程,实现精准唤醒。同步锁在同一时间只能由一个线程拥有,但是Lock锁没有这个限制。比如读写锁中的读锁可以同时被多个线程拥有。是的,但是Synchronized在性能上并不能起到什么作用:Synchronized在Java5及之前的性能比较低,但是在Java6之后,JDK对Synchronized做了很多优化,比如自适应自旋,锁消除,锁粗化,轻量级量化锁、偏向锁等。本文转载自微信公众号“月亮与飞鱼”,可通过以下二维码关注。转载本文请联系月版飞语公众号。