前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《多线程系列五》没人给你说的AQS,打通多线程编程

《多线程系列五》没人给你说的AQS,打通多线程编程

原创
作者头像
香菜聊游戏
修改2021-05-24 10:39:08
2750
修改2021-05-24 10:39:08
举报
文章被收录于专栏:香菜聊游戏香菜聊游戏
图片
图片

1、AQS 是什么?

AQS 是类 AbstractQueuedSynchronizer的简称,也是常用锁的基类,比如常见的ReentrantLock,Semaphore,CountDownLatch 等等。

AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。是Java提供的一种模板,一般在现有同步器无法完成的时候可以自行扩展。当然也可以自己实现,不过既然有现成的为什么还要自己瞎鸡儿写。

图片
图片

2、AQS 模型

图片
图片

注意:图来自网上,具体的原地点在哪我也不知道。如果有问题可以联系我。

解释:整个模型相当于在食堂吃饭,只开了一个窗口,只有一个打饭的师傅(state),所有想要吃饭的线程必须排队等待,直到轮到自己。

AQS就是基于队列,用共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。代码解决现实问题,现实问题催生解决方案。

3、AQS state

state 代表 共享资源和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

state的访问方式有三种:

  • getState()
  • setState()
  • compareAndSetState()

4、AQS 两种资源共享方式:

1.Exclusive:独占,只有一个线程能执行,如ReentrantLock

2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier。

5、模板方式实现自定义

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

  以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

  再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

  一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

6、锁的分类:公平锁和非公平锁,乐观锁和悲观锁

  • 公平锁:当线程A获取访问该对象,获取到锁后,此时内部存在一个计数器num+1,其他线程想访问该对象,就会进行排队等待(等待队列最前一个线程处于待唤醒状态),直到线程A释放锁(num = 0),此时会唤醒处于待唤醒状态的线程进行获取锁的操作,一直循环。如果线程A再次尝试获取该对象锁时,会检查该对象锁释放已经被占用,如果还是当前线程占用锁,则直接获得锁,不用进入排队。
  • 非公平锁:当线程A在释放锁后,等待对象的线程会进行资源竞争,竞争成功的线程将获取该锁,其他线程继续睡眠。

  公平锁是严格的以FIFO的方式进行锁的竞争,但是非公平锁是无序的锁竞争,刚释放锁的线程很大程度上能比较快的获取到锁,队列中的线程只能等待,所以非公平锁可能会有“饥饿”的问题。但是重复的锁获取能减小线程之间的切换,而公平锁则是严格的线程切换,这样对操作系统的影响是比较大的,所以非公平锁的吞吐量是大于公平锁的,这也是为什么JDK将非公平锁作为默认的实现。

  • 悲观锁:总是假设最坏的情况,每次想要使用数据的时候就恰好别人也要修改数据,一切是以安全第一,所以在每次操作资源的时候都会先加锁,不管有没有人抢,然后独占资源。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现
  • 乐观锁:乐观锁和悲观锁刚好相反,自己使用资源的时候没有人抢,所以不需要上锁。乐观锁的实现方案一般来说有两种:版本号机制CAS实现 。乐观锁多适用于多度的应用类型,这样可以提高吞吐量。

7、CAS

CAS(Compare And Swap),即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。sun.misc.Unsafe 类提供了硬件级别的原子操作来实现这个CAS。java.util.concurrent 包下的大量类都使用了这个 Unsafe.java 类的CAS操作。Unsafe 包是C++的接口实现,直接可以访问计算机的内存等敏感操作,所以标记为Unsafe。

CAS 是直接调用计算机的硬件指令,是硬件级别的线程同步,也是一种乐观锁。

8、总结

从整体上说了下AQS,没有深入到代码层级,先理解思想,后看具体实现。你理解了吗?下期具体分析一个CountDownLatch 源码,从细节理解AQS。

推荐阅读 点击标题可跳转

1、《多线程系列一》线程是什么?怎么理解多线程!

2、《多线程系列二》不理解future怎么能有future?

3、《多线程系列三》只会用,但是不懂的synchronized

4、《多线程系列四》解密线程池的所作所为

觉得本文对你有帮助?请分享给更多人

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、AQS 是什么?
  • 2、AQS 模型
  • 3、AQS state
  • 4、AQS 两种资源共享方式:
  • 5、模板方式实现自定义
    • 6、锁的分类:公平锁和非公平锁,乐观锁和悲观锁
    • 7、CAS
    • 8、总结
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档