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

面试官很喜欢问的Synchronized锁

时间:2023-03-22 12:53:18 科技观察

面试中难免会遇到同步锁,所以如何从面试官的角度完美应对棘手的问题就显得尤为重要。阿爸阿爸以身作则,为大家贡献面试经验。回家等着通知面试官:你要不要理解synchronized?告诉我怎么回事儿。阿爸阿爸:嗯,我明白synchronized是Java中的关键字。它的功能主要用于同步。一般称为同步锁。它通常可以用在方法和代码块中。AbbaAbba:当用在方法上时,它看起来像一个锁定的对象。如果用在代码块上,如果对象被修改,则对象被锁定。如果类被修改,则该类的所有对象都被锁定。面试官:对,synchronized可以用在构造方法上吗?你了解锁定过程吗?阿爸阿爸:嗯……这个……不是很清楚。面试官:能谈谈锁的优化吗?阿爸阿爸:嗯?锁有什么优化吗?不是很清楚。面试官:好的,今天就在这里见面,你回去等我通知你??阿爸阿爸:好的。现场offer面试官:synchronized你应该懂吧?告诉我。阿爸阿爸:嗯,我明白synchronized是Java中的关键字。它主要用于同步。一般称为同步锁。它可以用在实例方法、静态方法和代码块中。主要是维护一个状态,也就是说在同一时刻,只有一个线程可以访问synchronized修饰的方法或者代码块。AbbaAbba:用在实例方法上时,调用该方法的对象被锁定。当用于静态方法时,当前类的所有对象都被锁定。在代码块上使用时,如果对象被修改,则对象被锁定。如果修改的是一个类,则该类的所有对象都被锁定。(画图加强记忆)//synchronized用于静态方法publicsynchronizedstaticvoidtest01(){}//synchronized用于实例方法publicsynchronizedvoidtest02(){}//synchronized用于修改对象publicvoidtest03(){synchronized(this){}}//synchronized用于修饰当前类publicvoidtest04(){synchronized(TestSyn.class){}}面试官:对,synchronized可以用在构造方法中吗?你了解锁定过程吗?阿爸阿爸:synchronized不能直接加在构造函数中,但是可以在构造函数中使用synchronized代码块。阿巴阿巴:加锁过程涉及JDK版本问题。在JDK1.5及之前,synchronized关键字编译后,会在同步块前后形成monitorenter和monitorexit两条字节码指令。monitorenter命令执行时,对象锁(这个对象锁包括对象实例或Class对象),如果获取到的对象没有被锁,或者当前线程已经获取了该对象的锁(synchronized是可重入锁,即是已经获得锁的线程可以再次获得锁而不需要再次同步),然后将锁计数器加1,abaaba:同理,如果执行monitorexit指令,锁计数器减1,这样当计数器的值为0时,锁被释放。如果线程获取对象锁失败,就会阻塞等待,直到锁被释放。阿爸阿爸:synchronized重量级锁的实现是用C++代码实现的,有一个ObjectMonitor队列。代码中的重要属性如下所示。ObjectMonitor(){_recursions=0;//重入次数_owner=NULL;//指向持有ObjectMonitor对象的线程_WaitSet=NULL;//调用wait后,线程会加入到_WaitSet中,WaitSet是第一个节点_cxq=NULL;//多线程竞争锁进入时的单向链表_EntryList=NULL;//等待获取锁的线程会加入到链表中,_EntryList为第一个节点}ababababa:下面是一个线程流程图表。如上图0当多个线程同时竞争时,这些线程会被放入EntryList队列,此时线程处于阻塞状态1当线程获得对象的监听器时,可以进入运行状态,此时ObjectMonitor对象的/_owner指向当前线程,_count加1表示当前对象锁被线程获取。2当处于运行状态的线程调用wait()方法时,当前线程释放monitor对象,进入等待状态。ObjectMonitor对象的/_owner变为null,_count减1,线程进入_WaitSet队列。3直到有线程调用notify()方法唤醒线程,线程进入_EntryList队列,竞争锁后进入_Owner区域。4如果当前线程执行完毕,monitor对象也被释放,ObjectMonitor对象的/_owner变为null,_count减1。ababababa:JDK1.5之前的synchronized情况下,需要切换各个锁用户态(运行用户程序)到内核态(运行操作系统程序、操作硬件等),对系统资源的消耗是巨大的,所以JDK1.6版本对synchronized进行了优化,引入了以下概念:自旋锁,自适应自旋锁,淘汰锁、粗化、偏向锁、轻量锁、重锁。面试官:我想多听听Ba:引入自旋锁的主要原因是,在大多数情况下,线程占用锁的时间不会持续很长时间。如果有其他线程竞争,竞争失败的线程会被挂起,然后再恢复。显然,这种消耗是巨大的,所以采用了一种“观望”的方法,即让线程等待一段时间,看这段时间持有锁的线程是否会释放锁,这就是自旋.贝巴贝巴:但是,旋转并不能完全解决问题。需要考虑拥有锁的线程对锁的占用。如果占用时间过长,会导致自旋锁一直在做无用的可选操作,从而消耗CPU。因此,为旋转次数设置一个阈值就显得尤为重要。这个阈值也需要设置一个合适的值,既不能太高也不能太低。AbbaAbba:自适应自旋锁的诞生。自适应是指自旋的次数或时间不再固定,而是由上一次对同一个锁自旋的次数或时间决定:如果是在同一个锁对象上,则自旋等待刚刚成功如果锁已经获取到并且持有锁的线程正在运行,那么虚拟机就会认为这次自旋很可能再次成功。从某种意义上说,它会让自旋等待相对更长的时间。反之,如果自旋很少成功获取,则可以减少自旋时间或以后获取锁的次数,避免浪费CPU资源。阿爸阿爸:锁淘汰就是说在synchronized代码中,发现无论如何都不会发生锁竞争,那么就可以淘汰锁。这种分析称为逃逸分析。如果有一段同步代码不会被其他线程访问,那么这个同步就没有意义了。ababaababa:锁的粗化是指如果一段代码不停地加锁和解锁一个对象,比如在循环体中进行加锁和解锁操作,即使没有线程竞争,巨大的这种情况下,你可以考虑扩大锁的范围。这个过程是粗化的。阿巴阿巴:在线程安全的情况下,偏向锁不一定有线程竞争,也就是可能没有互斥。如果一个锁对象没有其他线程竞争,那么JVM会默认它为偏向锁。默认情况下,只有第一个申请锁的线程会使用锁,其他线程不会竞争锁。所以只需要在MarkWord中记录CAS中的owner即可。如果记录更新成功,则偏向锁获取成功。记录锁状态是偏向锁。以后等于owner的当前线程可以零成本直接获取锁;如果此时有其他线程在竞争,偏向锁就会扩展为轻量级锁。阿巴阿巴:使用轻量级锁时,不需要申请互斥量。只需要将MarkWord中部分字节的CAS更新到线程栈中的LockRecord中即可。如果更新成功,则轻量级锁获取成功。记录锁状态是一个轻量级的锁;一个轻量级的锁适合两个线程交替运行,但是没有实质性的竞争。如果发生锁竞争,轻量级锁将扩展为重量级锁。面试官:你很好,不错,可以回去准备下一次面试了??。阿爸阿爸:好的。本期采访到此结束。下一期阿爸阿爸会被问及更难的物件和锁。期待她完美的表现!