探究Java中的锁
一、锁的作用和比较
1、Lock接口及其类图
Lock接口:是Java提供的用来控制多个线程访问共享资源的方式。
ReentrantLock:Lock的实现类,提供了可重入的加锁语义
ReadWriteLock:读写锁的接口
ReentrantReadWriteLock: ReadWriteLock的实现类,维护一对锁,一个读锁(ReentrantReadWriteLock.ReadLock)和一个写锁(ReentrantReadWriteLock.writeLock),实现了锁的分离,提高了性能和吞吐量。也提供了可重入的加锁语义
ReentrantReadWriteLock.ReadLock:
ReentrantReadWriteLock.WriteLock:
Condition:条件队列接口,提供类似于Object监视器方法 ,可与Lock实现等待/通知模式
AbstractQueuedSynchronizer.ConditionObject: Condition的实现类
AbstractQueuedLongSynchronizer.ConditionObject:Condition的实现类
AbstractOwnableSynchronizer:可以被线程专有的同步器
AbstractQueuedSynchronizer(AQS):继承于AbstractOwnableSynchronizer,Java中构建锁和其他同步器的基础构建
AbstractQueuedLongSynchronizer:AbstractQueuedSynchronizer的一个版本,实现对Long型同步状态的同步。
(1)Lock与Synchronized同步锁、内置锁的的区别
加锁机制 | 特性 | 优点 | 缺点 | 适用范围 |
Synchronize(内置锁) | 1、实现操作的互斥性和原子性 2、实现内存的可见性 3、禁止重排序 4、锁的获取和释放都是隐式的 5、每一个对象都是一个内置锁 6、必须在获取锁的代码块内释放,简化编码工作 7、锁的获取与释放都是基于代码块的(获取锁的操作和释放锁的操作在同一个代码块) | 1、JVM的内置属性(当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 2、可重入 3、无需实现锁的获取和释放 | 1、尝试获取已被独占的锁时线程会被阻塞 4、无法中断一个正在等候获得锁的线程,也无法通过轮询得到锁,如果不想等下去,也就没法得到锁。 5、同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行,多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些非块结构的锁定更合适的情况 | 优先使用 2、监视器方法( wait(),waite(timeout),notify(),notifyAll() )与Synchronize实现通知/等待模式 |
ReentrantLock | 1、实现操作的原子性和互斥性 2、实现内存的可见性 3、禁止重排序 5、锁的获取和释放都是显示的 6、提供了可轮询、可定时、可中断的、公平性的锁获取机制 7、实现非块的加锁模式 | 1、性能优于内置锁 2、可重入 3、可在截止时间前获取锁 4、可非阻塞获取锁(尝试获取已被占用的锁的线程状态变更为等待,而不是阻塞) 5、可实现公平性锁 | 1、锁的获取和释放必须显示的执行锁的释放操作,不能制动清除锁 2、独占排他式锁,互斥锁,每次只能有一个线程持有该锁 | 1、需要使用锁的高级功能(可定时,可轮询,可以中断)的时候才使用LOCK 2、与Condition实现等待/通知模式 |
ReentrantReadWriteLock | 1、分离了读锁和写锁,提供了读锁多线程共享访问和写锁独占访问 | 1、多读少写的情况下性能优于排他锁 | 在不是多读少写的情况下性能低于排它锁 | 多读少写 |
二、Java构建锁的基础组件AQS(探究锁的实现原理)
简介:队列同步器AbstractQueueSynchronizer(AQS)是用来构建锁或者其他同步组件的基础框架,使用一个int成员变量表示同步状态,通过内置FIFO队列来实现资源获取线程的的排队工作。
(AbstractQueuedLongSynchronizer是用Long型来表示同步状态)
使用模式:
通过子类继承AQS并实现它的抽象方法,并作为自定义同步组件的静态内部类,代理实现相关方法。
内置FIFO队列:AbstractQueueSynchronizer.Node表示队列节点,用来保存获取同步状态失败的线程引用、等待状态、前驱后继节点。
获取同步状态失败的节点添加到队列的尾部(通过CAS设置尾节点)
1、AQS独占式同步状态获取和释放的工作原理
独占式获取同步状态的获取失败的时生成的节点类型为Node.EXCLUSIVE(独占式),在加入到同步队列末尾后,进入节点自旋中,只至前驱节点为头结点且成功获取同步状态才可退出。
acqiure()获取锁,release()释放锁
2、AQS共享式同步状态获取和释放的工作原理
共享式获取同步状态时生成节点的类型为Node.SHARED(共享式),但是将节点插入到同步队列末尾的时候并不适用CAS。
tryAcquireShare() 获取同步状态
3、独占式超时获取同步状态的工作原理
doAcquire(arg,nanosTimeout):超时获取同步状态
acquireInterruptibly(arg):可响应中断的获取同步状态
三、重入锁、公平锁、非公平锁
含义 | 优点 | 缺点 | |
公平锁 | 锁的获取顺序严格按照锁等待的时间,等待时间越长的最优先获取 | 减少发生“线程饥饿”的概率 | 性能开销大(因为线程上下文切换次数多) |
非公平锁 | 锁的获取采用抢占式获取,锁的默认实现 | 性能开销小(因为进行线程上下文切换的次数少),提供更大的吞吐量 | 会出现“线程饥饿”问题 |
四、Condition接口
对比项 | Object监视器方法 | Condition |
前置条件 | 获取对象的锁 | Lock.lock() Condition condttion=Lock.newCondition() |
调用方式 | object.wait() | condition.wait() |
等待队列个数 | 一个 | 多个 |
同步队列 | 一个 | 一个 |
当前线程释放锁并进入等待状态 | 支持 | 支持 |
等待状态不响应中断 | 支持 | 支持 |
超时等待状态 | 支持 | 支持 |
定时等待状态 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持 | 支持 |
唤醒等待队列中的全部线程 | 支持 | 支持 |
响应中断 | 不支持 | 支持 |
实现等待/通知模式 | 与Synchronize搭配使用 | 与Lock搭配使用 |