引言
终于走到这一步,在JDK5之前,Java都是通过synchronized
关键字实现同步锁功能的,通过前面对synchronized
关键字的阐述,相信我们已经非常了解了其特性,它是为了保证在多线程并发下对共享资源的访问的线程安全。从JDK5开始,Java提供了另一种加锁的方式即我们在可见性、有序性与原子性以及非原子协定章节中提到过的Lock显示锁。Lock显示锁也是Java并发包存在的主要原因,它提供了与synchronized关键字类似的线程同步功能,并且使用Lock显示锁也满足happens-before原则。Lock显示锁和synchronized关键字支撑起整个Java语言层面的线程同步解决办法,两者各有优劣,适用于不同的场景。
Lock接口
Lock作为Java并发包的基础核心组件,但是其只是一个接口,因为以模板方式实现的AbstractQueuedSynchronizer才是显示锁Lock最底层最精细的实现,但是Lock锁作为一个使用于不同场景的上层同步器,其加锁的特性在不同的场景肯定有不同的需求,所以为了达到不同场景的加锁特性,就需要以AQS的底层实现为基石,通过实现其特定的模板方法来达到不同的特性需要(例如:重入性,公平性,读写锁等)。说了半天这不就是我们在Java并发包核心框架AQS之一同步阻塞与唤醒续章节中对“自定义AQS同步器”的做法吗。
没错,自定义同步器就是在构造满足不同场景的Lock显示锁,还记得在“自定义同步器”时,我们首先要做的第一件事情吗,那就是需要先定义一个外部接口,利用该接口屏蔽Lock锁的具体实现,只对外提供几个简单的对Lock锁的操作方法。既然每次自定义同步器时都需要先定义一个外部接口,而Java并发包作为JDK又需要为开发者提供几种最常见最通用的显示锁的实现,因此一个通用的显示锁接口自然就诞生了,在实现自定义的同步器的时候,我们直接都实现这个接口就可以了,这个通用的接口就是我们这里说的Lock接口。
下面我们先看看Lock接口对外提供的需要开发者实现的接口方法:
从Lock接口的方法可以看出,除了阻塞式获取锁的方法void lock()与 synchronized关键字的语言几乎完全一样之外,另外几个获取锁的方法都是synchronized关键字所不能提供的,这就是Lock显示锁比synchronized关键字更强大的地方。
Lock接口的使用
Lock lock = new ReentrantLock(); //以ReentrantLock为例 lock.lock();//不能将锁的获取操作放在try块里面,因为如果在获取锁时发生了异常也会导致锁的无故释放 try { // 可能会出现线程安全的操作 } finally { // 一定在finally中释放锁 // 也不能把获取锁在try中进行,因为有可能在获取锁的时候抛出异常 lock.unlock(); }
Lock显示锁使用时,最需要注意的有两点,1,不要将获取锁的操作放进try块中;2,一定要记住在finally语句块中执行unlock()释放锁,否则可能导致死锁。
Lock显示锁与synchronize隐式锁的区别
实现方式区别:
synchronize是由C/C++书写的平台相关的native代码实现,底层使用CAS对锁对象的对象头中的Mark Word进行操作以标识加/解锁,至于锁的更多细节信息则被包含在Mark Word中指针指向的对象(Lock Record, 线程ID 或者ObjectMonitor)。在实现重量级锁的时候,JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度。另外使用了一个_WaitSet队列存储条件等待的线程。
Lock显示锁是由Java编写的AbstractQueuedSynchronizer队列同步器实现的,其内部使用CAS对一个volatile修饰的state变量进行操作以标识加/解锁,使用了一个双向链表存储同步等待的阻塞线程,而对于不同的条件等待阻塞线程,同样采用不同的双向链表存放。
就实现上来看,Lock显示锁未必会比synchronize好,由于synchronize是直接使用native代码实现的,毕竟JAVA语言效率和native语言效率比大多数情况总有不如,并且synchronize实现了自旋锁,并针对不同的系统和硬件体系进行了锁优化。
运行机制的区别:
synchronize是独占式的悲观锁,为什么都说synchronize是悲观锁呢?应该这主要是指重量级锁的情况,因为在进入synchronize同步块之前,重量级锁都悲观的认为数据已经被改变,必须要先获取到相应的锁,不论这时候有没有出现多线程竞争的情况。
而Lock锁在实现加锁机制的时候,使用的是CAS,并假设不存在竞争的情况,也就是假设同步资源state变量没有被修改,只有在CAS操作失败之后才会进行加锁,而在读写锁的实现中,甚至进行了更细粒度的加锁条件判断。
功能区别:
synchronize隐式锁,完全由JVM调度独占锁,所以功能上更加单一。对于线程通信的实现也比较简单,只能使用一个条件判断的wait()/notify()机制。
Lock显示锁则提供了非阻塞式、响应中断式的加锁实现,而且还能根据需要实现适用于不同场景的读写锁、共享锁、公共锁、非公平锁等,功能更加强大,使用上更加灵活。面对复杂的业务逻辑更加得心应手。在线程通信的实现方面也更加强大,可以使用绑定多个Condition实例获得不同的条件等待。比wait()/notify()更灵活。
使用方式的不同:
使用synchronize更加简单,获取和释放锁的过程都由JVM自己完成,开发人员使用更加容易放心。
使用Lock显示锁,开发人员必须手动获取锁,并且不能忘记在finally语句块中释放锁,否则会产生死锁。
日志分析的差异:
据说使用synchronize在出现死锁等需要分析线程dump数据查找问题的时候,更加清晰方面,而使用Lock显示锁则没有那么方便,不过我没有亲自验证过,但说话LockSupport中不是存在可以设置线程是被谁阻塞的方法setBlocker吗?
Lock显示锁的使用场景
并不是说在Lock锁出现之后,我们就应该毫无缘由的替换掉所有使用synchronize的加锁方式,只有在出现如下几种情况的时候我们才应当考虑使用Lock显示,否则还是应该使用synchronize关键字,毕竟它更加简单,而且优化的也很好,其性能甚至将来可能还会更好。
1.对Lock
的某个高级特性有明确的需要,例如响应超时或中断,或非阻塞式甚至需要实现特定的同步器,或者wait/notify方式的线程通信不方便(例如多条件判断)。
2.有明确的证据(而不是仅仅是怀疑)表明在特定情况下,同步已经成为可伸缩性的瓶颈。
3.在多线程高度竞争条件下可以选择使用Lock显示锁的实现ReentrantLock
,因为它在高争用的时候性能更好。
为什么在使用Lock显示锁的时候要持保守态度呢,因为对于Lock显示锁来说,synchronized 仍然有一些优势。比如,在使用 synchronized 的时候,不可能忘记释放锁;在退出 synchronized
块时,JVM 会为您做这件事。您很容易忘记用 finally
块释放锁,这对程序非常有害。另一方面几乎每个开发人员都熟悉 synchronized,但不是每个开发人员都属性Lock显示锁的使用。这样更方便代码维护。
总结起来,Lock
框架是同步的兼容替代品,它提供了 synchronized
没有提供的许多特性,它的实现在高争用下提供了更好的性能。 但是还不足以完全替代synchronized
,我们应当根据我们的需要作出是否要使用Lock显示锁的判断,大多数情况下,我们还是优先考虑使用synchronized
,只有在真正需要 Lock
的时候才用它。
Lock显示锁的通用实现
在JDK8 中,Java通过Lock接口主要实现了如下这些显示锁:ReentrantLock、ReadLock、ReadLockView、WriteLock、WriteLockView。其中的ReentrantLock就是synchronized
锁的对应Java版本实现,而ReadLock和WriteLock则是实现ReentrantReadWriteLock读写锁的内部基础,ReadLockView和WriteLockView则是实现StampedLock锁的内部基础,ReentrantLock甚至也是实现ConcurrentHashMap内部锁Segment的继承父类。在接下来的章节我们将会逐个对这些Lock接口的实现类,这些显示锁的特性进行了解分析。