前言
说到并发,就不能不提互斥锁。我们知道,互斥锁是为了使同一时间,只有一个线程可以对共享变量的状态造成影响,从而避免共享变量状态发生无法预测的变化。在java中,我们不仅可以使用关键字synchronized来加锁,还可以使用api级别的Lock来更加细粒度的加锁。
导航
内置锁:synchronized
synchronized作为关键字之一,为我们提供了便利的内置锁机制来保证线程安全。synchronized有三种用法,如下所示:
1 | public class Foo { |
synchronized内置锁拥有一个计数器,如果某线程进入了临界区,该线程便会占有锁并且内置锁的计数器加1,离开临界区计数器则会减1,直到计数器为0才会释放锁。这也就是说,对于一个线程来讲,内置锁是可以重入的。
而java中,挂起与恢复线程需要依赖操作系统,而操作系统切换线程的资源消耗比较大,属于重量级操作,所以频繁的切换线程将会大大影响并发性能。因此,JVM对内置锁进行了大量优化来减少切换线程的次数,如偏向锁、轻量级锁。
要理解偏向锁,先得眼光投向java对象在内存中的存储。在Hotspot虚拟机中,java对象的存储分为三个部分:对象头、实例数据和对齐填充字节。对象头又分为三个部分,其中一部分叫做Mark Word。而Mark Word中,有专门的锁状态标识来记录锁状态:无锁、偏向锁、轻量级锁与重量级锁。
偏向锁偏向于最近获得锁的线程。当锁对象被线程A获取时,JVM将会把锁状态标识设为偏向锁模式,并且用CAS将线程A的ID记录在Mark Word中。直到另一个线程B尝试去获取锁之前,线程A进入临界区时都不需要进行额外的操作。
如果线程B尝试获得锁时,锁未被占有,那么本着最近原则,偏向锁将会属于线程B。但如果锁正在被线程A占有,那么偏向锁将会膨胀成轻量级锁:锁状态变为轻量级锁。
轻量级锁主要是通过自旋操作,去让线程“忙等待”而不是被挂起(自旋锁)。当线程A占有锁并占有一个处理器进行计算时时,线程B在另一个处理器上通过自旋等待A释放锁。如果A在B的等待过程中释放了锁,那么显然就避免了一次切换线程的开销。不过,自旋等待中会白白耗费处理器资源,所以,在进行了一定次数的自旋之后,轻量级锁将会膨胀为重要级锁:线程将会挂起,然后恢复。
自旋还有一种自适应策略:如果当前拥有自旋锁的线程,也是在自旋等待期间获得锁的,那么JVM会认为这次自旋策略成功的可能性更大,从而增加当前线程容许的自旋次数。
同时,synchronized是非公平性的:一个线程会先试图直接抢占锁,失败之后才会加入等待队列排队获得锁。
所以,JVM通过增加偏向锁与轻量级锁等减少内置锁切换线程的开销,大大增加了synchronized的性能。而除了语法层面的互斥锁synchronized,java还提供了Api层面的Lock接口来进行同步。与synchronized相比,Lock额外拥有等待可中断、限时等待、多条件绑定等特性。
Lock与ReentrantLock
1 | public interface Lock { |
以lock.lock()和lock.unlock()包裹的代码块,大致相当于用synchronized修饰的代码块。而Lock接口的其他方法,则提供了synchronized锁不具备的特性:
- void lockInterruptibly():线程在等待锁的过程中可以被其他线程所中断
- boolean tryLock():尝试获得锁
- boolean tryLock(long time, TimeUnit unit):尝试在给定时间中获得锁
- Condition newCondition():新条件绑定
lock与condition.await()/condition.signal()的协作,类似于synchronized(obj)和obj.wait()/obj.notify()的配合,但一个lock可以通过lock.newCondition()绑定多个条件,一个synchronized却只能有一个obj。
ReentrantLock作为Lock的实现类,除了如类名所示,同synchronized一样可重入之外,还可以指定是公平锁还是非公平锁:
1 | public ReentrantLock(boolean fair) { |