From 444fe4183afea13b3784592a53a7b8a1601bb0f3 Mon Sep 17 00:00:00 2001 From: Omooo <869759698@qq.com> Date: Wed, 16 Jan 2019 19:13:53 +0800 Subject: [PATCH] =?UTF-8?q?Create=20=E9=94=81.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blogs/Java/锁.md | 145 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 blogs/Java/锁.md diff --git a/blogs/Java/锁.md b/blogs/Java/锁.md new file mode 100644 index 0000000..8dd18db --- /dev/null +++ b/blogs/Java/锁.md @@ -0,0 +1,145 @@ +--- +synchronized、CAS、AQS +--- + +#### 目录 + +1. 思维导图 +2. 概述 +3. 常见术语 + - 悲观锁/乐观锁 + - 公平锁/非公平锁 + - 可重入锁/不可重入锁 + - 死锁 +4. synchronized + - 使用方式 + - 实现原理 + - 锁优化 +5. CAS +6. AQS + - Lock + - ReadWriteLock +7. 参考 + +#### 思维导图 + +#### 概述 + +Java 中的并发锁大致上可以分为隐式锁和显式锁两种。隐式锁的代表就是 synchronized 关键字,显式锁主要包含两个接口:Lock 和 ReadWriteLock,主要实现类分别为 ReentrantLock 和 ReentrantReadWriteLock,这两个类都是基于 AQS(AbstractQueuedSynchronizer)队列同步器实现的,还有的地方把 CAS 也称为一种锁,在包括 AQS 在内的很多并发相关类中,CAS 都扮演了很重要的角色,比如 AtomicInteger 等原子操作类。 + +#### 常见术语 + +##### 悲观锁和乐观锁 + +悲观锁和独占锁是一个意思,它假设一定会发生冲突,因此获取到锁之后会阻塞其他等待线程。这么做的好处是简单安全,但是挂起线程和恢复线程都需要转入内核态进行,这样做会带来很大的性能开销。悲观锁的代表是 synchronized,然鹅在真实环境中,大部分时候都不会产生冲突。而乐观锁不一样,它假设不会产生冲突,先去尝试执行某项操作,失败了再进行其他处理(一般都是不断循环重试),这种锁不会阻塞其他线程,也不涉及上下文切换,性能开销小,代表实现是 CAS。 + +##### 公平锁和非公平锁 + +公平锁是指各个线程在获取锁前先检查有无排队的线程,按排队顺序去获取锁。非公平锁是指线程获取锁前不考虑排队问题,直接尝试获取锁。值得注意是,在 AQS 的实现中,一旦线程进入排队队列,即使是非公平锁,线程也得乖乖排队。 + +##### 可重入锁和不可重入锁 + +如果一个线程已经获取到了锁,那么它可以访问被这个锁锁住的所有代码块,不可重入锁与之相反。可重入锁包括 synchronized 和 ReentrantLock。 + +##### 死锁 + + + +### synchronized + +悲观、可重入,已经不那么重量级的锁。 + +##### 使用方式 + +- 修饰静态方法,锁 class 对象 +- 修饰非静态方法,锁当前实例对象 +- 修饰代码块,锁的是代码块里的对象 + +每个对象都有一个锁和一个等待队列,锁只能被一个线程持有,其他需要获取锁的线程需要阻塞等待,锁释放之后,对象会从队列中取出一个线程并唤醒,唤醒哪个线程是不确定的,不保证公平性。 + +多个线程是可以同时执行同一个 synchronized 实例方法的,只要它们访问的对象是不同的。 + +synchronized 锁住的是对象而非代码,只要访问的是同一个对象的方法,都得顺序访问。 + +##### 实现原理 + +synchronized 是基于 Java 对象头和 Monitor 机制来实现的。 + +- Java 对象头 + + 一个对象在内存中包含三个部分:对象头、实例数据、对其填充。其中 Java 对象头包含两个部分: + + - Class Metadata Adderess 类型指针 + + 存储类的元数据的指针,虚拟机通过这个指针找到它是哪个类的实例。 + + - Mark Work 标记字段 + + 存储对象自身的运行时数据,包括 hashCode、GC 分代年龄、锁状态标志等。 + +- Monitor + + Mark Word 有一个字段指向 monitor 对象,monitor 中记录了锁的持有线程,等待的线程队列等信息,前面说的每个对象都有一个锁和一个等待队列,就是在这实现的。 + +那 JVM 是如何把 synchronized 与 monitor 关联起来的呢?可以分为两种情况: + +1. 同步代码块,编译时会直接在同步代码块前加上 monitorenter 指令,代码块后加上 monitorexit 指令。 +2. 同步方法,虚拟机会为方法设置 ACC_SYNCHRONIZED 标志,在调用方法的时候根据这个标志判断是否是同步方法。 + +##### 锁优化 + +JDK 1.6 对锁的实现引入了大量的优化,包括自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁来减少锁操作的开销。 + +锁主要存在四种状态:依次是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁可以升级但是不能降级,这种策略是为了提高获取锁和释放锁的效率。 + +- 重量级锁 + + 重量级锁就是采用互斥量 Mutex 来控制对互斥资源的访问,在 Java 中被抽象为监视器锁 Monitor,这种同步方式成本非常高,包括内核态到用户态切换,性能差。 + +- 自旋锁与自适应自旋 + + 在许多应用中,锁定状态只会持续很短的时间,为了这么一点时间就去挂起、恢复线程,不值得。我们可以让等待线程执行一定次数的循环,在循环中去获取锁,这项技术称为自旋锁,它可以节省系统切换线程的消耗,但仍然占有处理器。在之后又引入自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 + +- 锁消除 + + 虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。 + +- 锁粗化 + + 当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如 StringBuffer 的 append 操作。 + +- 轻量级锁 + + 对绝大部分锁来说,整个同步周期内不存在竞争,如果没有竞争,轻量级锁可以使用 CAS 操作来避免使用互斥量的开销。 + +- 偏向锁 + + 偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁。 + +#### CAS + +CAS 即 CompareAndSwap 比较并替换。CAS 机制当中使用了三个基本操作数:内存地址 V,旧的预期值 A,要修改的新值 B。更新一个变量的时候,只有当变量的预期值 A 和内存地址 V 和实际值相同时,才会将内存地址 V 对应的值修改为 B。 + +但是问题也是显而易见的: + +1. ABA 问题 + + 加一个版本号。 + +2. 不能保证代码块的原子性 + +3. 在多次尝试更新一个值时不成功,CPU 开销大 + +在 Java 并发包中 AtomicInteger、等都是它的实现。 + +#### AQS + + + +#### 参考 + +[从 synchronized 到 CAS 和 AQS - 彻底弄懂 Java 各种并发锁](https://juejin.im/post/5c37377351882525ec200f9e) + +[synchronize早已经没那么笨重](https://juejin.im/post/5bff854b5188250e8601ec90) + +[什么是 CAS 机制?](https://www.jianshu.com/p/41216f83c0e1) \ No newline at end of file