悲观锁:在每次拿数据的时候都会上锁。
乐观锁:在数据提交更新的时候,才会正式对数据是否冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
二、读写锁synchronized初始使用乐观锁策略。当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略。
读写锁就是把读操作和写操作区分对待。
读读不互斥;
读写/写写互斥;
三、重量级锁和轻量级锁Synchronized不是读写锁
重量级锁:加锁机制重度依赖OS提供的mutex互斥锁。
轻量级锁:加锁机制尽可能不适用mutex,而是尽量在用户态代码完成。
如果获取锁失败,立即再次尝试获取锁,无线循环,直到获取到锁为止。第一次取锁失败,第二次的尝试会在极短的时间内到来。
优点:没有放弃CPU,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁。
缺点:如果锁被其他线程持有的时间比较久,那么就会持续消耗CPU的资源。
公平锁: 遵守 “先来后到”. B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
非公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁.
六、可重入锁 和不可重入锁synchronized 是非公平锁.
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。不可重入锁在递归过程中这个锁会阻塞自己。
七、CASJava里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括
synchronized关键字锁都是可重入的
Linux 系统提供的 mutex 是不可重入锁
- CAS: 全称Compare and swap
乐观锁基于CAS机制实现。
一个 CAS 涉及到以下操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
(1) 比较 A 与 V 是否相等。(比较)
(2) 如果比较相等,将 B 写入 V。(交换)
(3)返回操作是否成功
- CAS 的 ABA 问题
- ABA 的问题:
假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A;
接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要
先读取 num 的值, 记录到 oldNum 变量中;
使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z;
但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A; - 解决方案
给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
CAS 操作在读取旧值的同时, 也要读取版本号.
真正修改的时候,
如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了)
- Synchronized 基本特点
结合上面的锁策略, 我们就可以总结出, Synchronized 具有以下特性(只考虑 JDK 1.8):
(1) 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
(2) 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
(3) 实现轻量级锁的时候大概率用到的自旋锁策略
(4) 是一种不公平锁
(5) 是一种可重入锁
(6) 不是读写锁 - 加锁过程
JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。
(1) 偏向锁
第一个尝试加锁的线程, 优先进入偏向锁状态.偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程.
(2) 轻量级锁
随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).
此处的轻量级锁就是通过 CAS 来实现。
(3) 重量级锁
如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁
此处的重量级锁就是指用到内核提供的 mutex



