Java 多线程与锁策略的深入探讨
在 Java 的多线程编程中,锁策略、CAS(Compare and Swap)机制以及 synchronized
的优化过程是非常重要的概念。本文将对这些知识点进行总结和讲解,并加入一些代码示例以帮助理解。
一、锁策略
1. 悲观锁与乐观锁
-
悲观锁:总是假设最坏的情况,每次访问数据时都加锁,确保数据安全。
- 示例:
public class PessimisticLockExample { private final Object lock = new Object(); private int data; public void updateData(int newData) { synchronized (lock) { data = newData; // 加锁,确保数据安全 } }
}
乐观锁:假设不会发生冲突,只有在提交更新时才会检查是否有其他线程修改了数据。
- 示例:
public class OptimisticLockExample { private int data; public boolean updateData(int expectedValue, int newValue) { if (data == expectedValue) { data = newValue; // 直接更新数据 return true; } return false; // 更新失败 }
}
Synchronized 初始使⽤乐观锁策略. 当发现锁竞争⽐较频繁的时候, 就会⾃动切换成悲观锁策略.
2. 重量级锁与轻量级锁
锁的核⼼特性 "原⼦性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的.
- CPU 提供了 "原⼦操作指令".
- 操作系统基于 CPU 的原⼦指令, 实现了 mutex 互斥锁.
- JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.
- 重量级锁:依赖于操作系统提供的互斥锁,涉及到用户态和内核态的切换,成本较高。
- 轻量级锁:尽量在用户态完成加锁操作,只有在必要时才使用互斥锁。
synchronized 开始是⼀个轻量级锁. 如果锁冲突⽐较严重, 就会变成重量级锁.
3. 自旋锁
自旋锁是一种轻量级锁,线程在获取锁失败后不会进入阻塞状态,而是持续尝试获取锁,直到成功
- 示例:
public class SpinLock { private volatile Thread owner = null; public void lock() { while (!compareAndSetOwner(null, Thread.currentThread())) { // 自旋等待 } } public void unlock() { owner = null; } private boolean compareAndSetOwner(Thread expected, Thread newOwner) { if (owner == expected) { owner = newOwner; return true; } return false; }
}
如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌. 第⼀次获取锁失败, 第⼆次的尝试会 在极短的时间内到来.
⼀旦锁被其他线程释放, 就能第⼀时间获取到锁.
⾃旋锁是⼀种典型的 轻量级锁 的实现⽅式.
优点: 没有放弃 CPU, 不涉及线程阻塞和调度, ⼀旦锁被释放, 就能第⼀时间获取到锁.
缺点: 如果锁被其他线程持有的时间⽐较久, 那么就会持续的消耗 CPU 资源. (⽽挂起等待的时候是不 消耗 CPU 的).
synchronized 中的轻量级锁策略⼤概率就是通过⾃旋锁的⽅式实现的.
4. 公平锁与非公平锁
- 公平锁:遵循“先来后到”的原则,确保按照请求顺序获取锁。
- 非公平锁:不保证顺序,可能导致后来的线程优先获取锁。
操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是⾮公平锁. 如果要 想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序。
synchronized 是⾮公平锁.
5. 可重入锁
可重入锁指的是同一线程可以多次获取同一把锁而不会导致死锁。
Java⾥只要以Reentrant开头命名的锁都是可重⼊锁,⽽且JDK提供的所有现成的Lock实现类,包括 synchronized关键字锁都是可重⼊的。
⽽ Linux 系统提供的 mutex 是不可重⼊锁.
- 示例:
import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockExample { private final ReentrantLock lock = new ReentrantLock(); public void method() { lock.lock(); try { // 业务逻辑 method(); // 递归调用 } finally { lock.unlock(); } }
}
synchronized 是可重⼊锁
6. 读写锁
读写锁允许多个线程同时读取数据,但写操作是互斥的。
- 示例:
import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockExample { private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); private int sharedData = 0; public void readData() { readLock.lock(); try { // 读取共享数据 System.out.println("Reading data: " + sharedData); } finally { readLock.unlock(); } } public void writeData(int data) { writeLock.lock(); try { // 写入共享数据 sharedData = data; System.out.println("Writing data: " + sharedData); } finally { writeLock.unlock(); } }
}
二、CAS(Compare and Swap)机制
CAS: 全称Compare and swap,字⾯意思:”⽐较并交换“,⼀个 CAS 涉及到以下操作:
- ⽐较 A 与 V 是否相等。(⽐较)
- 如果⽐较相等,将 B 写⼊ V。(交换)
- 返回操作是否成功。
CAS 是一种乐观锁的实现方式,涉及三个操作:
- 比较内存中的值与预期值是否相等。
- 如果相等,则将新值写入内存。
- 返回操作是否成功。
- 示例:
public class CASExample { private volatile int value; public boolean compareAndSet(int expectedValue, int newValue) { if (value == expectedValue) { value = newValue; // 交换 return true; } return false; // 操作失败 }
}
针对不同的操作系统,JVM ⽤到了不同的 CAS 实现原理,简单来讲:
- ava 的 CAS 利⽤的的是 unsafe 这个类提供的 CAS 操作;
- unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
- Atomic::cmpxchg 的实现使⽤了汇编的 CAS 操作,并使⽤ cpu 硬件提供的 lock 机制保证其原⼦ 性。
1. 原子变量操作
Java提供了多种原子类(如AtomicInteger、AtomicLong、AtomicBoolean等),它们都使用CAS来实现原子更新操作。这些类在不使用锁的情况下提供了一种无锁的线程安全机制。
- 示例:
AtomicInteger atomicInteger = new AtomicInteger(0); public void increment() { atomicInteger.incrementAndGet(); // 使用CAS实现原子性
}
2. 自旋锁
自旋锁通过CAS循环不断尝试获取锁。与传统锁不同,自旋锁不会使线程进入阻塞状态。
- 示例:
public class SpinLock { private AtomicReference<Thread> owner = new AtomicReference<>(); public void lock() { Thread current = Thread.currentThread(); while (!owner.compareAndSet(null, current)) { // 自旋等待 } } public void unlock() { owner.set(null); }
}
3. ABA问题解决方案
CAS操作中的ABA问题是指:在一次CAS操作期间,一个变量可能被其它线程改变为另一个值,然后又恢复为原来的值。
为了解决这个问题,Java提供了AtomicStampedReference和 AtomicMarkableReference它们通过版本号(或标记)来避免ABA问题。
- 示例:
import java.util.concurrent.atomic.AtomicStampedReference; public class AtomicStampedReferenceExample { // 创建一个AtomicStampedReference对象,初始值为100,初始版本号为0 private AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(100, 0); // 更新操作 public void update() { // 用于存储当前版本号的数组 int[] stampHolder = new int[1]; // 获取当前值,并通过stampHolder获取当前的版本号 Integer value = stampedRef.get(stampHolder); // value为当前值,stampHolder[0]为当前版本号 // 计算新的版本号 int newStamp = stampHolder[0] + 1; // 新版本号为当前版本号加1 // 尝试使用CAS更新值和版本号 // 如果当前值和版本号分别等于value和stampHolder[0],则将值更新为value + 1,版本号更新为newStamp boolean updated = stampedRef.compareAndSet(value, value + 1, stampHolder[0], newStamp); // 结果:更新成功返回true,更新失败返回false if (updated) { System.out.println("Update successful: Value is " + stampedRef.getReference() + " with stamp " + stampedRef.getStamp()); } else { System.out.println("Update failed"); } } public static void main(String[] args) { AtomicStampedReferenceExample example = new AtomicStampedReferenceExample(); example.update(); // 对stampedRef进行更新操作 }
}
CAS为多线程编程提供了高效无锁的同步机制,广泛应用于需要多线程安全且高性能的场合。它通过乐观并发控制,允许多个线程同时操作数据,大大提高了系统的可伸缩性和响应速度。
3. synchronized的优化过程
synchronized
关键字是Java中用于实现同步的基本机制之一。在JDK 1.8中,synchronized
经历了一系列的优化,以提高其性能和效率。
1. 乐观锁与悲观锁
- 乐观锁:
synchronized
在初始阶段采用乐观锁的策略,假设不会发生锁竞争,因此不立即加锁。 - 悲观锁:如果锁竞争频繁,
synchronized
会转换为悲观锁,确保线程安全。
2. 轻量级锁与重量级锁
- 轻量级锁:在锁竞争不激烈的情况下,
synchronized
使用轻量级锁。轻量级锁通过CAS操作实现,避免了线程阻塞。 - 重量级锁:如果锁被持有的时间较长或竞争激烈,轻量级锁会膨胀为重量级锁,使用操作系统的互斥量来实现线程阻塞和唤醒。
3. 自旋锁
- 自旋锁:在实现轻量级锁时,
synchronized
大概率会使用自旋锁策略。自旋锁让线程在短时间内反复尝试获取锁,而不是立即进入阻塞状态,从而减少线程上下文切换的开销。
4. 不公平锁
- 不公平锁:
synchronized
是一种不公平锁,意味着线程获取锁的顺序不一定按照请求的顺序进行。这种策略可以提高吞吐量,但可能导致某些线程长期得不到锁。
5. 可重入锁
- 可重入锁:
synchronized
是可重入锁,同一线程可以多次获取同一把锁而不会导致死锁。这是通过在锁中记录持有锁的线程和计数器来实现的。
6. 锁消除
- 锁消除:编译器和JVM会判断某些锁是否可以消除。如果在单线程环境中使用了
synchronized
,编译器可能会优化掉这些不必要的锁。
7. 锁粗化
- 锁粗化:如果在一段逻辑中多次加锁和解锁,编译器和JVM会自动进行锁粗化,将多个锁操作合并为一个较大的锁操作,以减少频繁的加锁和解锁开销。
通过这些优化,synchronized
在JDK 1.8中变得更加高效,能够在保证线程安全的同时,尽量减少锁带来的性能损耗。这些优化使得synchronized
在许多情况下成为一个性能良好的同步机制。