当前位置: 首页 > 后端技术 > Java

浅谈自旋锁和 JVM 对锁的优化

时间:2023-04-01 23:33:14 Java

说说自旋锁和JVM对锁的优化why?好处阻塞和唤醒线程都需要很高的开销。如果同步代码块中的内容不复杂,切换线程的开销可能会大于实际业务代码执行的开销。在很多场景下,我们同步代码块的内容可能并不多,所以需要的执行时间也很短。如果我们只是为了这个时候切换线程状态,那么最好不要让线程切换状态,而是让它自旋尝试获取锁,等待其他线程释放锁。有时候我只需要等一会儿,这样可以避免上下文切换的开销,提高效率。用一句话概括自旋锁的好处,就是自旋锁使用循环不断尝试获取锁,使线程一直处于Runnable状态,节省了线程状态切换带来的开销。AtomicLong实现getAndIncrement方法publicfinallonggetAndIncrement(){returnunsafe.getAndAddLong(this,valueOffset,1L);}复制代码publicfinallonggetAndAddLong(Objecto,longoffset,longdelta){longv;做{v=getLongVolatile(o,offset);//如果修改过程遇到其他线程竞争,修改不成功,会一直死循环,直到修改成功}while(!compareAndSwapLong(o,offset,v,v+delta));返回v;}复制代码实验包com.reflect;importjava.util.concurrent.atomic.AtomicReference;classReentrantSpinLock{privateAtomicReferenceowner=newAtomicReference<>();privateintcount=0;publicvoidlock(){Threadt=Thread.currentThread();if(t==owner.get()){++count;返回;}while(!owner.compareAndSet(null,t)){System.out.println("}}publicvoidunlock(){Threadt=Thread.currentThread();if(t==owner.get()){如果(计数>0){--计数;}else{owner.set(null);}}}publicstaticvoidmain(String[]args){ReentrantSpinLockspinLock=newReentrantSpinLock();Runnablerunnable=newRunnable(){@Overridepublicvoidrun(){System.out.println(Thread.currentThread().getName()+"开始尝试获取自旋锁");自旋锁.lock();try{System.out.println(Thread.currentThread().getName()+"获取自旋锁");线程.睡眠(4000);}catch(InterruptedExceptione){e.printStackTrace();}最后{spinLock.unlock();System.out.println(Thread.currentThread().getName()+"自旋锁释放");}}};线程thread1=newThread(runnable);线程thread2=new线程(可运行);thread1.start();thread2.start();}}复制代码很多“自旋”,说明自旋过程中CPU还在不停的运行,避免线程切换开销,同时带来新的开销:不断尝试获取锁。如果不能释放锁,那么这种尝试是没有用的,浪费处理器资源。也就是说,自旋锁一开始的开销比线程要低。切换,但是随着时间的增加,这个开销甚至会超过后期线程切换的开销,得不偿失。适用于并发不是特别高,临界区比较短的场景。使用避免线程切换来提高效率。如果临界区很大,线程拿到锁很久之后才释放,所以自旋会一直占用CPU却拿不到锁,很浪费资源。JVM对锁做了哪些优化?与JDK1.5相比,JDK1.6中的HotSopt虚拟机对synchronized内置锁的性能进行了优化,包括自适应自旋、锁淘汰、锁粗化、偏向锁、轻量级锁等,通过这些优化措施后,性能得到了提升同步锁的数量得到了很大的改进。下面我们介绍这些具体的优化。Adaptivespinlock自适应自旋锁是在JDK1.6中引入的,用来解决长自旋的问题。自适应意味着自旋时间不再是固定的,而是会根据最近一次自旋尝试的成功率和失败率、当前锁所有者的状态等多种因素来确定。自旋的持续时间是可变的,自旋锁变得“聪明”。例如,如果最近一次尝试自旋获取某个锁成功,那么下一次可能会继续使用自旋,并且可能允许自旋更长的时间;但是如果最近一次获取某个锁的自旋失败了,那么可能会省略自旋的过程,以减少无用的自旋,提高效率。锁消除publicclassPerson{privateStringname;privateintage;publicPerson(StringpersonName,intpersonAge){name=personName;年龄=人物年龄;}publicPerson(Personp){this(p.getName(),}publicStringgetName(){returnname;}publicintgetAge(){returnage;}}classEmployee{privatePersonperson;publicPersongetPerson(){returnnewPerson(person);}publicvoidprintEmployeeDetail(Employeeemp){Personperson=emp.getPerson();System.out.println("员工姓名:"+person.getName()+";age:"+person.getAge());}}复制代码在这段代码中,我们看到下面Employee类中的getPerson()方法,它使用了类中的person对象并创建了一个新的person和它对象一样的属性,目的是防止方法调用者修改原来的person对象。但是在这个例子中,其实不需要创建一个新的对象,因为我们的printEmployeeDetail()方法并没有做任何修改这个对象,它只是打印。在这种情况下,我们实际上可以直接打印原始人对象。而不是创建一个新的。如果编译器可以确定原来的person对象不会被修改,它可能会优化并消除创建新person的过程。根据这个思路,我们来举一个锁消除的例子。经过逃逸分析,如果发现有些对象不能被其他线程访问,那么就可以认为是栈上的数据。栈上的数据由于只有本线程可以访问,所以自然是线程安全的,所以不需要加锁,所以这样的锁会自动解除。比如我们StringBuffer的append方法如下:@OverridepublicsynchronizedStringBufferappend(Objectobj){toStringCache=null;super.append(String.valueOf(obj));returnthis;},这个方法是synchronized修饰的方法,因为它可能同时被多个线程使用。但在大多数情况下,它只会在一个线程中使用。如果编译器可以判断StringBuffer对象只会在一个线程中使用,那么就说明它一定是线程安全的,那么我们的编译器就会做优化,去掉对应的synchronized,省去加锁和解锁的操作,这样以提高整体效率。锁粗化释放锁,然后什么都不做,然后重新获取锁publicvoidlockCoarsening(){synchronized(this){}synchronized(this){}synchronized(this){}}release和重新获取锁是完全没有必要的。如果我们扩大同步区,也就是只在开头加锁,在结尾直接解锁,就可以去掉中间这些无意义的解锁和加锁过程。相当于把几个同步块合并成一个更大的同步块。这样做的好处是线程在执行这些代码的时候,不需要频繁的申请和释放锁,减少了性能开销。但是,我们这样做也有一个副作用,那就是我们把同步区域变大了。如果我们在循环中也这样做,如代码所示:for(inti=0;i<1000;i++){synchronized(this){}}复制代码即,我们从第一个开始loop扩大同步区并持有锁直到最后一个循环结束,然后同步代码块释放锁,这会导致其他线程长时间无法获得锁。所以这里的锁粗化不适合循环场景,只适合非循环场景。锁粗化功能默认开启。使用-XX:-EliminateLocks禁用此功能。偏向锁/轻量级锁/重量级锁这三种锁,特指同步锁的状态。通过对象头中的markword来表示锁的状态Biasedlocks对于偏向锁,其思路是如果从头到尾都没有竞争这把锁,那么其实就不需要加锁了,只需标记它。一个对象初始化后,如果没有线程去获取它的锁,它就是可偏向的。当第一个线程访问它并试图获取锁时,它会记录这个线程。如果后面尝试获取锁的线程是这个偏向锁的拥有者,那么它可以直接获取锁,开销很小。轻量级锁JVM的开发者发现,在很多情况下,synchronized中的代码块是由多个线程交替执行的,也就是说,并没有真正的竞争,或者只是短暂的锁竞争。CAS是可以解决的。在这种情况下,不需要重量级锁。轻量级锁是指当锁原本是偏向锁时,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会尝试通过自旋来获取锁。阻塞式重量级锁是利用操作系统的同步机制实现的,所以开销比较大。当多个线程直接实际竞争,锁竞争时间比较长时,此时偏向锁和轻量级锁都不能满足需求,锁会膨胀为重量级锁。重量级锁会导致其他申请了锁却拿不到锁的线程进入阻塞状态。锁升级有利于最好的锁性能并避免CAS操作。但是轻量级锁使用自旋和CAS来避免重量级锁造成的线程阻塞和唤醒,性能一般。重量级锁会阻塞得不到锁的线程,性能最差。默认情况下,JVM会优先使用偏向锁,必要时逐步升级,大大提升锁的性能完整附件:点此下载附件