并发基础与CAS基本原理

💡 原文中文,约8300字,阅读约需20分钟。
📝

内容提要

Java中的线程与锁有6种状态:就绪、运行、等待、堵塞、超时等待和终止。线程进入堵塞态只能通过synchronized关键字,调用lock无法实现。开释锁的操作有多种情况,包括同步代码块执行完毕、遇到break或return、出现未处理的异常、调用wait()方法等。死锁的必要条件有互斥、请求和坚持、不可剥夺和循环等待。活锁是两个线程不断获取和释放锁而没有实际操作的情况。CAS(compare and swap)是一种原子操作,用于实现无锁编程。线程池和堵塞队列可以解决生产者和消费者的耦合问题。AQS(AbstractQueuedSynchronizer)是实现锁的基础结构,支持独占锁和共享锁。JMM(Java Memory Model)规定了线程对内存的访问规则。volatile关键字可以保证可见性,但不能保证原子性。锁的升级过程有倾向锁、轻量级锁、自旋锁和重量级锁。Synchronized是内置锁,ReentrantLock是显示锁。volatile在DCL(Double-Checked Locking)中起到禁止指令重排序的作用。sleep、wait和yield的区别是sleep让出CPU,wait让线程等待并释放锁,yield让出CPU但不释放锁。达观锁和失望锁的区别是达观锁会重新检查是否被修改。

🔎

延伸解读

线程状态与锁释放的细节

文章指出,线程进入堵塞态只能通过synchronized关键字,而调用Lock接口的方法只能进入等待或超时等待状态。堵塞是被动进入,等待是主动进入。释放锁的操作包括同步代码块执行完毕、遇到break或return、出现未处理的异常、调用wait()方法等。而调用sleep()、yield()或suspend()方法不会释放锁。理解这些细节有助于避免在多线程编程中错误地假设锁的行为。

CAS的原子性、问题与适用场景

CAS由CPU确保原子性,是实现无锁编程的基础。但它存在三个主要问题:ABA问题(可通过版本戳解决)、开销问题(线程不断重试消耗CPU)、只能保证一个共享变量的原子操作。对于代码块级别的原子性,CAS不适用,此时加锁机制更合适。文章还提到,volatile结合CAS在某些情况下可以替代重量级锁,但volatile本身不保证原子性。

锁升级过程与性能考量

为了减少线程上下文切换的开销,Synchronized引入了锁升级:偏向锁、轻量级锁、自旋锁、重量级锁。偏向锁适用于无竞争场景,一旦有竞争就升级为轻量级锁,通过CAS自旋获取;自旋超过一定次数后升级为重量级锁,线程被堵塞。文章特别指出,偏向锁撤销时可能发生Stop The World,因此在高竞争环境下建议禁用偏向锁。理解锁升级有助于优化并发性能。

AQS与可重入锁的实现机制

AQS(AbstractQueuedSynchronizer)是Java并发包的基础结构,内部使用state变量表示同步状态,并维护一个CLH队列管理线程排队。可重入锁通过记录线程获取锁的次数(state递增)来实现重入,释放时递减,直到state为0才真正释放锁。自定义同步工具类通常继承AQS并重写tryAcquire、tryRelease等方法。AQS支持独占和共享模式,是ReentrantLock、CountDownLatch等工具的核心。

❓

Q&A

Java中线程的状态有哪些?

Java中线程的状态包括就绪、运行、等待、堵塞、超时等待和终止。

什么是CAS操作,它的基本原理是什么?

CAS(compare and swap)是一种原子操作,由CPU确保,允许在多线程环境中安全地更新变量。

如何避免死锁?

避免死锁的策略包括有序资源分配法和银行家算法。

volatile关键字的作用是什么?

volatile关键字确保变量的可见性,禁止指令重排序,但不能保证原子性。

什么是线程池,它的优势是什么?

线程池是一种管理线程的机制,可以重用线程,减少创建和销毁线程的开销,提高系统性能。

sleep、wait和yield的区别是什么?

sleep让出CPU但不释放锁,wait让线程等待并释放锁,yield让出CPU但不释放锁。

➡️

继续阅读