Java并发(三)Java中的锁
Lock
Lock对比synchronized
相同点:
都用来保护资源线程安全
- ReentrantLock和synchronized都具有可重入的特点
- 都可以保证可见性
不同点:
- 加解锁:synchronized加解锁由JVM实现的(内置锁),Lock的加解锁需要手动控制,通过lock()和unlock(),一般把unlock操作 放入finally块解锁。以防忘记解锁
- synchronized锁只能同时被一个线程拥有,但是Lock锁没有这个限制:例如读写锁中的读锁可以同时被多个线程同时拥有
- 是否可以设置公平/不公平:公平锁是指多个线程在等待同一个锁时,根据先来后到的原则依次获得锁。ReentranLock等Lock实现类可以设置,synchronized不能设置
- synchronized不够灵活:synchronized一个线程获取锁后,其他线程想要获取锁只能等待,进入阻塞状态,直到持有锁的线程释放。Lock可以使用lockInterruptibly方法,不想等了可以中断退出,也可以使用tryLock获取锁,能获取就获取,不能获取线程也可以去干别的事
整体上来说Lock是synchronized的扩展版,Lock提供了可轮询的(tryLock方法)、可中断的(lockInterruptibly)锁操作,提供可被多个线程持有的读锁,但需要手动加解锁。另外Lock的实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
怎么理解Lock与AQS的关系?
Lock是面向锁的使用者的,他定义了使用者与锁的交互接口,隐藏了实现细节。
而AQS是面向锁的实现者的,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待与唤醒等底层操作。
锁和同步器很好的隔离了使用者和实现者所需关注的领域。
AQS
什么是AQS?
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个volatile类型的整数state表示同步状态,通过内置的FIFO队列来完成想要获取资源的线程的排队工作,是实现大部分同步需求的基础。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update)来进行操作,因为它们能够保证状态的改变是安全的。
子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件(锁,CountDownLatch等)使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(可重入锁ReentrantLock、可重入读写锁ReentrantReadWriteLock和CountDownLatchs等)。
什么是:队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,它使用了一个volatile类型的整数state表示同步状态,通过内置的双向链表FIFO队列来完成想要获取资源的线程的排队工作,是实现大部分同步需求的基础。
做什么:管理同步状态
怎么做:通过将等待线程加入同步队列中,然后在释放同步状态的时候,从同步队列中唤醒等待线程,从而实现了同步机制。同步器定义了若干同步状态获取和释放的方法,子类通过继承同步器并实现它的抽象方法来管理同步状态,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,以方便实现不同类型的同步组件
AQS是怎么实现同步管理的?底层数据结构?
AQS主要依赖于一个双向链表和一个volatile类型的整数state来实现同步控制。该整数state用来表示同步状态,一般情况下,state=0表示没有线程占用同步资源,state>O表示有线程占用同步资源,state>1表示同步资源已经被争用了多次,比如ReentrantLock可以允许一个线程多次获得锁,每次state值加1。
AQS实现同步的关键在于,它提供了一个基于FIFO队列的同步队列,通过将等待线程加入同步队列中,然后在释放同步状态的时候,从同步队列中唤醒等待线程,从而实现了同步机制。
AQS的实现主要有两种方式:独占式(Exclusive)和共享式(Shared)
- 独占式是指只有一个线程可以占用同步资源,比如ReentrantLock
- 而共享式是指多个线程可以同时占用同步资源,比如CountDownLatch
在AQS中,这两种方式的实现是基本相同的,区别在于获取和释放同步状态的方式不同。
AQS有哪些核心的方法?
一共三类方法 第一类:3个访问和修改同步状态的方法
- getState():获取当前同步状态。
- setState(int newState):设置当前同步状态。
- compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性
第二类:5个可重写方法
- isHeldExclusively):该线程是否正在独占资源。只有用到condition才需要去实现它
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false
- tryAcquireShared:共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成 功,且有剩余资源
- tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,失败则返回false
第三类:9个模版方法
- aquire,release,aquireShared,releaseShared,aquireInterruptibly等
三类方法关系:
- 实现一个同步组件时,使用者继承AbstractQueuedSynchronizer并重写5个指定的方法(第二类)
- 重写同步器指定的方法时,需要使用同步器提供的3个方法来访问或修改同步状态(第一类)
- 最后将AQS组合在自定义同步组件的实现中,并调用其9个模板方法(第三类)和5个重写过的方法来实现,另外模板方法会调用使用者重写的方法
ReentrantLock
什么是可重入,什么是可重入锁?
可重入锁概念是:自己可以再次获取自己的内部锁。
比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
公平锁和非公平锁有什么区别?
- 公平锁:锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁(会有一个让的过程。当一个线程申请公平锁但无法获得时,它会被加入到等待队列的末尾。这个入队操作会导致当前线程被挂起,进行上下文切换,让出CPU给其他线程执行)
- 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁
为什么非公平锁比公平锁性能更好?
- 公平锁执行流程:获取锁时,先将线程自己添加到同步队列的队尾并休眠,当某线程用完锁之后,会去唤醒同步队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,而且上下文切换更频繁,所以公平锁的执行速度会比较慢。(线程来了都先放队尾,再拿队首去获取锁)
- 非公平锁执行流程:当线程获取锁时,会先通过CAS尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入同步队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规测,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。
ReentrantLock是如何实现公平锁的?非公平锁的?
ReentrantLock类内部总共存在Sync、NonfairSync、FairSync.三个类,NonfairSync与FairSynca类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。
非公平锁是ReentrantLock的默认实现。公平锁对比非公平锁的实现差异主要体现在tryAcquire方法(获取锁)这里。
非公平锁(NonfairSync)的tryAcquire实现直接调用了父类Sync中的nonfairTryAcquire
而公平锁tryAcquire的唯一不同的点为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁(这就叫公平)
ReentrantLock和synchronized的区别
除无读锁外,同Lock对比synchronized
ReentrantLock独占锁,实现公平锁,非公平锁
ReentrantReadWriteLock读写锁,读锁+写锁
ReentrantReadWriteLock
ReentrantReadWriteLock是什么?
ReentrantReadWriteLock实现了ReadWriteLock,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。
ReentrantReadWriteLock其实是两把锁,一把是WriteLock(写锁),一把是ReadLock(读锁)。
- 读锁是共享锁。读锁可以被同时读,可以同时被多个线程持有,
- 写锁是独占锁。写锁最多只能同时被一个线程持有。
共享锁和独占锁有什么区别?
- 共享锁:一把锁可以被多个线程同时获得。
- 独占锁:一把锁只能被一个线程获得。
线程持有读锁还能获取写锁吗?
在线程持有读锁的情况下:该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)
在线程持有写锁的情况下:该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)
什么是锁的升降级?RentrantReadWriteLock为什么不支持锁升级?
写锁可以降级为读锁,但是读锁却不能升级为写锁。
- 这是因为读锁升级为写锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能
- 另外,还可能会有死锁问题发生
- 举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁
ReentrantReadWriteLock底层读写状态如何设计的?
高16位为读锁,低16位为写锁