ReentrantLock(可重入锁) 与synchronize 区别
(1)、可重入性
ReentrantLock与synchronize 都具有可重入性,就是同一个线程已经获得了锁,可以再多次获取当前锁,解锁次数要与加锁次数相同,才能释放锁。
(2)、锁的实现
ReentrantLock 是通过JDK 实现的,synchronize 是JVM实现的,区别就是synchronize 是操作系统实现,ReentrantLock 是用户敲代码实现,synchronize 实现是JVM ,很难查到源码,ReentrantLock 可以很容易的阅读源码了解实现
(3)、性能的区别
synchronize 优化以前,性能是不如ReentrantLock 的,但是优化以后,synchronize引入了自旋锁,偏向锁等,synchronize 性能与ReentrantLock 性能就差不多了,两者都可用的情况下,官方更建议使用synchronize ,因为写法更容易。
其实synchronize 优化,就是借鉴了ReentrantLock 中的cas技术,都是试图在用户态把加锁问题解决,避免进入内核态的线程阻塞。
(4)、功能区别
(1)便利性:synchronize 使用更便利简洁,并且由编译器保证锁的加锁和释放,而ReentrantLock 需要手动声明加锁和释放锁,为了避免忘记释放锁或者抛异常无法释放锁,一般在finally中释放锁
(2)锁的细粒度和灵活度:ReentrantLock 优于synchronize
ReentrantLock 独有功能
(1)可指定是公平锁还是非公平锁
ReentrantLock 可以指定是公平锁还是非公平锁,而synchronize 只能是非公平锁,所谓公平锁就是先等待的线程先获得锁,这点ReentrantLock 是独有的,它可以选择公平或者非公平。
(2)提供了一个condition(条件)类,可以分组唤醒需要唤醒的线程
ReentrantLock 可以实现分组唤醒需要唤醒的线程,而不是像synchronize 一样,要么随机唤醒一个线程,要么唤醒全部线程
(3)提供能够中断等待锁的线程的机制,lock.lockInterruptibly()
ReentrantLock 实现是一种自旋锁,通过循环调用CAS操作来实现加锁,它的性能比较好,也是因为避免线程进入内核态阻塞状态,想尽办法避免内核进入阻塞状态是我们去分析和理解锁设计的关键钥匙
如果需要实现上面三个功能,就必须使用ReentrantLock ;
其他情况下可以根据性能或者业务场景选择ReentrantLock 或者synchronize ,synchronize 能做的事,ReentrantLock 都可以做,而ReentrantLock 能做的,synchronize 不一定能做,性能方面两者差不多。
当JVM用synchronize 管理锁和锁释放时,JVM在生成线程转储时,能够包括锁定信息,这些对调试非常有价值,因为它们能标识死锁或者异常信息的来源,而Lock类只是普通的类,JVM 不知道具体哪个线程拥有对象,synchronize 几乎可以再JVM的所有版本里工作,ReentrantLock 主要实在JDK1.5之后使用。
例子(1):
执行结果 5000
例子(2):
执行结果 5000
new ReentrantLock()或新建一个非公平锁,
ReentrantLock(boolean fair),表示可以传入一个boolean值来决定启用公平或者非公平锁,true表示公平锁,false表示非公平锁
tryLock()作用:表示仅在调用时,锁未被另一个线程保持的情况下,才获取锁定,就是锁没有线程占用的情况下,才获取锁。
tryLock(long timeout, TimeUnit unit)作用是,如果锁在给定的时间内,没有被另一个线程保持,且当前线程没有被中断,则获取这个锁。
lockInterruptibly() 如果当前线程没有被中断, 获取锁,如果已经中断了,就抛出异常。
isLocked() 查询此锁是否有任意线程保持
isHeldByCurrentThread() 查询当前线程是否保持锁定状态
isFair() 判断是不是公平锁
hasQueuedThread(Thread thread)查询指定线程是否在等待此锁定
hasQueuedThreads() 查询是否有线程正在等待此锁定
getHoldCount() 查询当前线程保持锁定的个数,也就是定义lock()方法的个数。
ReentrantLock 的同步组件 Condition
执行结果:
可以看出,整个过程协作过程是依靠节点在AQS队列和condition队列来回移动实现的,condition作为一个条件类,很好的维护了一个等待信号的队列,并在适时将节点加入AQS等待队列中,实现唤醒操作。通过代码执行的结果及分析,condition也是一个多线程间协调工具的类,使得某个或者某个线程一起等待条件,只有当条件满足时,线程才会被唤醒,这里的具备的条件就是得到的信号,线程被唤醒后, 重新争夺锁,condition就是做这个事情。
ReentrantReadWriteLock
该类在内部实现了具体独占锁特点的写锁,以及具有共享锁特点的读锁,
ReentrantReadWriteLock类具有如下特点:
1.1 支持公平/非公平策略
与ReadWriteLock类一样,ReentrantReadWriteLock对象在构造时,可以传入参数指定是公平锁还是非公平锁。
1.2 支持锁重入
同一读线程在获取了读锁后还可以获取读锁;
同一写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;
1.3 支持锁降级
所谓锁降级,就是:先获取写锁,然后获取读锁,最后释放写锁,这样写锁就降级成了读锁。但是,读锁不能升级到写锁。简言之,就是:
写锁可以降级成读锁,读锁不能升级成写锁。
1.4 Condition条件支持
ReentrantReadWriteLock的内部读锁类、写锁类实现了Lock接口,所以可以通过newCondition()方法获取Condition对象。但是这里要注意,读锁是没法获取Condition对象的,读锁调用newCondition() 方法会直接抛出UnsupportedOperationException。
我们知道,condition的作用其实是对Object类的wait()和notify()的增强,是为了让线程在指定对象上等待,是一种线程之间进行协调的工具。
当线程调用condition对象的await方法时,必须拿到和这个condition对象关联的锁。由于线程对读锁的访问是不受限制的(在写锁未被占用的情况下),那么即使拿到了和读锁关联的condition对象也是没有意义的,因为读线程之前不需要进行协调。
ReentrantReadWriteLock在没有任何读取锁时,才可以获取写入锁,有一个读锁,一个写锁
ReentrantReadWriteLock用于,如果执行中需要读取,可能有另一个需求需要写入,为了保证同步ReentrantReadWriteLock的读取锁定就可以派上用场了,然而,如果读取情况很多,写入情况很少,会导致写入线程遭遇饥饿,也就是写入线程迟迟无法锁定,而进入等待状态,也就是,一直有线程在读取,导致一直没办法获取写锁,然后写线程就一直处于等待,不知道什么时候才能真正操作写
StampedLock
StampedLock类,在JDK1.8时引入,是对读写锁ReentrantReadWriteLock的增强,该类提供了一些功能,优化了读锁、写锁的访问,同时使读写锁之间可以互相转换,更细粒度控制并发。
StampedLock状态是由版本和模式两个部分组成,锁获取方法返回的是一个数字,作为票据stamp ,StampedLock用相关的锁状态来表示和控制锁访问。0表示没有写锁被授权访问,读锁中分为悲观锁和乐观锁,
乐观读:如果读的操作很多,写的操作很少的情况下,可以乐观的认为读取与写入同时发生的几率很少,因此不悲观的使用完全的读取锁定,程序可以读取资料之后,判断是否遭到写入执行的变更,再采取后续的措施,这个小小的改进可以大幅度提高程序的吞吐量。
StampedLock的特点
StampedLock的主要特点概括一下,有以下几点:
所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
StampedLock有三种访问模式:
①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
③Optimistic reading(乐观读模式):这是一种优化的读模式。
StampedLock支持读锁和写锁的相互转换
我们知道RRW中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。
StampedLock提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。
无论写锁还是读锁,都不支持Conditon等待
我们知道,在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会阻塞。
但是,在Optimistic reading中,即使读线程获取到了读锁,写线程尝试获取写锁也不会阻塞,这相当于对读模式的优化,但是可能会导致数据不一致的问题。所以,当使用Optimistic reading获取到读锁时,必须对获取结果进行校验。
StampedLock类源码上的示例代码:
可以看到,上述示例最特殊的其实是distanceFromOrigin方法,这个方法中使用了“Optimistic reading”乐观读锁,使得读写可以并发执行,但是“Optimistic reading”的使用必须遵循以下模式:
总结
(1)、synchronize JVM实现的,不但可以用一些监控工具监控,而且在代码出现异常时,虚拟机会自动释放锁, JVM会自动做加锁与解锁,synchronize 不会引发死锁,JVM会自动解锁。其他的锁,如果使用不当会造成死锁的。
(2)、ReentratLock、ReentratReadWriteLock、StempedLock,都是对象层面的锁定,要保证锁一定要释放,需要吧解锁放到finally里,才比较保险,stempedLock 在吞吐量有巨大的改进,特别是在读线程越来越多的场景下,stempedLock 有个复杂的AIP,对加锁操作很容易误用其他的方法,这里要注意。
怎么选择加锁类
(1)、在竞争线程比较少量的情况下,synchronize是个很好的锁实现
(2)、竞争者不少,但是线程的增长趋势是可以预估的,ReentrantLock是个很好的通用锁实现