1.Synchronized
Java在JDK1.6以前Synchronized都是使用的重量级锁,所以在效率方面和ReentrantLock相比是大大不如。在1.6以后Java对其进行了优化,分为了偏向锁,轻量级锁,重量级锁。并在一定程度上借鉴了ReentrantLock使用的CAS思想。现在来说Synchronized和ReentrantLock效率方面来说基本差不多,各个适用于不同的情况。Java官网推荐大多数时候应该选用Synchronized。
偏向锁:首先,Java1.8默认开启的就是偏向锁。当一个线程获取锁的时候,它首先回去找到堆中当前对象的对象头,其中有一个MarkWord属性,里面存了偏向锁id,这个时候线程会用CAS原子指令把其设置为当前线程id。当一段同步代码块一直被同一个线程访问,既没有多个线程竞争的情况下,它会自动获得偏向锁,并且也不需要做释放锁的操作。减少了获取锁释放锁的消耗。
之所以设计偏向锁,是因为根据经验,大部分情况下都是同一个线程对一个锁的多次访问,这时候时候偏向锁就能很好的提升性能。
轻量级锁:接上面。当以前有线程获取了偏向锁,而其他线程也想要获取这把锁,它会尝试修改MarkWord中的线程id,如果成功,则获取到锁,这时还是保持偏向锁。如果获取失败,表示有多个线程竞争,这时就会把偏向锁升级为轻量级锁。
这种锁的特征是,当其他线程尝试获取锁的时候,如果成功则不谈。如果失败,它会以自旋的方式,反复尝试获取锁。
如果线程自旋超过一定次数(JVM中默认是10次,也可以自己设置次数)还没有获取到锁,或者说这个时候又有其他的线程进来竞争锁,那表示锁的竞争很大,这个时候就会升级为重量级锁。
轻量级锁在获取锁,释放锁的时候均会使用到CAS原子指令,和偏向锁不同,偏向锁只有设置id时才会用。
重量级锁:当升级为重量级锁之后,未获取到锁的线程会被阻塞挂起,并把线程状态修改为Blocked。重量级锁通过操作所有对象内部自带的Monitor对象,在同步方法开始时记录一条MonitorEnter指令,在结束后记录MonitorExit指令。
而Monitor对象时基于操作系统层面的互斥锁来实现的,也就是说当线程被阻塞后,就是需要从用户态切换到内核态,并交出线程占用的所有资源。也就是说,把控制权交给操作系统,由操作系统来进行线程的调度和切换。这个时候阻塞的线程会沉睡在内核态等待,知道释放锁之后再从内核态切换回用户态,并再次尝试获取锁,如果还是失败,又切换回来。
这种反复的上下文切换,会占用大量的系统资源,导致性能低下。这也是为什么Java会在1.6以后设置了三种级别的锁,就是为了避免线程过早的进入重量级。
偏向锁:适用于单线程,没有线程竞争的情况
轻量级锁:适用于线程竞争不激烈的情况
重量级锁:适用于线程竞争十分激烈的情况。
2.ReentrantLock
ReentrantLock是属于JUC(java.util.concurrent)包下面的一个锁的实现。
JUC包下大量使用了CAS指令,可以是说在CAS的基础上而来。ReentrantLock中也多次使用了CAS。
ReentrantLock中维护了一个node节点,它是一个AQS对象,AQS中实现了一个CLH的FIFO队列。而LCK原始的方式是使用自旋,队列中的线程会不断自旋尝试获取到锁。而AQS中使用的是它的变种,是一种自旋+阻塞的方式。
当一个线程进来它会用CAS指令去尝试把state设置为1,如果成功则获取到锁。如果时多个线程竞争,CAS指令只会让一个线程获取到锁,其他线程则入队。入队以后,线程会通过自旋尝试用CAS指令获取锁,如果成功就返回,如果失败,这时会把它的前驱节点的状态设置为 SIGNAL,并通过调用LockSupport.park方法把线程阻塞,让线程进入WAITING状态。当前驱节点获取到了锁并出队后,会调用LockSupport.unpark方法唤醒线程,再次自旋尝试用CAS指令获取锁。
这里重量级锁是进入BLOCKED状态,这里是进入WAITING状态。LockSupport.park最终也会把线程交给操作系统内核进行阻塞操作,也会带来从用户态到内核态的切换。
这里会引入公平锁和非公平锁。公平锁就是按照进来的顺序,线程依次在队列中排队。而非公平锁则允许插队,无论是刚进来的线程,还是刚刚释放了锁的线程,都可以直接参与自旋的竞争锁,只有失败了以后才会被放入队列之后。
3.选用
正常情况下应该优先选用Synchronized。虽然说其重量锁比较消耗性能,不过其前面两种锁可以保证在竞争不大的情况下Synchronized的效率是更优秀的。
而ReentrantLock则适用于需要使用到公平锁。亦或者代码执行时间短,并且线程数量大的情况。重点是执行时间短,如果执行时间长,自旋的方式会一直占用CPU资源,一直去获取锁,也是很消耗性能的。
Lock在应用层来说扩展性更好。我们可以通过继承AQS来实现读写锁,公平,非公平锁等。