在并发情况下,多个线程会对同一个资源进行争抢,可能会导致数据不一致的问题。
为了解决这个问题,通过引入锁机制🔒,使用一种抽象的锁,对资源进行锁定,达到同步访问的目的。
实现
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]
|
|