02-Java中的锁详解
I. 使用Lock接口
只要不涉及到复杂用法,一般采用的是Java的synchronized机制
不过,Lock可以提供一些synchronized不支持的机制
- 非阻塞的获取锁:尝试获取锁,如果能获取马上获取,不能获取马上返回,不会阻塞
- 中断获取锁:当获取锁的线程被中断时,抛出异常,锁被释放
- 超时获取锁:为尝试获取锁设定超时时间
相应API:
- void lock():普通的获取锁
- void lockInterruptibly() throws InterruptedException:可中断的获取锁,锁的获取中可以中断线程
- boolean tryLock():非阻塞获取锁
- boolean tryLock(long time, TimeUnit unit):超时获取锁
- void unlock():释放锁
一般框架:
//不要将lock写进try块,防止无故释放
Lock lock = new ReentrantLock();
lock.lock();
try{
...;
}finally{
lock.unlock();
}
II. 队列同步器AQS
AbstractQueuedSynchronizer:队列同步器,简称AQS,用来构建锁或者其他同步组件的基础框架
使用一个int的成员变量表示同步状态,通过内置的FIFO队列完成资源的排队工作
AQS实现锁可以看作:获取同步状态,成功则加锁成功;失败则加锁失败
调用AQS内部的获取同步状态的API,保证是线程安全的
- getState()
- setState(int newState)
- compareAndSetState(int expect, int update)
1. 自己实现一个Mutex互斥锁
首先要继承一个Lock接口,然后自己实现里面的方法
public class Mutex implements Lock {...}
Lock里面的方法是没有默认实现的,因此都需要重写
一般会实现一个继承于AQS的内部类来执行获取同步状态的实现:加锁相当于获取同步状态
public class Mutex implements Lock {
private static class Syn extends AbstractQueuedSynchronizer{...}
}
可以看到,AQS的方法和锁需要实现的方法是对应的
先实现对应的AQS的几个方法
private static class Syn extends AbstractQueuedSynchronizer{
//判断同步器是否被线程占用
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
//获取锁
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0,1)){
setExclusiveOwnerThread(Thread.currentThread()); //设置占用线程
return true;
}
return false;
}
//释放锁
@Override
protected boolean tryRelease(int arg) {
if(getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null); //清空占用线程
setState(0);
return true;
}
}
锁的获取和AQS获取同步状态其实是一个道理
通过代理模式可以像下面这样实现
public class Mutex implements Lock {
private static class Syn extends AbstractQueuedSynchronizer{...}
Syn syn = new Syn();
@Override
public void lock() {
syn.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
syn.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return syn.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return syn.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
syn.release(1);
}
@Override
public Condition newCondition() {
return null;
}
}
2. AQS实现分析
锁实现的本质:信号量机制,互斥锁也就是0和1两个信号量
AQS维护了一个FIFO的队列,线程获取同步状态失败则会加入这个队列,然后阻塞,直到同步状态释放,队列首节点的线程被唤醒
同步队列中的节点保存的信息有:获取同步状态失败的线程引用,等待状态,前驱和后继节点
同步器有一个头节点和尾节点
加入新的阻塞线程:
构造节点,加入队列的尾节点
使用compareAndSetTail()加到尾部,这是一个原子操作
2.1 独占式的获取和释放
获取同步状态:
acquire()方法会调用tryAcquire(),如果获取失败,则开始调用addWaiter()来给尾节点添加新节点,再调用acquireQueued()等待请求调度
addWaiter()的作用是给FIFO队列添加尾节点,并返回这个节点的引用
因为可能会多个线程申请失败,因此需要使用原子操作compareAndSetTail()
enq()的作用是快速添加失败后的反复尝试,直到添加尾节点成功
acquireQueued()用来请求调度
可见等待调度期间是支持中断的
这个请求调度有两个条件:
- 该节点是首节点
- 申请互斥信号量成功
for循环的这个操作被称为自旋
release()释放互斥信号量,根据上文提到的获取信号量,除了tryRelease(),还应该唤醒后继节点
2.2 共享式状态获取和释放
最典型的场景就是读写场景:一个资源允许多个线程进行读取,此时写线程阻塞;而写线程执行时,所有读线程阻塞
共享锁锁也就是资源信号量的应用,主要解决下面问题:只想要有限的线程执行
调用tryAcquireShared()来申请资源信号量
doAcquireShared()是申请失败后,构造节点加入FIFO队列然后自旋的操作
使用releaseShared()来释放
注意:共享式的释放可能有多个线程,需要用CAS操作来实现tryReleaseShared()
3. 自己实现一个TwinsLock共享锁
需要自己实现的:
tryAcquiredShared()
tryReleaseShared():要保证释放操作的原子性
State()的取值就是资源信号量的取值
public class TwinsLock {
private int count;
TwinsLock(int count){
this.count = count;
}
private final Sync sync = new Sync(count);
private static final class Sync extends AbstractQueuedSynchronizer{
Sync(int count){
if(count < 0) throw new IllegalArgumentException();
setState(count); //设置资源总数
}
@Override
protected int tryAcquireShared(int arg) {
for(;;){
int current = getState();
int newCount = current - arg;
if(newCount<0 || compareAndSetState(current, newCount)){
return newCount;
}
}
}
@Override
protected boolean tryReleaseShared(int arg) {
for(;;){
int current = getState();
int newCount = current + arg;
if(compareAndSetState(current, newCount)){
return true;
}
}
}
}
public void lock(){
sync.acquireShared(1);
}
public void unlock(){
sync.releaseShared(1);
}
}
III. 可重入锁
可重入锁:支持一个线程对资源反复加锁
synchronized支持可重入
ReentrantLock
是可重入锁的一种实现,支持反复加锁
锁的公平性:
- 公平:先对锁进行获取的请求先被满足
- 不公平:先对锁进行获取的请求不一定先被满足
1. 实现可重入
只需要判断当前线程是否是获取了锁的线程,如果是,则同步状态加一
每次释放同步状态减一,减到0的时候设置获取锁的线程为null,此时允许其他线程获取
接下来来看看ReetrantLock
的实现
2. 公平锁与非公平锁
继续观察nofairTryAcquire()方法,发现只要CAS成功,则线程直接获取到锁
而公平锁需要确定队列中没有前驱节点,即自己就是首节点
公平锁:确保线程的FIFO,先上下文切换开销大
非公平锁:可能造成线程饥饿,但线程切换少,吞吐量更大
IV. 读写锁
读写锁,是一种提供共享式和独占式两种方式的锁
- 支持公平锁和非公平锁
- 支持重进入
- 支持锁降级
一个资源允许多个线程进行读取,此时写线程阻塞;而写线程执行时,所有读线程阻塞
1. 读写锁的实现
读写锁的同步状态是按位切割使用的
维护了一个int型的同步状态,32位
高16为读状态,低16位为写状态
1.1 写锁的获取
w是c与0x0000FFFF做与运算后的值,w=0有两种情况:
- 有读锁,低16位全0
- 无读锁也无写锁,需要后面的条件判断是否为当前线程
1.2 读锁的获取
和写锁的获取类似,需要判断先有没有写锁
不过读锁是共享式的,可以允许多个线程获取读锁
不过读锁也支持重进入,因此不光要维护获取读锁的总状态,还要维护每个线程获取读锁的状态
2. 锁降级
锁降级指:线程先获取写锁,然后再获取读锁,最后释放写锁,实现从写锁降到读锁
目的:保证读写操作的连贯性
使用场景:写操作执行完马上需要读一次,不加读锁的话可能会被其他写线程修改,再读数据可能就变了
V. LockSupport工具
用于阻塞和唤醒线程
VI. Condition接口
Condition接口依赖于Lock对象,用于实现等待-通知模式
核心API就是两个,这两个API的扩展可以增加超时时间,设置中断不敏感等等:
- await()
- signal()
1. 使用Condition实现一个阻塞队列
队列满的时候,填充操作阻塞;队列空的时候,取出操作阻塞
public class BoundedQueue <T>{
private Object[] items;
private int addIndex, revIndex, count;
private ReentrantLock lock = new ReentrantLock();
private Condition empty = lock.newCondition();
private Condition full = lock.newCondition();
public BoundedQueue(int size){
items = new Object[size];
}
/**
* 添加元素
* @param t
*/
public void add(T t) throws InterruptedException {
lock.lock();
try{
while(count == items.length){
System.out.println("已满,请等待消耗");
empty.await();
}
items[addIndex] = t;
if(++addIndex == items.length) addIndex = 0;
count++;
full.signal();
}finally {
lock.unlock();
}
}
/**
* 取出元素
* @return
*/
public T remove() throws InterruptedException {
lock.lock();
try{
while(count == 0){
System.out.println("已空,请等待生产");
full.await();
}
Object temp = items[revIndex];
if(++revIndex == items.length) revIndex = 0;
count--;
empty.signal();
return (T) temp;
}finally {
lock.unlock();
}
}
}
2. Condition的实现分析
每个Condition会维护一个等待队列,一个锁支持支持多个等待队列
获取到锁的线程也就是同步队列的首节点
此时再调用await,则首节点进入等待队列,直到其他线程唤醒
相应的,调用signal则是将等待队列的首节点拆下来放到同步队列,唤醒线程开始自旋
当节点回到同步队列,之前调用的await()中的isOnsyncQueue()会返回true,结束等待,在调用acquireQueued()加入竞争
通过isHeldExclusively判断有没有拿到锁
02-Java中的锁详解的更多相关文章
- Java并发编程(十一)-- Java中的锁详解
上一章我们已经简要的介绍了Java中的一些锁,本章我们就详细的来说说这些锁. synchronized锁 synchronized锁是什么? synchronized是Java的一个关键字,它能够将代 ...
- java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock
原文:java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock 锁 锁是用来控制多个线程访问共享资源的方式,java中可以使用synch ...
- Java中日志组件详解
avalon-logkit Java中日志组件详解 lanhy 发布于 2020-9-1 11:35 224浏览 0收藏 作为开发人员,我相信您对日志记录工具并不陌生. Java还具有功能强大且功能强 ...
- java中的注解详解和自定义注解
一.java中的注解详解 1.什么是注解 用一个词就可以描述注解,那就是元数据,即一种描述数据的数据.所以,可以说注解就是源代码的元数据.比如,下面这段代码: @Override public Str ...
- Java中dimension类详解
Java中dimension类详解 https://blog.csdn.net/hrw1234567890/article/details/81217788
- [转载]java中import作用详解
[转载]java中import作用详解 来源: https://blog.csdn.net/qq_25665807/article/details/74747868 这篇博客讲的真的很清楚,这个作者很 ...
- JAVA中Object类方法详解
一.引言 Object是java所有类的基类,是整个类继承结构的顶端,也是最抽象的一个类.大家天天都在使用toString().equals().hashCode().waite().notify() ...
- Java中反射机制详解
序言 在学习java基础时,由于学的不扎实,讲的实用性不强,就觉得没用,很多重要的知识就那样一笔带过了,像这个马上要讲的反射机制一样,当时学的时候就忽略了,到后来学习的知识中,很多东西动不动就用反射, ...
- Java中的多线程详解
如果对什么是线程.什么是进程仍存有疑惑,请先Google之,因为这两个概念不在本文的范围之内. 用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现.说这个话其 ...
- Java中Volatile关键字详解
一.基本概念 先补充一下概念:Java并发中的可见性与原子性 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值, ...
随机推荐
- javascript/html 禁止图片缓存
更新图片, 如果图片的url没有改变, 刷新页面之后图片会使用缓存的图片 Solutions: * js改变图片链接 (添加get参数) // 假设当前这个图片的dom对象为img img.src + ...
- Jmeter压力测试学习7--压测带token的接口
前言 工作中我们需要压测的接口大部分都是需要先登陆后,带着token的接口(或者带着cookies),我们可以先登陆获取token再关联到下个接口.比如我现在要压测一个修改用户的个人的密码 场景案例 ...
- Python代码阅读(第10篇):随机打乱列表元素
本篇阅读的代码实现了随机打乱列表元素的功能,将原有列表乱序排列,并返回一个新的列表(不改变原有列表的顺序). 本篇阅读的代码片段来自于30-seconds-of-python. shuffle fro ...
- WebMagic 爬虫技术
WebMagic WebMagic 介绍 WebMagic基础架构 Webmagic 的结构分为 Downloader.PageProcessor.Scheduler.Pipeline四大组件,并由 ...
- 看动画学算法之:栈stack
目录 简介 栈的构成 栈的实现 使用数组来实现栈 使用动态数组来实现栈 使用链表来实现 简介 栈应该是一种非常简单并且非常有用的数据结构了.栈的特点就是先进后出FILO或者后进先出LIFO. 实际上很 ...
- windows下编译caffe出现错误 C4996: 'std::_Copy_impl': Function call with parameters that may be unsafe?
解决方案来自http://blog.csdn.net/u012556077/article/details/50353818
- maven配置下载包 解决SunCertPathBuilderException:unable to find valid certification path to requested target
解决 『SunCertPathBuilderException:unable to find valid certification path to requested target』 问题 ★ ...
- 题解 [HAOI2012]道路
题目传送门 题目大意 给出一个 \(n\) 个点 \(m\) 条边的有向图,问每一条边在多少个最短路径中出现. \(n\le 1500,m\le 5000\) 思路 算我孤陋寡闻了... 很显然,我们 ...
- 洛谷2149 Elaxia的路线(dp+最短路)
QwQ好久没更新博客了,颓废了好久啊,来补一点东西 题目大意 给定两个点对,求两对点间最短路的最长公共路径. 其中\(n,m\le 10^5\) 比较简单吧 就是跑四遍最短路,然后把最短路上的边拿出来 ...
- shell关键字含义
linux中shell变量$#,$@,$0,$1,$2的含义解释: 变量说明: $$ Shell本身的PID(ProcessID) $! Shell最后运行的后台Process的PID $? 最后运行 ...