通常在面试时,面试官会问到你,Java中实现同步有哪些方式(或者可以问你Java中有哪些加锁的方式?或是Java中有哪些方式可以保证线程并发安全?)此类的问题,今天谈谈相应的实现方式和使用场景。
Synchronized关键字
这个JVM原生语法层面的互斥锁,核心是通过使用对象锁进行实现(对象锁,简单的说在每个Java对象的对象头中的Mark Word中都会有一个Monitor对象-由C语言的ObjectMonitor实现),
- 在对实例方法或者类方法(static方法)添加此关键字时,是通过标志位ACC_SYNCHRONIZED标志此方法为同步方法(本质跟锁代码块一样通过Monitor对象的两个指令实现)。相应的如果是实例方法,即没有指定要进行锁定的对象,那么就使用当前的实例对象来走位锁对象,而如果是类方法,则使用当前类的Class对象作为锁对象。
- 在对代码块添加此关键字时,是通过字节码指令monitorenter,monitorexit实现(源码在ObjectMonitor中),当执行monitorenter命令时,首先要尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有那个对象的锁(ObjectMonitor->_owner),把锁的计数器(ObjectMonitor->_recursive)加1,相应的执行monitorexit时进行减1,当计数器为0时,对象锁被释放,(这里也是可重入锁的实现原理,保证线程不被自己锁死)。如果获取失败,就当前线程就阻塞等待,直到对象锁被另一个线程释放为止。
最后,你应该要知道,Java线程都是映射到OS的原生线程上的,要阻塞或者唤醒一个线程,都需要叫操作系统来帮忙,
(使real墙钟时间远大于user+sys的CPU耗时)
这就需要由 用户态->内核态 的转换,这种状态的转换是需要耗费很多处理器时间的。所以Synchronized是一个
重量级(Heavyweight)的操作,应该在确实有必要时才使用这种操作。当前在虚拟机中本身也会进行一些优化
(JDK1.6中),
例如:偏向锁,轻量级的自旋锁(默认10次),类似的操作来避免频繁切入内核态中。
J.U.C->ReetrantLock
在基本用法上,ReetrantLock与Synchronized很相似,都具体线程重入的特性,但是在代码的写法上就有一定的区别了,ReentrantLock表现为API层面的互斥锁(通过lock(),unlock()方法配合try/finally块来完成,面试中请记得ReentrantLock的原理为Java中的AQS(抽象队列同步器),具体为两个双向列表+volatile的标志位state+CAS和自旋操作)。
相比Synchronized,ReentrantLock增加了一些高级功能,主要是等待可中断,可实现公平锁(两种所默认都是非公平锁(每次都是所有线程一起竞争资源,不排队,为了提高吞吐量)),以及锁可以绑定多个条件。
- 等待可中断指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,去处理其他的事情,可中断特性对处理执行时间非常长的同步块很有帮助。
- 公平锁的指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获取锁:而非公平锁则不保证这一点,在锁被释放时,任何一个等待的线程都有机会获取取。可通过带boolean值的构造方法来使用公平锁。
- 锁绑定多个条件指的是一个ReentrantLock对象可以同时绑定多个Condition对象(AQS中),而Synchronized中,锁对象的wait(), notify()和notifyAll()方法可以实现一个隐含的条件,如果要多余一个的条件关联的时候,就必须得额外添加一个锁。ReentrantLock只需要多次调用newCondition()方法来获取多个Condition实例(构建多个ConditionQueue)即可。
所以如需使用上述功能的场景,ReentrantLock对象是一个很好的选择。