目录
1.1乐观锁&悲观锁
1.2轻量级锁&重量级锁
1.3自旋锁&挂起等待锁
1.4互斥锁&读写锁
1.5可重入锁&不可重入锁
1.6公平锁&非公平锁
1.7synchronized的特点
2.CAS(Compare and swap)
2.1.是什么
2.2.基于CAS方式实现的线程安全优缺点
2.3.使用场景
1.1乐观锁&悲观锁
两类锁,是在锁冲突的概率上进行区分的
乐观锁指的是预测锁竞争不是很激烈(做的工作相对少一些),悲观锁预测锁竞争会很激烈。
悲观锁认为多个线程访问同一个共享变量冲突的概率较大 , 会在每次访问共享变量之前都去真正加锁. 乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁 , 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突 . 悲观锁的实现就是先加锁( 比如借助操作系统提供的 mutex), 获取到锁再操作数据 . 获取不到锁就 等待. 乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突 .
1.2轻量级锁&重量级锁
是从锁开销的角度区分的
轻量级锁加锁解锁开销比较小,效率更高.重量级锁加锁解锁开销比较大,效率更低.
多数情况下,乐观锁也是一个轻量级锁,悲观锁也是一个重量级锁。
1.3自旋锁&挂起等待锁
自旋锁 :是典型的轻量级锁,是一种忙等的锁,当一个线程尝试获取自旋锁时,如果锁已被其他线程占用,该线程会一直循环检测(自旋)直到锁可用为止,而不是被挂起等待。自旋锁适用于锁的持有时间较短或者线程并发度较低的情况,因为忙等待会消耗CPU资源。
第一次获取锁失败 , 第二次的尝试会在极短的时间内到来。一旦锁被其他线程释放, 就能立即获取到锁,线程会一直持有该锁直到释放。忙等消耗CPU换来了更快的响应速度。挂起等待锁 :是典型的重量级锁,指当一个线程尝试获取锁时,如果锁已被其他线程占用,则该线程将会被挂起(即阻塞,阻塞的开销很大),直到锁可用为止。当锁可用时,系统会唤醒被挂起的线程继续执行。挂起等待锁可以减少CPU资源的消耗,适合于锁的持有时间较长或者线程竞争激烈的情况。
- 如果锁的竞争频率高、持有时间短,可以考虑使用自旋锁,避免线程频繁切换和挂起带来的开销。
- 如果锁的竞争不频繁、持有时间较长,可以考虑使用挂起等待锁,避免浪费CPU资源。
1.4互斥锁&读写锁
互斥锁:
- 互斥锁是最基本的同步工具,用于保护临界区(Critical Section),确保同一时刻只有一个线程可以访问共享资源,避免多个线程同时修改数据导致数据不一致。
- 当一个线程获取了互斥锁后,其他线程需要等待该线程释放锁才能访问被保护的资源。
- 互斥锁会引起线程阻塞,因为其他线程可能会竞争锁而处于忙等状态。
1.针对写加锁,在数据库事务的脏读中提起过,脏读就是一个事务读取到了另一个事务未提交的数据,采用了写加锁,写的时候不能进行读。
2.针对读加锁,在不可重复读中提起,指一个事务多次读取同一数据,而另一事务对该数据进行了修改,导致两次读取到的结果不一致,采用读加锁,读取的时候不能进行写。
多线程针对同一个变量并发读是没有线程安全问题的.也不需要加锁.读写锁就是把读操作和写操作分别进行加锁 .读锁和读锁之间不互斥 .写锁和写锁之间互斥 .写锁和读锁之间互斥 .读写锁最主要用在 " 频繁读 , 不频繁写 " 的场景中
假设一组线程并发读同一个变量,这时线程之间是没有锁竞争的,也没有线程安全问题!假设一组线程有读又有写,才会产生锁竞争..实际开发中,读操作非常高频
- 如果读操作非常频繁,而写操作较少,并且读操作之间没有依赖关系,那么读写锁可能是更好的选择。
1.5可重入锁&不可重入锁
一个线程针对同一把锁连续加锁两次如果不会出现死锁就是可重入锁,sychronized就是可重入锁,其中引入计数器,并对锁的持有线程进行身份标识,当发现当前加锁的线程就是持有锁的线程就直接进行+1操作。
1.6公平锁&非公平锁
主要区别在于获取锁的顺序以及线程等待锁时的公平性。
公平锁:注重顺序
- 公平锁是指多个线程按照它们请求锁的顺序获取锁,即先到先得。也就是说,当一个线程请求获取锁时,如果锁已经被其他线程占用,该线程会被放入等待队列,并在等待队列中按照先后顺序等待获取锁。
- 公平锁保证了所有线程获取锁的公平性,避免了某些线程长时间无法获取锁的情况,确保每个线程都有机会获取锁。
非公平锁:注重性能synchronized
- 非公平锁允许多个线程竞争锁,线程获取锁的顺序不再受到先后顺序的限制。当一个线程请求获取锁时,如果锁已经被其他线程占用,该线程可能会直接尝试获取锁,而不必进入等待队列。
- 非公平锁的优点在于可以提高系统的吞吐量,减少线程切换的开销。但是可能会导致某些线程长时间无法获取锁,出现“饥饿”现象。
1.7synchronized的特点
上述就是锁的基本特点,通过上述特点我们来描述synchronized的特点:
1.synchronized既是一个悲观锁,又是个乐观锁,自适应的!
synchronized默认是乐观锁,但是如果发现锁竞争比较激烈,就会变成悲观锁!!
2.synchronized既是轻量级锁,又是一个重量级锁,自适应!
synchronized默认是轻量级锁,当锁冲突剧烈后,就变成重量级锁!
3.synchronized这里的轻量级锁是基于自旋锁的方式实现的
synchronized这里的重量级锁是基于挂起等待锁的方式实现的
4.synchronized不是读写锁
5.synchronized是可重入锁
6.synchronized是非公平锁
2.CAS(Compare and swap)
2.1.是什么
通常用于实现乐观锁。CAS 操作包括三个步骤:比较、更新和写入。比较和交换的是内存和寄存器。CAS是一个CPU指令。一个CPU指令就可以完成上述操作,单个CPU指令是原子的即线程安全,可以使用cas操作完成一些操作进而代替“加锁”。CAS是CPU提供的一个特殊指令,通过这个指令,就可以一定程度的处理线程安全问题!
boolean CAS(V,A,B){if(A == V){V = B;return true;}return false; } //像是赋值
全称 Compare and swap, 即 " 比较并交换 ". 相当于通过一个原子的操作 , 同时完成 " 读取内存 , 比 较是否相等, 修改内存 " 这三个步骤 . 本质上需要 CPU 指令的支撑
2.2.基于CAS方式实现的线程安全优缺点
基于CAS方式实现的线程安全(无锁编程):
优点:保证线程安全避免阻塞;
缺点:代码复杂,只适合特定场景
CAS 操作是原子性操作,它可以保证在多线程环境下对共享数据的操作是线程安全的。如果 CAS 操作失败(即预期值和内存位置的值不相等),通常会进行重试或执行其他逻辑来处理并发冲突。在 Java 中,CAS 操作通常通过
java.util.concurrent.atomic
包中的原子类来实现,例如AtomicInteger
、AtomicLong
、AtomicReference
等。这些原子类提供了一系列 CAS 操作方法,如compareAndSet()
、getAndIncrement()
、getAndSet()
等,让开发者能够方便地利用 CAS 机制实现线程安全的操作。CAS 的优点包括无锁化、减少线程切换开销、提高并发度等;缺点则包括 ABA 问题、循环重试导致性能消耗等。因此,在使用 CAS 时需要注意处理可能出现的问题,并结合具体场景进行合理的选择。
2.3.使用场景
CAS本质上是CPU提供的指令,又被系统封装提供成api,然后被JVM封装成api,我们就可以使用了。
实现原子类标准库中提供了 java.util.concurrent.atomic 包 , 里面的类都是基于这种方式来实现的 .典型的就是 AtomicInteger 类 . 其中的 getAndIncrement 相当于 i++ 操作 .public class Test {public static void main(String[] args) throws InterruptedException {AtomicInteger count = new AtomicInteger(0);Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {count.getAndIncrement();//count++}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {count.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count.get());} }
getAndIncrement归于unsafe里面,因为稍有不慎就可能会出错。compareAndSwapInt为native方法即在JVM源码中通过c++实现。
public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1); }||\/public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;}||\/public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
伪代码实现原子类:
class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue+1) != true) {//通过重试判定了是否有穿插,oldValue = value;}return oldValue;} } //原子类基于CAS实现,synchronized通过阻塞,
自旋锁
public class SpinLock {private Thread owner = null;public void lock(){// 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就自旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){this.owner = null;} }
ABA问题
CAS操作中通过之没有发生变化作为没有其他线程穿插执行的判定。ABA问题是指在并发编程中,一个共享变量的值从 A 变成 B,再从 B 变成 A,期间可能会引发一些意外情况。这种情况通常发生在使用 CAS(Compare and Swap)操作时。
举个例子来说明ABA问题:
- 线程 T1 读取共享变量的值为 A。
- 线程 T2 将共享变量的值由 A 修改为 B,然后再修改回 A。
- 线程 T1 再次检查共享变量的值,发现仍然是 A,认为共享变量的值没有发生变化。
在上面的过程中,尽管共享变量经历了从 A 到 B,再到 A 的变化,但是线程 T1 并没有意识到这个过程,因此可能导致一些潜在的问题。
CAS 操作在读取旧值的同时 , 也要读取版本号 .真正修改的时候 ,如果当前版本号和读到的版本号相同 , 则修改数据 , 并把版本号 + 1.如果当前版本号高于读到的版本号 . 就操作失败 ( 认为数据已经被修改过了).