1.如何关联对象头、MarkWord、monitor、synchronized(1)首先,Java中每个对象的底层都会为其创建一个monitor监听器。这是我们保证的JVM级别。该监视器类似于锁。哪个线程持有这个监视器的操作权就相当于获得了锁。(2)其次,被synchronized修饰的代码或方法,会在底层产生两条指令,分别是monitorenter和monitorexit。(3)在进入synchronized代码块之前,会执行monitorenter指令,申请monitor监视器的操作权。如果申请成功,就相当于获得了锁。如果另一个线程成功申请了monitor,此时就得等待。其他线程执行完synchronized中的代码后,会执行monitorexit命令释放monitor监视器,让其他等待的线程可以重新申请。获取监视器监视器。什么是监视器?为什么monitor可以当锁用?首先,既然知道每个对象都有一个monitor监视器,那你知道每个对象是如何与其monitor监视器相关联的吗?通过synchronized加锁是通过对象头的MarkWord关联起来的,它记录了锁的状态和拥有锁的线程的地址指针。当MarkWord中最后两个lockflags为10时,MarkWord前面就是monitor的地址,我给大家画出对象头、MarkWord和monitor(32位)的关系:2.监视器的内部结构监视器称为对象监视器,也称为监视器锁。JVM规定每个java对象都有一个monitor对象与之对应。这个monitor是JVM为我们创建的,底层是用C++实现的。.其实monitor也是C++底层某个类的对象。那个类就是ObjectMonitor,它的属性如下://结构如下ObjectMonitor::ObjectMonitor(){_header;_count;//很重要,表示加锁计数器,_count=0表示还没有人加锁,_count>0表示加锁次数_waiters;_recursions;_owner;//很重要,指向成功加锁的线程,_owner=null表示没有人加锁_waitset;//等待线程的集合,在synchronized代码块中调用wait()方法的线程会被添加到这个集合中休眠,等待别人唤醒它_waitsetLock;_responsiable;_succ;_cxq;_freenext;_entrylist;//很重要,等待队列,加锁失败的线程会被加入到这个等待队列中,等待再次竞争锁_spinFreq;//获取锁前的自旋次数_spinclock;//获取ownerIsThread前每次锁自旋的时间;}3.1.监听锁原理_count:这个属性很重要,直接表示是否被锁。如果没有被线程锁定,_count=0。如果_count大于0,则表示已锁定。_owner:这个属性也很重要,直接指向加锁的线程。比如线程A获取锁成功,那么_owner=threadA;当_owner=null时,表示没有线程可以锁定。_waitset:当持有锁的线程调用wait()方法时,线程会释放锁,然后线程会被加入到monitor的waitset集合中等待,然后线程会被挂起。只有其他线程调用通知将其唤醒。_entrylist:这是等待队列。当线程加锁失败时,会被阻塞,然后该线程会被加入到entrylist队列中,等待获取锁。_spinFreq:获取锁失败前的自旋次数;JDK1.6之后,优化了synchronized;在JDK1.6之前,只要线程获取锁失败,线程就会立即挂起,线程醒来后会去竞争锁。导致上下文切换频繁,性能太差。JDK1.6之后对这个问题进行了优化,即线程获取锁失败后,不会立即挂起,而是每次都会重试,争夺。这个_spinFreq是最大重试次数,也就是自动自旋的次数,如果超过这个次数抢不到,那么线程就只能休眠了。_spinClock:上面说了如果获取锁失败,会每隔一段时间重试一次。这个属性是自旋间隔的时间段,比如50ms,那么每隔50ms就会去尝试获取锁。下面通过图文展示加锁过程:(1)首先,当没有线程加锁监听时,是这样的:注:_count=0表示加锁次数为0,即没有线程锁;_owner指向null,即没有线程锁(2)那么,此时线程A和线程B竞争锁,如下图:(3)线程A竞争锁,改变_count为1,表示lock次数为1,而_owner=线程A,即指向自身,表示线程A获得了锁。当_count=0且_owner=null时,表示没有人锁定监视器。此时线程A和线程B同时请求锁,即竞争将_count改为1,由于线程A的buddy移动速度较快,因此将_count改为1,获得锁成功。也闹了,同时设置_onwer=threadA,表示已经获取到了锁,并告诉线程B,对不起兄弟,我获取到锁了,我先去操作。因为锁定意味着将_count设置为1并将_owner指向它自己。那么反过来,释放锁的时候把_count设为0,把_owner设为null就可以了吗?是的,释放锁的过程就是这么简单:加完释放锁之后,接下来要做的就是_spinFreq、_spinclock、_entrylist:上面解释字段属性的时候说_spinFreq是自动入口在等待锁的过程中。自旋次数,_spinclock为自旋周期,即每次自旋多长时间,_entrylist为未获取的自旋次数,只能放入_entrylist等待队列挂机。继续看图:(1)线程B在获取锁的时候,发现monitor已经被线程A锁定了(2)然后monitor中记录的_spinFreq和spinclock信息告诉线程B,你可以每隔50ms来试试加一次锁,一共可以尝试10次(3)如果线程B在这10次加锁尝试中成功获取到锁,则线程B将_count设置为1,_owner指向自己,表示成功获取到锁lock(4)如果此时10次尝试获取锁都用完了,那就没办法了,只能放到等待队列先休眠,也就是线程B被挂起。_spinFreq和_spinclock这两个监视器的属性主要是在线程自旋的时候用到的。entryList的作用是当线程自旋次数用完后,只能进入等待队列休眠。4.6.轻量级锁轻量级锁模式,在加锁前会创建一个锁记录,然后将MarkWord中的数据备份到锁记录中(MarkWord中保存着hashcode、GCage等重要数据,不能丢失。)用于后续恢复MarkWord。这条锁记录放在锁线程的虚拟机栈中。加锁的过程就是把MarkWord前面的30位指向加锁记录的地址。因此,markword的地址指向哪个线程的虚拟机栈,就说明哪个线程获得了轻量级锁。就像下图,线程A获取了一个轻量级的锁,锁记录保存在线程A的虚拟机栈中,然后锁记录的地址保存在MarkWord的前30位。了解了轻量级锁的原理之后,我们继续说一下从偏向锁升级为轻量级锁的过程:(1)首先线程A持有偏向锁,然后正在执行synchronized块中的代码(2)这时线程B来争锁,发现有人加了偏向锁,正在执行synchronized块中的代码。为了避免上述线程A一直持有锁不释放的情况,需要将锁升级为轻量级Lock(3)首先挂起线程A,为线程A创建一个锁记录LockRecord,将MarkWord的数据复制到锁记录中;然后把锁记录放入线程A的虚拟机栈中(4)然后把MarkWord中的前30位指向锁记录在线程A中的地址,当线程A醒来时,线程A就知道了它持有一个轻量级锁4.6.2,在轻量级锁模式下,多个线程如何竞争锁和释放锁?(1)线程A和线程B同时竞争锁。在轻量级锁模式下,他们会创建LockRecord锁记录,并放在自己的栈帧中。(2)同时进行CAS操作,将MarkWord的前30位设置为自己的锁记录的地址,谁设置成功,谁就获得锁。恢复时,进行CAS操作恢复Mark字数据为锁定前的状态。java同步偏向锁后hashcode存在哪里?jdk8BiasedLocking默认是开启的,但是有延迟,可以通过参数关闭:-XX:BiasedLockingStartupDelay=0。hashcode是懒加载,调用hashCode方法后会保存在对象头中。当对象头中没有hashcode时,对象头锁的状态为biasable(biasable,101,无线程id)。如果在同步代码块之前调用hashCode方法,对象头中会有hashcode,锁状态为无偏(001)。这时再次执行同步代码块,锁直接是轻量级锁(thinlock,00)。如果hashcode在synchronized代码块中执行,锁直接从偏向锁扩展为重量级锁。
