在说synchronized的相关底层实现我们就简单聊一聊synchronized的用法,其实最常用的就两种方式,直接去修饰一个方法,我们把这个方法叫做同步方法,或者用大括号去修饰一个代码块,当然代码块里的内容就叫做同步代码块
public synchronized void fun(){ //同步方法
//to do
}
public void fun1(){
synchronized (this/对象名){ //同步代码块
//to do
}
当然synchronized还可以去修饰静态方法或者类本身,但是无所谓的,我们就只说这两个最常用的方式,去小探讨一下synchronized更底层一些的东西。
都知道synchronized是通过加锁去保证线程或数据的同步,要探讨synchronized底层的东西首先得要知道synchronized到底是去锁住了什么东西?是锁住代码块?这个类?或者是对象?你要去了解synchronized去如何上锁,你至少知道是如何给什么东西上锁吧。其实我觉得同步代码块修饰的那个方式应该还是比较明了,我特意在this旁又写了一个对象名,你那括号里面填的就是相应的对象名,其实就是锁住的就是对象本身。
如果说我们已经明了synchronized是去给对象本身加锁,那么它是如何给对象加锁的,没研究过我们肯定不知道,但是可以肯定的是它一定是去改变了什么东西才会使对象成为一个锁住的状态,你总不可能啥也没干它自己就锁住了吧???如果学过集合类容器或者说java中也非常常用的另一个内置锁ReentrantLock,他们去改变状态的依据就是去把某个成员变量的值改变以下,类似于版本号一样,比如HashMap中有serialVersionUID,当HashMap结构发生相应的变化其值也会发生变化,ReentrantLock加锁会把相应的继承类AQS里的state变量值去用CAS机制改变,当然这里可能说的复杂了,学以致用嘛,不过不要紧,我的核心意思是就是把类里的某个成员变量当作标记,改变它的值就是改变为相应的状态,虽然synchronized是给对象本身上锁,对象的概念可能没有相应类的概念那么清晰,有成员变量有方法,但是可以肯定,它一定是去改变了对象结构里的某个标识。(但是不会真的有人觉得是把我对象相应类里的某个成员变量改变了吧,这个就很扯淡了,那程序不就乱套了,我上面那就是举个例子)
为了解决synchronized是对对象做了什么改变,凭什么现在A线程过来给这个对象加了锁,B线程再次加锁是不成功的,那就要去研究对象本身的结构,无可避免要聊到Java对象的布局,或者说Java对象由什么组成,我第一次听到这个问题 的时候很傻,我竟然想了个由字节码组成,这就很扯淡了,不过类本身确实就是由字节码组成的,对象本身new出来后是在堆上存储着,Java对象在内存上的结构如下:
这就是Java对象在堆上的实际样子,我们着重于关注那个叫对象头的部分,由Markword和Klass指针构成,我特意去查了一下官方文档对于对象头的描述,当然我看的是Open JDK,开源的嘛。
object header:Common structure at the beginning of every GC-managed heap object.(Every oop points to an object header.)Includes fundamental information about the heap objet’s layout,type,GC state,synchronization state,and identity hash code.Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format.
当然我就不装了,我用有道翻译的,而且我发现有道确实比金山更好用一点:
每个gc管理的堆对象开头的公共结构。(每个oop都指向一个对象头。)包括堆对象布局、类型、GC状态、同步状态和标识哈希码的基本信息。由两个词组成。在数组中,它后面紧跟着一个长字段。注意,Java对象和VM内部对象都有一个通用的对象头格式。
其中对象头中的MarkWord结构如下:
MarkWord里面有许多复杂的用来描述状态的信息,除了各种锁以外,还有线程ID、GC分代年龄、哈希码等等常见信息,不过我们今天的主题是锁,可能你会草率地认为synchronized会通过修改图上那些锁的标志位达到上锁的目的,其实也不完全是。
往往看待问题要能够全面,只去关注了特定的某一方面就认为把这个知识掌握好了这就是我们总是学不好Java的原因,要去尽可能关注底层一点的东西,我们何必不写个测试代码把它反编译一下
public class TestDemo7 {
public final static Object mutex = new Object();
// public static void func(){
synchronized (mutex){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} // 同步代码块
// public static synchronized void func(){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
} // 同步方法
public static void main(String[] args) {
for(int i=0;i<5;i++){
new Thread(){
@Override
public void run(){
func();
}
}.start();
}
}
}
其实代码就是我平时练习过程中的小代码顺手拿来测试了,为了明了起见,我还是把代码贴出来,代码里有两个func( ),当然每次我只会选用一个。
如果是用同步代码块反编译后会看到:
这里有两个比较特殊的指令,monitorenter和monitorexit,我下面会再做详细的分析,如果是同步方法,反编译以后:
没有那两个monitor的指令介入,取而代之的是ACC_SYNCHRONIZED标识符。
关于monitor的指令我们可以在官方的JVM规范中看到:
monitorenter :
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.
这段话的大概意思为:
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1)如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2)如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
3)如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
这段话的大概意思为:
执行monitorexit的线程必须是object ref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
其实我觉得官方的文档说明已经很清楚了,我们new出来的每个对象都与生俱来带有一把锁,就是monitor锁,英文不好也可以叫对象锁,或者监视器锁,相当于我们想给哪个对象加锁的时候就是操作这个对象的monitor,从反编译的结果来看,同步方法虽然并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。其实JVM会根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
可能我们上面既聊了MarkWord,又聊了Monitor,如果是刚接触多线程的初学者可能已经很迷了,不知道我到底想表达什么,到底是通过哪里加锁,但是我这里有一个非常简单的问题:Java中有哪些锁? 对,就是这个最简单最纯粹的问题,这个问题面试的时候问你不过分吧,但是如果接触过多线程并发,你可能听过公平锁、非公平锁、乐观锁、悲观锁、包括MarkWord里的偏向锁、轻量级锁、重量级锁等等,是不是又头大了起来,我们这里只说MarkWord里的这几个锁的关系,为什么在上面那个标题中没有说呢,因为确实不是一句两句可以明了,而且Synchronized锁的演变升级概念是在JDK1.6之后才提出的,当然是为了优化锁的机制,让程序更灵活。
情况是这样的:
我觉得图上那个表将属性绘制的很清楚,锁的升级也是按列由上到下演变的,无锁的时候那一行只有对象的哈希码和GC标记,这也是我们每个对象必要的属性,偏向锁标志位是0,锁标志位是01;
第一次有线程A抢占了锁之后就会变为偏向锁,所以偏向锁那一栏也多出了一个线程ID属性,标记哪个线程抢占了它,并且偏向锁标志位改位1,这就代表1是有效的;此时变为偏向锁的好处是当A线程再次请求该锁的时候无需任何同步操作,不需要去再次执行获得锁的过程;
在锁已经被A线程抢占为偏向锁的状态,如果有别的B线程再来抢占这把锁,B线程去抢占锁的方式是CAS机制,这个我只是提一下不过不是重点,B线程可能抢占成功也可能抢占不成功,如果B线程抢占成功那么此时里面的线程ID就会替换为B的并且也处于偏向锁状态,但是实际上更多的情况是B线程抢占不成功,因为线程A正在使用这把锁,在B线程去抢占锁的那一刻开始就已经存在锁的竞争了,有竞争没关系,但是如果你去抢占失败那么锁就会升级,此时状态依然A线程持有锁,但锁会升级为轻量级锁,没有了偏向锁标志位,而锁标志位变为00。此时的轻量级锁在同步周期内是不存在竞争的;
以此类推,在A线程已经是持有轻量级锁状态下时,B线程如果继续抢锁失败,锁就会升级为自旋锁,上面图中其实并没有自旋锁状态,它是属于轻量级锁的一种,是JVM内部自己优化的一种状态,此时JVM相当于会给你一个阈值,你在这个阈值次数内才会去抢锁,过了阈值得不到锁才会去升级,当然抢占成功就是B持有轻量级锁;
在上述自旋锁的若干次竞争后,超过了阈值依然失败就会升级为最后的重量级锁,此时所有没有抢到锁的线程都会被阻塞挂起,此时对于资源的消耗会比较大,虽然阻塞的线程本身不会去占用过多的CPU资源,但是线程的阻塞和唤醒都是需要操作系统用户态和内核态的切换,会相当耗时,有可能比同步代码块本身里的代码执行还耗时。
画了个流程图:
总结一下我上面所说的话,synchronized加锁是去给对象加锁,是去修改对象的Monitor进入数以及对象头里的MarkWord锁标志位,而MarkWord里的锁标志位又分为偏向锁、轻量级锁、自旋锁(JVM优化的结果还是属于轻量级锁)、重量级锁4种状态,它们会根据当前程序并发的情况而不停演变,当程序并发量小,锁的竞争小,那么处于偏向锁状态已经持有过锁的线程不需要再去执行加锁的过程,程序性能会大大提高,随着并发量的增加,或者由于同步代码块执行的时间过长,线程抢锁的程度也越来越激烈,偏向锁就会失效,为了保证竞争内的线程的响应时间,或者为了考虑程序过于复杂,单个线程需要过久的时间去执行同步代码块,锁就会不断演变为轻量级锁、重量级锁。
1.对象的Monitor和MarkWord是有对应关系的,据说MarkWord里有一个LockWord的指针会指向Monitor,但可能不叫LockWord,但是确实是有一个指针,朋友们非要刁钻这个东西可以自己去读官方的JVM规范或者Open JDK文档
2.在锁的演变过程种,线程抢锁的方式是在自己线程栈里开辟一块空间,用来保存一个指向对象MarkWord的指针(或者说是把该对象的MarkWord复制过来的),MarkWord中也有一个类似于这样的引用,去替换为指向该线程的,它两互相保存,都保存成功了就是抢锁成功了。