一、CAS
1、CAS特点
CAS就是compare and swap(比较交换),是一种很出名的无锁的算法,就是可以不使用锁机制实现线程间的同步。使用CAS线程是不会被阻塞的,所以又称为非阻塞同步。CAS算法涉及到三个操作:三个操作数——内存位置、预期原值及新值
需要读写内存值V;进行比较的值A;准备写入的值B
当且仅当V的值等于A的值等于V的值的时候,才用B的值去更新V的值,否则不会执行任何操作(比较和替换是一个原子操作-A和V比较,V和B替换),一般情况下是一个自旋操作,即不断重试
特别注意:
- CAS 是一个原子的硬件指令完成的。CAS 的读内存,比较,写内存操作是一条硬件指令,是原子的。
- CAS 是直接读写内存的, 而不是操作寄存器。
- 当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。CAS 可以视为是一种乐观锁,或者可以理解成 CAS 是乐观锁的一种实现方式。
2、CAS的应用
标准库 java.util.concurrent.atomic 中的类,它们都是使用 CAS(Compare-And-Swap)技术实现的:
例如 AtomicInteger 类,这些类本身就是原子的,因此相关操作即使在多线程下也是安全的:
- num.getAndIncrement();// 此操作相当于num
- num.incrementAndGet();// 此操作相当于 num
- num.getAndDecrement();// 此操作相当于num–
- num.decrementAndGet();// 此操作相当于–num
测试原子类:
代码语言:javascript复制public class CASTest {
public static void main(String[] args) throws InterruptedException {
// 创建原子类,初始化值为0
AtomicInteger num = new AtomicInteger(0);
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i ) {
// num 操作
num.getAndIncrement();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i ) {
num.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(num);
}
}
num.getAndIncrement();操作伪代码:
代码语言:javascript复制import java.util.concurrent.atomic.AtomicInteger;
public class Main {
static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) {
int currentValue = num.get();
int incrementedValue = num.incrementAndGet();
System.out.println("Current value before increment: " currentValue);
System.out.println("Incremented value: " incrementedValue);
}
}
num.get()
用于获取当前值,num.incrementAndGet()
用于增加当前值并获取增加后的值。这两个操作是原子操作,也就是说,它们不会被线程调度机制打断。因此,num.getAndIncrement()
操作可以在多线程环境中安全使用。
3、CAS 实现自旋锁
代码实现:
代码语言:javascript复制import java.util.concurrent.atomic.AtomicBoolean;
public class SpinLock {
private AtomicBoolean locked = new AtomicBoolean(false);
public void lock() {
// 自旋锁的核心部分:一直循环尝试获取锁
while (!locked.compareAndSet(false, true)) {
// 等待,直到可以获取锁
Thread.yield();
}
}
public void unlock() {
locked.set(false);
}
}
原理说明:lock
方法尝试将locked
变量的值从false
更改为true
。如果成功,那么线程就获得了锁,可以继续执行。如果失败(也就是说,locked
变量的值已经是true
),那么线程就会在循环中等待,直到它可以获得锁。unlock
方法将locked
变量的值设回false
,释放锁。
4、CAS的ABA问题
CAS操作存在一个称为ABA问题。这个问题源于CAS操作的特性:在执行CAS操作时,如果内存位置V的值被其他线程更改,然后再次更改回原始值A,那么对于执行CAS操作的线程来说,内存位置V的值仍然与预期值A匹配,因此它会错误地认为没有其他线程修改过该值。
假设有一个共享变量count
,初始值为0。有两个线程A和B,它们都执行以下操作:
- 读取
count
的值; - 增加
count
的值; - 尝试使用CAS操作将
count
的新值写入内存。
假设线程A先读取count
的值为0,然后增加它的值为1,并尝试使用CAS操作将新值1写入内存。此时,如果线程B先读取count
的值为0,增加它的值为1,然后再次将count
的值更改回0,并尝试使用CAS操作将新值0写入内存。由于线程B的CAS操作成功,count
的值变为0,但是实际上count
的值应该为1。
解决方案:可以给每个变量加上一个版本号。当变量被修改时,版本号自增。在执行CAS操作时,除了比较变量的值是否与预期值匹配外,还需要比较版本号是否一致。如果版本号不一致,则说明变量已经被其他线程修改过,需要重新读取变量的最新值并重新尝试CAS操作。
二、synchronized 原理
1、synchronized 基本特征
结合以上的锁策略,我们就可以总结出,Synchronized 具有如下特性:
- 原子性:synchronized 关键字可以保证被其修饰的方法或代码块具有原子性,即一个或多个操作要么全部执行成功,要么全部执行失败。
- 可见性:synchronized 关键字可以保证当一个线程修改共享变量的值后,其他线程可以立即看到修改后的值。
- 有序性:synchronized 关键字可以保证程序的执行顺序按照代码的先后顺序执行,即同一线程中的所有操作都是有序执行的。
- 可重入性:synchronized 关键字还可以保证同一个线程可以多次获取同一个锁,不会产生死锁。
- 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁。
- 开始是轻量级锁实现,如果锁被持有的时间较长,就转换成重量级锁。
- 轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现。
2、synchronized 锁升级策略
从上述synchronized锁具有的策略可知,synchronized锁可根据实际场景进行锁升级,在JVM中对synchronized主要有以下锁升级策略:
synchronized 锁升级策略是Java6之后引入的概念,在Java6之前只有两种锁状态:无锁和重量级锁;当一个线程获得锁时,其他线程需要等待该线程释放锁后才能继续执行。这种策略存在一些问题,例如死锁和性能问题。
为了解决这些问题,Java 6 引入了偏向锁、轻量级锁和重量级锁三种锁升级策略。这些策略的目的是在保证线程安全的前提下,尽可能减少锁的竞争和系统的开销。
- 偏向锁:偏向锁是一种优化策略,它偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。当锁对象第一次被线程获取的时候,线程使用CAS操作把这个锁的线程ID记录再对象Mark Word之中,同时置偏向标志位1。当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。
- 轻量级锁:轻量级锁是偏向锁失败后的升级策略。当线程A获取轻量级锁时,会将synchronized对象头Mark Word复制一份到线程A在栈帧中创建的存储锁记录空间(即DispalcedMarkWord),然后使用CAS将对象头中的内容替换为线程A存储的所记录(DisplacedMarkWord)地址。如果线程A复制对象头时,线程B也准备获取锁,复制对象头到线程B的锁记录空间,当线程B进行CAS时发现,线程A已经将对象头替换,线程B的CAS失败,线程B尝试使用自旋等待线程A释放锁。如果线程B自旋次数到了上限,线程A仍没有释放锁,线程B仍在自选等待,此时,线程C又来竞争锁对象,轻量级锁会膨胀为重量级锁。
- 重量级锁:重量级锁是一种阻塞锁,它将未获得锁的线程阻塞,防止CPU空运行。重量级锁将未获得锁的线程挂起,并记录当前线程占用的资源,以便其他线程可以继续执行。当持有锁的线程释放锁时,被挂起的线程将被唤醒并重新竞争锁。这种策略可以有效地防止死锁和提高系统的性能。
3、synchronized 锁优化操作
- 减少锁的时间:不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放;
- 减少锁的粒度:它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;java中很多数据结构都是采用这种方法提高并发操作的效率。
- 锁消除:编译器和JVM检测到你加锁的某块代码不涉及线程安全问题,没必要加锁,就自动帮你消除了加锁的步骤。例如:单线程情况下,使用多线程安全的类,比如StringBuffer的append等等。
- 锁粗化:你一个线程重复多次的获取释放同一把锁,编译器和JVM就认为你可以一个获取到之后,直到全部事情做完再释放,而不再是做一部分就释放,然后再获取。