synchronized基础特性1互斥性同步关键字修饰方法或代码块保证同一时刻只有一个线程访问等于一个锁。2可见性每次线程的到来都能访问到最新的值因为在互斥性的基础上由于每次仅有一个线程执行临界区的代码因此其修改的任何变量值对于稍后执行该临界区的线程来说是可见的。因为互斥性的存在也保证了临界区变量修改的原子性而volatile仅仅只能保证变量修改的可见性并不能保证原子性。3有序性同样因为在互斥性的基础上代码块也好实例方法或静态方法也好一旦被synchronized后各个线程相互竞争反正每次只能有一个线程执行。使用修饰静态方法// 静态方法publicsynchronizedstaticvoidstaticFun(){System.out.println(synchronized修饰静态方法锁对象是当前class);// 业务代码……}修饰实例方法// 实例方法publicsynchronizedvoidinstanceFun(){System.out.println(synchronized修饰实例方法锁对象是类实例);// 业务代码……}修饰代码块publicvoidfun(){synchronized(TestSync.class){// 锁对象是当前class// synchronized (this){ // 锁对象是实例System.out.println(synchronized修饰代码块锁对象可以是实例可以是类);}}synchronized可以修饰构造方法吗不能修饰构造方法构造方法只能有权限修饰符比如public、private之类的它本身就是线程安全的。synchronized底层原理java对象头1对象在内存中的布局对象 对象头 实例变量 填充数据实例变量存放类的属性数据信息包括父类的属性信息如果是数组的实例部分还包括数组的长度这部分内存按4字节对齐。填充数据jvm要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的仅仅是为了字节对齐。对象头它是实现synchronized的锁对象的基础。jvm用2个字存储对象头如果是数组则3个字多出来的1个字记录的是数组长度。2对象头 (Marked Word) (Class Metadata Address)Mark Word存储对象的hashCode、锁信息、分代年龄、GC标志。Class Metadata Address类型指针指向对象的类元数据。3Mark Word结构32位下锁状态25bit4bit1bit是否是偏向锁2bit锁标志位无锁状态对象HashCode对象分代年龄001有锁状态的结构Monitor1对象监视器它是一个同步工具。2每个对象都存在着一个 monitor 与之关联在Java虚拟机(HotSpot)中monitor是由ObjectMonitor实现的其主要数据结构如下位于HotSpot虚拟机源码ObjectMonitor.hpp文件C实现的ObjectMonitor() { _header NULL; _count 0; //记录个数 _waiters 0, _recursions 0; _object NULL; _owner NULL; _WaitSet NULL; //处于wait状态的线程会被加入到_WaitSet _WaitSetLock 0 ; _Responsible NULL ; _succ NULL ; _cxq NULL ; FreeNext NULL ; _EntryList NULL ; //处于等待锁block状态的线程会被加入到该列表 _SpinFreq 0 ; _SpinClock 0 ; OwnerIsThread 0 ; }ObjectMonitor 中有两个队列_EntryList和_WaitSet一个变量_onwer指向持有ObjectMonitor对象的线程。3当多个线程同时访问一段同步代码时首先会进入 _EntryList 集合当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1若线程调用 wait() 方法将释放当前持有的monitorowner变量恢复为nullcount自减1同时该线程进入WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值以便其他线程进入获取monitor(锁)。如下图所示synchronized代码块底层原理1准备代码publicclassSyncCodeBlock{publicinti;publicvoidsyncTask(){//同步代码库synchronized(this){i;}}}编译上述代码并使用javap反编译后可以得到字节码反编译命令javap -c -s -v -l Main.class。3:monitorenter//进入同步方法//..........省略其他15:monitorexit//退出同步方法16:goto24//省略其他.......21:monitorexit//退出同步方法2同步语句块的实现使用的是 monitorenter 和 monitorexit 指令其中monitorenter指令指向同步代码块的开始位置monitorexit指令则指明同步代码块的结束位置当执行monitorenter指令时当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权当 objectref 的 monitor 的进入计数器为 0那线程可以成功取得 monitor并将计数器值设置为 1取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权那它可以重入这个 monitor (关于重入性稍后会分析)重入时计数器的值也会加 1。3倘若其他线程已经拥有 objectref 的 monitor 的所有权那当前线程将被阻塞直到正在执行线程执行完毕即monitorexit指令被执行执行线程将释放 monitor(锁)并设置计数器值为0 其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令而无论这个方法是正常结束还是异常结束。4为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行编译器会自动产生一个异常处理器这个异常处理器声明可处理所有的异常它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个 monitorexit 指令它就是异常结束时被执行的释放 monitor 的指令。synchronized方法底层原理1方法级的同步是隐式即无需通过字节码指令来控制的它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。2当方法调用时调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置如果设置了执行线程将先持有monitor虚拟机规范中用的是管程一词 然后再执行方法最后在方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间执行线程持有了monitor其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛出了异常并且在方法内部无法处理此异常那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。下面我们看看字节码层面如何实现publicclassSyncMethod{publicinti;publicsynchronizedvoidsyncTask(){i;}}使用javap反编译后的字节码如下Classfile/Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.classLastmodified2017-6-2;size308bytesMD5checksum f34075a8c059ea65e4cc2fa610e0cd94CompiledfromSyncMethod.javapublicclasscom.zejian.concurrencys.SyncMethodminor version:0major version:52flags:ACC_PUBLIC,ACC_SUPERConstantpool;//省略没必要的字节码//syncTask方法publicsynchronizedvoidsyncTask();descriptor:()V//方法标识ACC_PUBLIC代表public修饰ACC_SYNCHRONIZED指明该方法为同步方法flags:ACC_PUBLIC,ACC_SYNCHRONIZEDCode:stack3,locals1,args_size10:aload_01:dup2:getfield #2// Field i:I5:iconst_16:iadd7:putfield #2// Field i:I10:returnLineNumberTable:line12:0line13:10}SourceFile:SyncMethod.java从字节码中可以看出synchronized修饰的方法并没有monitorenter指令和monitorexit指令取得代之的确实是ACC_SYNCHRONIZED标识。java1.6之后synchronized的优化在Java早期版本中synchronized属于重量级锁效率低下因为监视器锁monitor是依赖于底层的操作系统的Mutex Lock来实现的而操作系统实现线程之间的切换时需要从用户态转换到核心态这个状态之间的转换需要相对比较长的时间时间成本相对较高这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化所以现在的synchronized锁效率也优化得很不错了Java 6之后为了减少获得锁和释放锁所带来的性能消耗引入了轻量级锁和偏向锁。优化1自旋锁1、线程的阻塞和唤醒需要CPU从用户态转为核心态频繁的阻塞和唤醒对CPU来说是一件负担很重的工作势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面对象锁的锁状态只会持续很短一段时间为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。2、何谓自旋锁所谓自旋锁就是让该线程等待一段时间不会被立即挂起看持有锁的线程是否会很快释放锁。怎么等待呢执行一段无意义的循环即可自旋。3、自旋锁在JDK 1.4.2中引入默认关闭但是可以使用-XX:UseSpinning开启在JDK1.6中默认开启。同时自旋的默认次数为10次可以通过参数-XX:PreBlockSpin来调整4、如果通过参数-XX:PreBlockSpin来调整自旋锁的自旋次数会带来诸多不便。假如我将参数调整为10但是系统很多线程都是等你刚刚退出的时候就释放了锁假如你多自旋一两次就可以获取锁你是不是很尴尬。于是JDK1.6引入自适应的自旋锁让虚拟机会变得越来越聪明。优化2适应性自旋锁JDK 1.6引入了更加聪明的自旋锁即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢线程如果自旋成功了那么下次自旋的次数会更加多因为虚拟机认为既然上次成功了那么此次自旋也很有可能会再次成功那么它就会允许自旋等待持续的次数更多。反之如果对于某个锁很少有自旋能够成功的那么在以后要获取这个锁的时候自旋的次数会减少甚至省略掉自旋过程以免浪费处理器资源。优化3锁消除1、为了保证数据的完整性我们在进行操作时需要对这部分操作进行同步控制但是在有些情况下JVM检测到不可能存在共享数据竞争这时JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。2、如果不存在竞争为什么还需要加锁呢所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸对于虚拟机来说需要使用数据流分析来确定但是对于我们程序员来说这还不清楚么我们会在明明知道不存在数据竞争的代码块前加上同步吗但是有时候程序并不是我们所想的那样我们虽然没有显示使用锁但是我们在使用一些JDK的内置API时如StringBuffer、Vector、HashTable等这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法Vector的add()方法publicvoidvectorTest(){VectorStringvectornewVectorString();for(inti0;i10;i){vector.add(i);}System.out.println(vector);}在运行这段代码时JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外所以JVM可以大胆地将vector内部的加锁操作消除。优化4锁粗化1、我们知道在使用同步锁的时候需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步这样做的目的是为了使需要同步的操作数量尽可能缩小如果存在锁竞争那么等待锁的线程也能尽快拿到锁。2、在大多数的情况下上述观点是正确的。但是如果一系列的连续加锁解锁操作可能会导致不必要的性能损耗所以引入锁粗化的概念。3、锁粗化概念比较好理解就是将多个连续的加锁、解锁操作连接在一起扩展成一个范围更大的锁。如上面实例vector每次add的时候都需要加锁操作JVM检测到对同一个对象vector连续加锁、解锁操作会合并一个更大范围的加锁、解锁操作即加锁解锁操作会移到for循环之外。优化5锁的等级锁的状态总共有四种无锁状态、偏向锁、轻量级锁、重量级锁。他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级这种策略是为了提高获得锁和释放锁的效率。无锁对象未被任何线程锁定线程可以自由访问共享资源。偏向锁当只有一个线程访问同步块时JVM 会偏向该线程后续该线程可以直接进入同步块无需任何同步操作。1.在对象头Mark Word中记录偏向的线程 ID。2.后续该线程进入同步块时只需检查 Mark Word 是否偏向自己如果是则直接执行。轻量级锁当偏向锁失效有其他线程竞争时JVM 会尝试使用 自旋CAS代替阻塞线程避免线程挂起和恢复的开销。1.线程在栈帧中创建 锁记录Lock Record存储对象头的 Mark Word 副本。2.通过 CAS 尝试将对象头的 Mark Word 替换为指向锁记录的指针。3.如果成功线程获得轻量级锁如果失败其他线程已持有锁则自旋等待。4.自旋超过阈值后轻量级锁升级为重量级锁。重量级锁当轻量级锁自旋失败竞争激烈时JVM 会升级为重量级锁依赖操作系统 互斥量Mutex实现线程阻塞和唤醒这时候也就成为了原始的Synchronized的实现。1.线程通过 ObjectMonitor 进入 等待队列EntryList阻塞直到获取锁。2.锁释放时唤醒等待队列中的线程。synchronized其他关键点synchronized的可重入性从互斥锁的设计上来说当一个线程试图操作一个由其他线程持有的对象锁的临界资源时将会处于阻塞状态但当一个线程再次请求自己持有对象锁的临界资源时这种情况属于重入锁请求将会成功。publicclassAccountingSyncimplementsRunnable{staticAccountingSyncinstancenewAccountingSync();staticinti0;staticintj0;Overridepublicvoidrun(){for(intj0;j1000000;j){//this,当前实例对象锁synchronized(this){i;increase();//synchronized的可重入性}}}publicsynchronizedvoidincrease(){j;}publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1newThread(instance);Threadt2newThread(instance);t1.start();t2.start();t1.join();t2.join();System.out.println(i);//2000000}}线程中断与synchronized1如果线程处于阻塞状态或者试图执行一个阻塞操作此时中断该线程将会抛出一个InterruptedException异常同时中断状态会被复位由中断状态改为非中断状态。importjava.util.concurrent.TimeUnit;publicclassMain{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1newMyThread();t1.start();TimeUnit.SECONDS.sleep(2);//中断处于阻塞状态的线程t1.interrupt();}}classMyThreadextendsThread{Overridepublicvoidrun(){//while在try中通过异常中断就可以退出run循环try{while(true){//当前线程处于阻塞状态异常必须捕捉处理无法往外抛出TimeUnit.SECONDS.sleep(2);}}catch(InterruptedExceptione){System.out.println(Interrupted When Sleep);booleaninterruptthis.isInterrupted();//中断状态被复位System.out.println(interrupt:interrupt);}}}输出Interrupted When Sleep interrupt:false2如果线程处于非阻塞状态此时中断该线程不会得到任何响应。publicclassInterruputThread{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1newThread(){Overridepublicvoidrun(){while(true){System.out.println(未被中断);}}};t1.start();TimeUnit.SECONDS.sleep(2);t1.interrupt();/** * 输出结果(无限执行): 未被中断 未被中断 未被中断 ...... */}}虽然我们调用了interrupt方法但线程t1并未被中断因为处于非阻塞状态的线程需要我们手动进行中断检测并结束程序改进后代码如下publicclassInterruputThread{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1newThread(){Overridepublicvoidrun(){while(true){//判断当前线程是否被中断if(this.isInterrupted()){System.out.println(线程中断);break;}}System.out.println(已跳出循环,线程中断!);}};t1.start();TimeUnit.SECONDS.sleep(2);t1.interrupt();}}输出线程中断 已跳出循环,线程中断!3事实上线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用也就是对于synchronized来说如果一个线程在等待锁那么结果只有两种要么它获得这把锁继续执行要么它就保存等待即使调用中断线程的方法也不会生效。importjava.util.concurrent.TimeUnit;publicclassMain{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1newThread(()-{SyncCall.f(t1);});//让t1去获取锁t1.start();//暂停让t1先拿到锁TimeUnit.SECONDS.sleep(1);Threadt2newThread(()-{while(true){if(Thread.interrupted()){System.out.println(中断线程!!);break;}else{System.out.println(t2 enter else);SyncCall.f(t2);}}});//让t2去获取锁t2.start();//暂停让t2执行到等待锁TimeUnit.SECONDS.sleep(1);//中断t2无法生效t2.interrupt();}}classSyncCall{publicsynchronizedstaticvoidf(Stringname){System.out.println(Trying to call f(): name);while(true){Thread.yield();}}}输出Trying to call f(): t1 t2 enter else //死循环...等待唤醒机制与synchronized所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法在使用这3个方法时必须处于synchronized代码块或者synchronized方法中否则就会抛出IllegalMonitorStateException异常这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象也就是说notify/notifyAll和wait方法依赖于monitor对象在前面的分析中我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针)而synchronized关键字可以获取 monitor 这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。synchronized(obj){obj.wait();obj.notify();obj.notifyAll();}需要特别理解的一点是与sleep方法不同的是wait方法调用完成后线程将被暂停但wait方法将会释放当前持有的监视器锁(monitor)直到有线程调用notify/notifyAll方法后方能继续执行而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后并不会马上释放监视器锁而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。
java-锁-synchronized
发布时间:2026/5/22 10:44:01
synchronized基础特性1互斥性同步关键字修饰方法或代码块保证同一时刻只有一个线程访问等于一个锁。2可见性每次线程的到来都能访问到最新的值因为在互斥性的基础上由于每次仅有一个线程执行临界区的代码因此其修改的任何变量值对于稍后执行该临界区的线程来说是可见的。因为互斥性的存在也保证了临界区变量修改的原子性而volatile仅仅只能保证变量修改的可见性并不能保证原子性。3有序性同样因为在互斥性的基础上代码块也好实例方法或静态方法也好一旦被synchronized后各个线程相互竞争反正每次只能有一个线程执行。使用修饰静态方法// 静态方法publicsynchronizedstaticvoidstaticFun(){System.out.println(synchronized修饰静态方法锁对象是当前class);// 业务代码……}修饰实例方法// 实例方法publicsynchronizedvoidinstanceFun(){System.out.println(synchronized修饰实例方法锁对象是类实例);// 业务代码……}修饰代码块publicvoidfun(){synchronized(TestSync.class){// 锁对象是当前class// synchronized (this){ // 锁对象是实例System.out.println(synchronized修饰代码块锁对象可以是实例可以是类);}}synchronized可以修饰构造方法吗不能修饰构造方法构造方法只能有权限修饰符比如public、private之类的它本身就是线程安全的。synchronized底层原理java对象头1对象在内存中的布局对象 对象头 实例变量 填充数据实例变量存放类的属性数据信息包括父类的属性信息如果是数组的实例部分还包括数组的长度这部分内存按4字节对齐。填充数据jvm要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的仅仅是为了字节对齐。对象头它是实现synchronized的锁对象的基础。jvm用2个字存储对象头如果是数组则3个字多出来的1个字记录的是数组长度。2对象头 (Marked Word) (Class Metadata Address)Mark Word存储对象的hashCode、锁信息、分代年龄、GC标志。Class Metadata Address类型指针指向对象的类元数据。3Mark Word结构32位下锁状态25bit4bit1bit是否是偏向锁2bit锁标志位无锁状态对象HashCode对象分代年龄001有锁状态的结构Monitor1对象监视器它是一个同步工具。2每个对象都存在着一个 monitor 与之关联在Java虚拟机(HotSpot)中monitor是由ObjectMonitor实现的其主要数据结构如下位于HotSpot虚拟机源码ObjectMonitor.hpp文件C实现的ObjectMonitor() { _header NULL; _count 0; //记录个数 _waiters 0, _recursions 0; _object NULL; _owner NULL; _WaitSet NULL; //处于wait状态的线程会被加入到_WaitSet _WaitSetLock 0 ; _Responsible NULL ; _succ NULL ; _cxq NULL ; FreeNext NULL ; _EntryList NULL ; //处于等待锁block状态的线程会被加入到该列表 _SpinFreq 0 ; _SpinClock 0 ; OwnerIsThread 0 ; }ObjectMonitor 中有两个队列_EntryList和_WaitSet一个变量_onwer指向持有ObjectMonitor对象的线程。3当多个线程同时访问一段同步代码时首先会进入 _EntryList 集合当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1若线程调用 wait() 方法将释放当前持有的monitorowner变量恢复为nullcount自减1同时该线程进入WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值以便其他线程进入获取monitor(锁)。如下图所示synchronized代码块底层原理1准备代码publicclassSyncCodeBlock{publicinti;publicvoidsyncTask(){//同步代码库synchronized(this){i;}}}编译上述代码并使用javap反编译后可以得到字节码反编译命令javap -c -s -v -l Main.class。3:monitorenter//进入同步方法//..........省略其他15:monitorexit//退出同步方法16:goto24//省略其他.......21:monitorexit//退出同步方法2同步语句块的实现使用的是 monitorenter 和 monitorexit 指令其中monitorenter指令指向同步代码块的开始位置monitorexit指令则指明同步代码块的结束位置当执行monitorenter指令时当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权当 objectref 的 monitor 的进入计数器为 0那线程可以成功取得 monitor并将计数器值设置为 1取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权那它可以重入这个 monitor (关于重入性稍后会分析)重入时计数器的值也会加 1。3倘若其他线程已经拥有 objectref 的 monitor 的所有权那当前线程将被阻塞直到正在执行线程执行完毕即monitorexit指令被执行执行线程将释放 monitor(锁)并设置计数器值为0 其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令而无论这个方法是正常结束还是异常结束。4为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行编译器会自动产生一个异常处理器这个异常处理器声明可处理所有的异常它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个 monitorexit 指令它就是异常结束时被执行的释放 monitor 的指令。synchronized方法底层原理1方法级的同步是隐式即无需通过字节码指令来控制的它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。2当方法调用时调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置如果设置了执行线程将先持有monitor虚拟机规范中用的是管程一词 然后再执行方法最后在方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间执行线程持有了monitor其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛出了异常并且在方法内部无法处理此异常那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。下面我们看看字节码层面如何实现publicclassSyncMethod{publicinti;publicsynchronizedvoidsyncTask(){i;}}使用javap反编译后的字节码如下Classfile/Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.classLastmodified2017-6-2;size308bytesMD5checksum f34075a8c059ea65e4cc2fa610e0cd94CompiledfromSyncMethod.javapublicclasscom.zejian.concurrencys.SyncMethodminor version:0major version:52flags:ACC_PUBLIC,ACC_SUPERConstantpool;//省略没必要的字节码//syncTask方法publicsynchronizedvoidsyncTask();descriptor:()V//方法标识ACC_PUBLIC代表public修饰ACC_SYNCHRONIZED指明该方法为同步方法flags:ACC_PUBLIC,ACC_SYNCHRONIZEDCode:stack3,locals1,args_size10:aload_01:dup2:getfield #2// Field i:I5:iconst_16:iadd7:putfield #2// Field i:I10:returnLineNumberTable:line12:0line13:10}SourceFile:SyncMethod.java从字节码中可以看出synchronized修饰的方法并没有monitorenter指令和monitorexit指令取得代之的确实是ACC_SYNCHRONIZED标识。java1.6之后synchronized的优化在Java早期版本中synchronized属于重量级锁效率低下因为监视器锁monitor是依赖于底层的操作系统的Mutex Lock来实现的而操作系统实现线程之间的切换时需要从用户态转换到核心态这个状态之间的转换需要相对比较长的时间时间成本相对较高这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化所以现在的synchronized锁效率也优化得很不错了Java 6之后为了减少获得锁和释放锁所带来的性能消耗引入了轻量级锁和偏向锁。优化1自旋锁1、线程的阻塞和唤醒需要CPU从用户态转为核心态频繁的阻塞和唤醒对CPU来说是一件负担很重的工作势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面对象锁的锁状态只会持续很短一段时间为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。2、何谓自旋锁所谓自旋锁就是让该线程等待一段时间不会被立即挂起看持有锁的线程是否会很快释放锁。怎么等待呢执行一段无意义的循环即可自旋。3、自旋锁在JDK 1.4.2中引入默认关闭但是可以使用-XX:UseSpinning开启在JDK1.6中默认开启。同时自旋的默认次数为10次可以通过参数-XX:PreBlockSpin来调整4、如果通过参数-XX:PreBlockSpin来调整自旋锁的自旋次数会带来诸多不便。假如我将参数调整为10但是系统很多线程都是等你刚刚退出的时候就释放了锁假如你多自旋一两次就可以获取锁你是不是很尴尬。于是JDK1.6引入自适应的自旋锁让虚拟机会变得越来越聪明。优化2适应性自旋锁JDK 1.6引入了更加聪明的自旋锁即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢线程如果自旋成功了那么下次自旋的次数会更加多因为虚拟机认为既然上次成功了那么此次自旋也很有可能会再次成功那么它就会允许自旋等待持续的次数更多。反之如果对于某个锁很少有自旋能够成功的那么在以后要获取这个锁的时候自旋的次数会减少甚至省略掉自旋过程以免浪费处理器资源。优化3锁消除1、为了保证数据的完整性我们在进行操作时需要对这部分操作进行同步控制但是在有些情况下JVM检测到不可能存在共享数据竞争这时JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。2、如果不存在竞争为什么还需要加锁呢所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸对于虚拟机来说需要使用数据流分析来确定但是对于我们程序员来说这还不清楚么我们会在明明知道不存在数据竞争的代码块前加上同步吗但是有时候程序并不是我们所想的那样我们虽然没有显示使用锁但是我们在使用一些JDK的内置API时如StringBuffer、Vector、HashTable等这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法Vector的add()方法publicvoidvectorTest(){VectorStringvectornewVectorString();for(inti0;i10;i){vector.add(i);}System.out.println(vector);}在运行这段代码时JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外所以JVM可以大胆地将vector内部的加锁操作消除。优化4锁粗化1、我们知道在使用同步锁的时候需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步这样做的目的是为了使需要同步的操作数量尽可能缩小如果存在锁竞争那么等待锁的线程也能尽快拿到锁。2、在大多数的情况下上述观点是正确的。但是如果一系列的连续加锁解锁操作可能会导致不必要的性能损耗所以引入锁粗化的概念。3、锁粗化概念比较好理解就是将多个连续的加锁、解锁操作连接在一起扩展成一个范围更大的锁。如上面实例vector每次add的时候都需要加锁操作JVM检测到对同一个对象vector连续加锁、解锁操作会合并一个更大范围的加锁、解锁操作即加锁解锁操作会移到for循环之外。优化5锁的等级锁的状态总共有四种无锁状态、偏向锁、轻量级锁、重量级锁。他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级这种策略是为了提高获得锁和释放锁的效率。无锁对象未被任何线程锁定线程可以自由访问共享资源。偏向锁当只有一个线程访问同步块时JVM 会偏向该线程后续该线程可以直接进入同步块无需任何同步操作。1.在对象头Mark Word中记录偏向的线程 ID。2.后续该线程进入同步块时只需检查 Mark Word 是否偏向自己如果是则直接执行。轻量级锁当偏向锁失效有其他线程竞争时JVM 会尝试使用 自旋CAS代替阻塞线程避免线程挂起和恢复的开销。1.线程在栈帧中创建 锁记录Lock Record存储对象头的 Mark Word 副本。2.通过 CAS 尝试将对象头的 Mark Word 替换为指向锁记录的指针。3.如果成功线程获得轻量级锁如果失败其他线程已持有锁则自旋等待。4.自旋超过阈值后轻量级锁升级为重量级锁。重量级锁当轻量级锁自旋失败竞争激烈时JVM 会升级为重量级锁依赖操作系统 互斥量Mutex实现线程阻塞和唤醒这时候也就成为了原始的Synchronized的实现。1.线程通过 ObjectMonitor 进入 等待队列EntryList阻塞直到获取锁。2.锁释放时唤醒等待队列中的线程。synchronized其他关键点synchronized的可重入性从互斥锁的设计上来说当一个线程试图操作一个由其他线程持有的对象锁的临界资源时将会处于阻塞状态但当一个线程再次请求自己持有对象锁的临界资源时这种情况属于重入锁请求将会成功。publicclassAccountingSyncimplementsRunnable{staticAccountingSyncinstancenewAccountingSync();staticinti0;staticintj0;Overridepublicvoidrun(){for(intj0;j1000000;j){//this,当前实例对象锁synchronized(this){i;increase();//synchronized的可重入性}}}publicsynchronizedvoidincrease(){j;}publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1newThread(instance);Threadt2newThread(instance);t1.start();t2.start();t1.join();t2.join();System.out.println(i);//2000000}}线程中断与synchronized1如果线程处于阻塞状态或者试图执行一个阻塞操作此时中断该线程将会抛出一个InterruptedException异常同时中断状态会被复位由中断状态改为非中断状态。importjava.util.concurrent.TimeUnit;publicclassMain{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1newMyThread();t1.start();TimeUnit.SECONDS.sleep(2);//中断处于阻塞状态的线程t1.interrupt();}}classMyThreadextendsThread{Overridepublicvoidrun(){//while在try中通过异常中断就可以退出run循环try{while(true){//当前线程处于阻塞状态异常必须捕捉处理无法往外抛出TimeUnit.SECONDS.sleep(2);}}catch(InterruptedExceptione){System.out.println(Interrupted When Sleep);booleaninterruptthis.isInterrupted();//中断状态被复位System.out.println(interrupt:interrupt);}}}输出Interrupted When Sleep interrupt:false2如果线程处于非阻塞状态此时中断该线程不会得到任何响应。publicclassInterruputThread{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1newThread(){Overridepublicvoidrun(){while(true){System.out.println(未被中断);}}};t1.start();TimeUnit.SECONDS.sleep(2);t1.interrupt();/** * 输出结果(无限执行): 未被中断 未被中断 未被中断 ...... */}}虽然我们调用了interrupt方法但线程t1并未被中断因为处于非阻塞状态的线程需要我们手动进行中断检测并结束程序改进后代码如下publicclassInterruputThread{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1newThread(){Overridepublicvoidrun(){while(true){//判断当前线程是否被中断if(this.isInterrupted()){System.out.println(线程中断);break;}}System.out.println(已跳出循环,线程中断!);}};t1.start();TimeUnit.SECONDS.sleep(2);t1.interrupt();}}输出线程中断 已跳出循环,线程中断!3事实上线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用也就是对于synchronized来说如果一个线程在等待锁那么结果只有两种要么它获得这把锁继续执行要么它就保存等待即使调用中断线程的方法也不会生效。importjava.util.concurrent.TimeUnit;publicclassMain{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1newThread(()-{SyncCall.f(t1);});//让t1去获取锁t1.start();//暂停让t1先拿到锁TimeUnit.SECONDS.sleep(1);Threadt2newThread(()-{while(true){if(Thread.interrupted()){System.out.println(中断线程!!);break;}else{System.out.println(t2 enter else);SyncCall.f(t2);}}});//让t2去获取锁t2.start();//暂停让t2执行到等待锁TimeUnit.SECONDS.sleep(1);//中断t2无法生效t2.interrupt();}}classSyncCall{publicsynchronizedstaticvoidf(Stringname){System.out.println(Trying to call f(): name);while(true){Thread.yield();}}}输出Trying to call f(): t1 t2 enter else //死循环...等待唤醒机制与synchronized所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法在使用这3个方法时必须处于synchronized代码块或者synchronized方法中否则就会抛出IllegalMonitorStateException异常这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象也就是说notify/notifyAll和wait方法依赖于monitor对象在前面的分析中我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针)而synchronized关键字可以获取 monitor 这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。synchronized(obj){obj.wait();obj.notify();obj.notifyAll();}需要特别理解的一点是与sleep方法不同的是wait方法调用完成后线程将被暂停但wait方法将会释放当前持有的监视器锁(monitor)直到有线程调用notify/notifyAll方法后方能继续执行而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后并不会马上释放监视器锁而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。