并发学习之synchronized关键字(上)
synchronized概述
synchronized是java的一个关键字,是jvm的内置锁,由于synchronized的加锁过程是在jvm内部实现的,所以也叫隐式锁。
synchronized是保证多线程并发情况下同步互斥的方式之一,在jdk1.6版本之前,synchronized是直接依赖Monitor管程对象(监视器锁)实现同步,Monitor是一个重量级锁,底层依赖于底层操作系统Mutex lock互斥量实现,由于Java是用户线程,每次加锁解锁过程需要cpu经过用户态和操作系统的内核态之间的转化,所以性能较低。
并发大神Doug Lea基于此,独自使用Java语言开发出AQS框架,基于AQS框架开发出另一个同步互斥锁——ReentrantLock(保证可重入和公平性),在性能上,ReentrantLock要优于此时的synchronized。于是在jdk1.6版本,Oracle官方对synchronized锁进行了一系列优化升级,如:锁粗化,锁消除,新增偏向锁、轻量级锁状态、适应性自旋。在性能方面,已经没有明显差异,需要根据使用场景自行选择。
synchronized加锁方式
1、加在对象上,锁的是括号里的对象(Object)
2、加在方法上,锁的是当前实例对象(this)
3、加在静态方法上,锁的是该类(Class)
synchronized底层原理
monitor监视器锁
synchronized底层都是基于monitor监视器锁实现。当一个Monitor被持有之后,它会处于锁定状态,同步就是基于进入和退出Monitor对象来实现方法同步和代码块同步。
每一个Java对象都可以成为Monitor锁(通常说的synchronized的对象锁),实际上,Monitor是由c++的ObjectMonitor对象实现的。ObjectMonitor中的重要属性:
1、count:记录加锁次数。
2、owner:指向持有ObjectMonitor对象的线程。
3、EntryList:处于等待锁block状态的线程,会被加入到该队列。
4、WaitSet: 存放wait状态的线程。
当多个线程同时访问同一个共享变量时:
1、首先进入EntryList队列等待锁。
2、当线程拿到对象锁monitor后,count+1,并且owner设置为当前持有锁的线程。
3、如果持有monitor的线程调用了wait()方法,就会释放掉monitor,owner置为null,count-1,该线程进入WaitSet等待被唤醒。
4、如果持有monitor的线程执行完毕,释放monitor,将count减为0,以便其他线程获取锁。
加锁过程
在字节码层面,如果synchronized锁的是对象,在编译成字节码时,会在同步块的起止位置分别加上monitorenter 和 monitorexit 两条字节码指令,为了避免同步块中可能发生异常无法释放锁,monitorexit可能会加多条确保锁能成功释放。
上面已经知道,synchronized的加锁和解锁过程实际上是线程对monitor对象锁的持有与释放的过程。
monitorenter:线程执行monitorenter时尝试获取monitor对象锁,如果monitor的count数为0,那么线程进入 monitor,并将count+1,表示线程获取到monitor; 如果线程已经持有了monitor,只是重新进入,count+1;
monitorexit:执行monitorexit,count-1,直到减为0,表示线程释放锁,其他阻塞的线程可以进入获取锁。
如果锁的是方法,经过反编译会发现在字节码中添加了一个ACC_SYNCHRONIZED 访问标志符,而没有通过monitorenter和monitorexit指令实现。常量池中会多一个 ACC_SYNCHRONIZED 标示符,jvm就是通过判断ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,线程就会尝试获取monitor,如果获取成功,在方法执行完并释放monitor之前,其他线程不能进入该方法。
上面的方式无论是通过字节码指令还是使用访问标识符,本质上都是通过调用操作系统的互斥原语mutex来实现。被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
需要注意的是:wait()、notify()、notifyAll()等操作,都是基于Monitor的,所以这些操作要在同步代码块中执行。
锁的膨胀升级过程
jdk1.6之后,加入了偏向锁、轻量级锁。锁的升级过程是根据线程的竞争程度经无锁、偏向锁(-XX:-UseBiasedLocking关闭,默认开启)、轻量级锁到重量级锁,并且该过程是不可逆的。当锁竞争不激烈且代码执行时间不长时,线程会使用自旋的方式避免进入阻塞状态等待唤醒,减少底层内核切换造成的性能消耗。
从无锁状态到重量级锁的状态都是存储在对象锁的对象头的Mark Word区域中,每经过一次升级,对象头中的状态都会发生改变。