说说自旋锁和JVM对锁的优化尝试。为什么?好处阻塞和唤醒线程都需要很高的开销。如果同步代码块中的内容不复杂,切换线程的开销可能会大于实际业务代码执行的开销。在很多场景下,我们同步代码块的内容可能并不多,所以需要的执行时间也很短。如果我们只是为了这个时候切换线程状态,那么最好不要让线程切换状态,而是让它自旋尝试获取锁,等待其他线程释放锁。有时候我只需要等一会儿,这样可以避免上下文切换的开销,提高效率。用一句话概括自旋锁的好处,就是自旋锁使用循环不断尝试获取锁,使线程一直处于Runnable状态,节省了线程状态切换带来的开销。AtomicLong实现getAndIncrement方法:做{v=getLongVolatile(o,offset);//如果修改过程遇到其他线程竞争,修改不成功,会一直死循环,直到修改成功}while(!compareAndSwapLong(o,offset,v,v+delta));returnv;}实验包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()){if(count>0){--count;}else{owner.set(null);}}}公共站ticvoidmain(String[]args){ReentrantSpinLockspinLock=newReentrantSpinLock();Runnablerunnable=newRunnable(){@Overridepublicvoidrun(){System.out.println(Thread.currentThread().getName()+"开始尝试获取自旋锁");自旋锁.lock();try{System.out.println(Thread.currentThread().getName()+"acquiredaspinlock");线程.睡眠(4000);}catch(InterruptedExceptione){e.printStackTrace();}最后{spinLock.unlock();System.out.println(Thread.currentThread().getName()+"自旋锁释放");}}};线程thread1=newThread(runnable);线程thread2=newThread(runnable);thread1.start();thread2.start();}}很多“自旋”,说明自旋过程中CPU还在运行操作上的缺点虽然避免了线程切换的开销,但是在避免线程切换开销的同时又带来了新的开销:不断尝试获取锁,如果不能释放锁,那么这种尝试是没有用的,浪费处理器资源,也就是说自旋锁的开销一开始比线程切换的开销要低,但是随着时间的增加,这个开销甚至会超过后期线程切换的开销,得不偿失并发不是特别高,临界区比较短的应用场景,避免线程切换,提高效率。如果临界区较大,线程释放锁的时间较长,自旋会一直占用CPU却得不到锁,浪费资源。JVM对锁做了哪些优化?与JDK1.5相比,JDK1.6中的HotSopt虚拟机对synchronized内置锁的性能进行了优化,包括自适应自旋、锁淘汰、锁粗化、偏向锁、轻量级锁等。通过这些优化措施,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对象。而不是创建一个新的。如果编译器可以确定originalperson对象不会被修改,它可以优化和消除创建新人的过程。根据这个思路,我们来举一个锁消除的例子。经过逃逸分析,如果发现有些对象不能被其他线程访问,那么就可以认为是栈上的数据。栈上的数据由于只有本线程可以访问,所以自然是线程安全的,所以不需要加锁,所以这样的锁会自动解除。比如我们StringBuffer的append方法如下:@OverridepublicsynchronizedStringBufferappend(Objectobj){toStringCache=null;super.append(String.valueOf(obj));returnthis;}从代码可以看出,this方法是一个用synchronized修饰的同步方法,因为它可能同时被多个线程使用。但在大多数情况下,它只会在一个线程中使用。如果编译器可以判断StringBuffer对象只会在一个线程中使用,那么就说明它一定是线程安全的,那么我们的编译器就会做优化,去掉对应的synchronized,省去加锁和解锁的操作,这样以提高整体效率。锁粗化释放锁,然后什么都不做,重新获取锁:publicvoidlockCoarsening(){synchronized(this){}synchronized(this){}synchronized(this){}}其实这个释放和就是完全没有必要重新获取锁。如果我们扩大同步区,也就是只在开头加一次锁,在结尾直接解锁,那么就可以省去中间无意义的解锁和加锁过程,就相当于把几个synchronizedblock合并了进入一个更大的同步块。这样做的好处是线程在执行这些代码的时候,不需要频繁的申请和释放锁,减少了性能开销。但是,我们这样做也有一个副作用,那就是我们把同步区域变大了。如果我们在循环中也这样做,如代码所示:for(inti=0;i<1000;i++){synchronized(this){}}也就是我们在开始的时候开始展开同步先循环Area并持有锁,直到最后一个循环结束,然后同步代码块释放锁,这样会导致其他线程长时间无法获取到锁。所以这里的锁粗化不适合循环场景,只适合非循环场景。默认情况下启用锁粗化,可以使用-XX:-EliminateLocks禁用。偏向锁/轻量级锁/重量级锁这三种锁,特指同步锁的状态,锁的状态由对象头中的mark字表示。(1)偏向锁偏向锁的思路是,如果从头到尾都没有竞争这个锁,那么其实也不需要加锁,标记一下即可。一个对象初始化后,如果没有线程去获取它的锁,它就是可偏向的。当第一个线程访问它并试图获取锁时,它会记录这个线程。如果后面尝试获取锁的线程是这个偏向锁的拥有者,那么它可以直接获取锁,开销很小。(2)轻量级锁JVM的开发者发现,很多时候synchronized中的代码块是多个线程交替执行的,也就是说没有真正的竞争,或者只有短暂的锁竞争,可以用CAS解决。在这种情况下,不需要重量级锁。轻量级锁是指当锁原本是偏向锁时,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会尝试通过自旋来获取锁。阻塞式(3)重量级锁这种锁是靠操作系统的同步机制来实现的,所以开销比较大。当多个线程直接实际竞争,锁竞争时间比较长时,此时偏向锁和轻量级锁都不能满足需求,锁会膨胀为重量级锁。重量级锁会导致其他申请了锁却拿不到锁的线程进入阻塞状态。锁升级有利于最好的锁性能并避免CAS操作。但是轻量级锁使用自旋和CAS来避免重量级锁造成的线程阻塞和唤醒,性能一般。重量级锁会阻塞得不到锁的线程,性能最差。默认情况下,JVM会优先使用偏向锁,必要时逐步升级,大大提高了锁的性能。