在并发情况下,多个线程会对同一个资源进行争抢,可能会导致数据不一致的问题。
为了解决这个问题,通过引入锁机制🔒,使用一种抽象的锁,对资源进行锁定,达到同步访问的目的。
实现
Java 采用了两种实现方式:
- 阻塞同步(😢 悲观锁) - 📝 适合于写多读少的场景。先加锁,保证写操作时的数据正确。 - 基于 - Object的- synchronized内部锁 🔒
- 基于 - API类库的- java.util.concurrent.locks.Lock🔒
 
- 非阻塞同步(😁 乐观锁) - 📖 适合于读多写少的场景。不加锁,可以大幅提高读的性能。 - 基于 CAS的乐观锁
 
- 基于 
😢 悲观锁

在 Java 中,每个对象(Object),都拥有一把锁,存放在对象头中,记录了当前对象被哪个线程所占用。
🔐 内部锁 synchronized
synchronized 关键字可以用来同步线程,其被编译后,会生成 monitorenter 和 moniterexit 两个字节码指令,用来进行线程的同步。
这两个字节码指令都需要一个 reference 类型的参数来指明要锁定和解锁的对象。
如果 Java 源码中的 synchronized 明确指定了对象参数,那就以这个对象的引用作为 reference;
如果没有明确指定,就根据 synchronized 修饰的方法类型(实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的 Class 对象来作为线程要持有的锁。
加锁方式
- 对象锁 - 修饰实例方法 - pubilc sychronized void method() { ... }
- 代码块锁住当前对象 - synchronized(this)
 
- 类锁 - 修饰静态方法 - public static synchronized void method() { ... }
- 代码块锁定 class 对象 - synchronized(MyClass.class) { ... }
 
synchronized 是非公平锁。一把锁只能被一个线程获取,没有获得锁的线程只能等待。线程对内部锁的申请和释放由 JVM 负责实施。
synchronized 是可重入锁。持有锁的对象,可以多次获得锁。
synchronized 修饰的方法,无论方法正常执行完还是抛出异常,都会释放锁。
🌸 JDK 1.6 对 synchronized 的优化
synchronized 依赖于 JVM,使用的是操作系统底层的 Mutex Lock 实现。
Java 中的线程都是与操作系统的原生线程一一对应的,如果要阻塞或唤醒一个线程,都需要依靠操作系统来实现。
操作系统线程之前的切换,都需要从用户态到内核太的转换,都是很耗时的操作,所以使用 synchronized 的成本较高。
JDK 1.6 对锁的实现引入了大量的优化,如 锁粗化,锁消除,轻量级锁,偏向锁,自旋锁,适应性自旋等。
并且,对象锁也引入了四种状态,会随着竞争情况逐渐升级(不可以降级),提高获取锁和释放锁的效率。
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
- 🔒 无锁状态 - 不对资源进行锁定 - 某些资源不会出现多线程竞争的情况,随意多个线程调用
 
- 🔒 偏向锁 - 只有一个线程访问同步代码块的场景
 - 在一些情况下,没有多线程的竞争,每次都是同一个线程多次获取锁,那么对象锁会“记住”这个线程,只要是这个线程过来,就直接把锁交出去 - 如果对象发现目前不是只有一个线程,而是有多个线程在竞争锁,偏向锁就会升级为轻量级锁 
- 🔒 轻量级锁 - 适合同步代码块的执行速度非常快的场景
 - 当锁升级为轻量级锁的时候,其他线程会通过 CAS 进行自旋等待来获取锁,不会阻塞,从而提高性能(并且会根据等待时间调整 CAS 的时间) 
- 🔒 重量级锁 - 适合同步代码块执行速度较长的场景
 - 对象锁状态被标记为重量级锁,通过 Monitor 来对线程进行控制 
😁 乐观锁 CAS

在一些情况下,同步代码块执行的时间远远小于线程切换的耗时。所以希望能够在用户态中对线程的切换进行管理,这样效率更高。
我们让线程“乐观地”反复尝试获取共享资源,当发现空闲时便进行使用,否则继续“乐观地”进行重试。
基于以上想法,诞生了 CAS (Compare And Swap) 算法:比较并交换。
CAS算法涉及到三个操作数:
- 需要读写的内存值 addr
- 进行比较的值 oldValue
- 要写入的新值 newValue
|  |  | 
这个 cas() 函数看起来没有任何同步措施,似乎还是存在线程不安全的问题。
当 A 线程比较了 oldValue 的值是想要的,但是这个瞬间,B 线程突然抢到了时间片,更改了值;但是 A 线程并不知道,将值改成了 newValue,这就出现了线程安全问题,A B 两个线程同时获得了资源。
所以,CAS 的操作必须是 原子性 的。这个原子操作,由计算机处理器指令集提供,直接由硬件保障。
Java 中的原子变量类
- 基础数据类型 - AtomicInteger
- AtomicLong
- AtomicBoolean
 
- 数组类型 - AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
 
- 字段更新类型 - AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicReferenceFieldUpdater
 
- 引用类型 - AtomicReference
- AtomicStampedReference
- AtomicMarkableReference
 
|  |  | 
AtomicInteger 主要由一个 Unsafe 类型的实例 unsafe 和 Long 类型的 offset 实现。
Java 通过 Unsafe 的 CAS 操作来对 volatile int 值进行更新。根据 value 在对象中的偏移量,CAS 操作内存数据,执行一些底层,和平台相关的方法。
|  |  | 
例如 incrementAndGet()。
|  |  | 
缺点:
- 循环开销大 
- 只能处理一个共享变量 - 封装成对象 AtomicReference
 
- ABA 
ABA 问题
CAS 需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。
如果内存值原来是 A,后来变成了 B,然后又变成了 A,那么 CAS 进行检查时会发现值没有发生变化,但是实际上是有变化的。
A -> B -> A
可以为共享变量引入一个修订号(时间戳),每次更新都会更新相应的修订号,判断变量的值是否被其他线程给修改过。AtomicStampedReference
[A, 0] -> [B, 1] -> [A, 2]
|  |  | 
