Java多线程--JUC-Lock锁(ReentrantLock、AQS)
共 4653字,需浏览 10分钟
·
2020-09-12 20:27
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
java.util.concurrent 在并发编程中使用的工具类,其重点有lock锁、辅助工具类、Atomic原子类以及并发集合框架等。
lock 最 常 用 的 类 就 是 ReentrantLock , 其 底 层 实 现 使 用 的 是AbstractQueuedSynchronizer(AQS)
Java是如何实现原子操作?
在Java中可以通过锁和循环CAS的方式来实现原子操作
如:Atomic原子类(循环CAS操作直到成功)
AQS(自旋、LockSupport、CAS)
AQS(AbstractQueuedSynchronizer)是一个抽象同步框架,可以用来实现一个依赖状态的同步器,也就是队列同步器。多线程访问共享资源的同步器。
AQS 有⼀个 state 标记位,值为1 时表示有线程占⽤,其他线程需要进⼊到同步队列等待,同步队列是⼀个双向链表。
当获得锁的线程需要等待某个条件时,会进⼊ condition 的等待队列,等待队列可以有多个。
当 condition 条件满⾜时,线程会从等待队列重新进⼊同步队列进⾏获取锁的竞争。
AQS 原理
Node内部类构成的⼀个双向链表结构的同步队列,通过控制(volatile的int类型)state状态来判断锁的状态,对于⾮可重⼊锁状态不是0则去阻塞;
对于可重⼊锁如果是0则执⾏,⾮0则判断当前线程是否是获取到这个锁的线程,是的话把state状态+1,⽐如重⼊5次,那么state=5。⽽在释放锁的时候,同样需要释放5次直到state=0其他线程才有资格获得锁
AQS加锁过程
AQS两种资源共享⽅式
Exclusive:独占,只有⼀个线程能执⾏,如ReentrantLock
Share:共享,多个线程可以同时执⾏,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
ReentrantLock(可重入锁)
什么是 “可重入”
可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁(如同计数器++而已),可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),一定程度上避免死锁。
ReentrantLock 是如何实现可重⼊性的 ?
内部⾃定义了同步器 Sync,加锁的时候通过CAS 算法 ,将线程对象放到⼀个双向链表 中,每次获取锁的时候 ,看下当前维 护的那个线程ID和当前请求的线程ID是否⼀样,⼀样就可重⼊了;
synchronized与ReentrantLock区别
✓都是可重⼊锁;ReentrantLock是显示获取和释放锁,synchronized是隐式;
✓ReentrantLock更灵活可以知道有没有成功获取锁,可以定义读写锁,是api级别,synchronized是JVM级别;
✓ReentrantLock可以定义公平锁;Lock是接⼝,synchronized是java中的关键字
ReentrantLock 就是基于 AQS 实现的,如下图所示,ReentrantLock 内部有公平锁和⾮公平锁两种实现,差别就在于新来的线程是否⽐已经在同步队列中的等待线程更早获得锁。
公平锁:非常公平, 不能够插队,必须先来后到!
非公平锁:非常不公平,可以插队 (默认都是非公平),但公平锁需多维护⼀个锁线程队列,效率低
从图中可以看到,ReentrantLock⾥⾯有⼀个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的⼤部分操作实际上都是在Sync中实现的。
它有公平锁FairSync和⾮公平锁NonfairSync两个⼦类。
ReentrantLock默认使⽤⾮公平锁,也可以通过构造器来显示的指定使⽤公平锁。
ReentrantLock原理(CAS+AQS)
CAS+AQS队列来实现
(1):先通过CAS尝试获取锁, 如果此时已经有线程占据了锁,那就加⼊AQS队列并且被挂起;
(2):当锁被释放之后, 排在队⾸的线程会被唤醒CAS再次尝试获取锁,
(3):如果是⾮公平锁, 同时还有另⼀个线程进来尝试获取可能会让这个线程抢到锁;
(4):如果是公平锁, 会排到队尾,由队⾸的线程获取到锁。
ReentrantLock如何避免死锁?
✓响应中断lockInterruptibly()
✓可轮询锁tryLock()
✓定时锁tryLock(long time)
tryLock 和 lock 和 lockInterruptibly 的区别
(1):tryLock 能获得锁就返回 true,不能就⽴即返回 false, (2):tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得
锁,返回 false
(3):lock 能获得锁就返回 true,不能的话⼀直等待获得锁
JUC辅助工具类—Semaphore(计数信号量)
和 ReentrantLock 实现⽅式类似,Semaphore 也是基于 AQS 的,差别在于 ReentrantLock 是独占锁,Semaphore 是共享锁。
什么是信号量Semaphore(共享锁)
信号量是⼀种固定资源的限制的⼀种并发⼯具包,基于AQS实现的,在构造的时候会设置⼀个值,代表着资源数量。信号量主要是应⽤于是⽤于多个共享资源的互斥使⽤,和⽤于并发线程数的控制(druid的数据库连接数,就是⽤这个实现的),信号量也分公平和⾮公平的情况,基本⽅式和reentrantLock差不多,在请求资源调⽤task时,会⽤⾃旋的⽅式减1,如果成功,则获取成功了,如果失败,导致资源数变为了0,就会加⼊队列⾥⾯去等待。调⽤release的时候会加⼀,补充资源,并唤醒等待队列。
过程:
semaphore.acquire() 获得,假设如果已经满了,等待,等待被释放为止!
semaphore.release(); 释放,会将当前的信号量释放 + 1,然后唤醒等待的线程!
作用:多个共享资源互斥的使用!并发限流,控制最大的线程数!
Semaphore 应⽤
acquire() release() 可⽤于对象池,资源池的构建,⽐如静态全局对象池,数据库连接池;
可创建计数为1的S,作为互斥锁(⼆元信号量)
JUC辅助工具类—CountDownLatch
允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助,用来协调多个线程之间的同步,或者说起到线程之间的通信。
能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。
过程:
countDownLatch.countDown(); // 数量-1
countDownLatch.await(); // 等待计数器归零,然后再向下执行
每次有线程调用 countDown() 数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续执行!
CountDownLatch的不足
CountDownLatch是一次性的,计算器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用
JUC辅助工具类—CyclicBarrier(加法计数器)
CyclicBarrier字面意思是“可重复使用的栅栏”,它是 ReentrantLock 和 Condition 的组合使用。
允许一组线程全部等待彼此达到共同屏障点的同步辅助。循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。
如旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始。
CyclicBarrier与CountDownLatch的区别
CountDownLatch和CyclicBarrier的比较
✓CountDownLatch是线程组之间的等待,即一个(或多个)线程等待N个线程完成某件事情之后再执行;而CyclicBarrier则是线程组内的等待,即每个线程相互等待,即N个线程都被拦截之后,然后依次执行。
✓ CountDownLatch是减计数方式,而CyclicBarrier是加计数方式。
✓CountDownLatch计数为0无法重置,而CyclicBarrier计数达到初始值,则可以重置。
✓CountDownLatch不可以复用,而CyclicBarrier可以复用。
ReentrantReadWriteLock读写锁(非公平锁)
对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了
ReentrantReadWriteLock 能够兼顾数据操作的原子性和读写的性能。
读可以被多线程同时读,但写只能一个线程写
独占锁(写锁) 一次只能被一个线程占有
共享锁(读锁) 多个线程可以同时占有
ReadWriteLock
读-读 可以共存!
读-写 不能共存!
写-写 不能共存!
读锁
读锁是一个共享锁。读锁是 ReentrantReadWriteLock 的内部静态类,它的 lock()、trylock()、unlock() 都是委托 Sync 类实现。
Sync 是真正实现读写锁功能的类,它继承AbstractQueuedSynchronizer
写锁
写锁是一个排他锁。写锁也是 ReentrantReadWriteLock 的内部静态类,它的 lock()、trylock()、unlock() 也都是委托 Sync 类实现。写锁的代码类似于读锁,但是在同一时刻写锁是不能被多个线程所获取,它是独占式锁。
写锁可以降级成读锁
锁降级
锁降级是指先获取写锁,再获取读锁,然后再释放写锁的过程 。锁降级是为了保证数据的可见性。锁降级是 ReentrantReadWriteLock 重要特性之一。
值得注意的是,ReentrantReadWriteLock 并不能实现锁升级
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获的读写锁,否则必须自旋,直到没有任何的写锁或者读锁存在。如果读写锁没有写锁,那么读锁可以立马获取,否则必须等待写锁释放。(但是有一个例外,就是读写锁中的锁降级操作,当同一个线程获取写锁后,在写锁没有释放的情况下可以获取读锁再释放读锁这就是锁降级的一个过程)
ReentrantReadWriteLock 读写锁适用于读多写少的场景,以提高系统的并发性
感谢点赞支持下哈