JAVA EE(9)——线程安全——锁策略&CAS

发布于:2025-03-16 ⋅ 阅读:(16) ⋅ 点赞:(0)

本文内容

1.锁策略:乐观/悲观,轻量/重量,自旋/挂起等待,读写,公平/非公平,可重入/不可重入,其他锁策略,Callable
2.CAS:原理,应用(原子类,自旋锁),ABA
3.JUC(java.util.concurrent) 的常见类:ReentrantLock,原子类,Semaphore,CountDownLatch
4.线程安全的集合类:多线程环境使用 ArrayList/队列/哈希表

一.锁策略

1.1各种锁策略介绍

乐观锁&悲观锁

1.1.1乐观锁&悲观锁

乐观锁和悲观锁只是锁的一种策略,并不是具体实现

乐观锁:
假设冲突概率低,先操作,更新时检查数据有没有被修改过,比如用版本号机制。更新数据时带上版本号,如果版本号不匹配,说明数据被修改过,这时候需要处理冲突,可以重试或者报错

悲观锁:
总是假设最坏的情况,每次在处理数据的时候都认为别人会修改,所以每次处理数据之前都会上锁,防止干扰

synchronized初始使用乐观锁策略,当发现锁竞争比较频繁的时候,,就会自动切换成悲观锁策略

1.1.2轻量级锁&重量级锁

锁的核心特性 “原子性”,这样的机制追根溯源是CPU这样的硬件设备提供的
在这里插入图片描述

  1.  重量级锁:严重依赖操作系统内核提供的互斥机制(mutex)
    
  2.  轻量级锁:尽量不依赖mutex,能在用户态解决就不切换内核态
    

当线程遇到轻量级锁时,会使用CAS指令快速获取锁,当获取成功后,该线程继续执行,如果获取失败,线程不会直接陷入阻塞,而是进入自旋状态,持续探测锁是否被释放了。但不会无限制的自旋下去,达到一定自旋次数时,停止自旋,进入阻塞状态。所以,轻量级锁和重量级锁是可以相互切换的。
重量级锁开销大的原因

  1.  上下文切换开销:线程被挂起和唤醒时,需要进行上下文切换,这涉及到保存和恢复线程的寄存器状态,程序计数器等,开销较大
    
  2.  操作系统内核介入:重量级锁的实现依赖于操作系统的内核函数调用,这会增加系统的调用开销。
    

1.1.3自旋锁&挂起等待锁

自旋锁是轻量级锁的具体实现,挂起等待锁是重量级锁的具体实现
自旋锁:自旋锁是一种忙等待的锁,当某线程尝试获取自旋锁时,如果该锁已经被其他线程持有,该线程不会陷入阻塞,而是会在一个循环中不断地检查锁是否被释放,这个过程不涉及内核态和用户态的切换,是轻量级锁的实现

挂起等待锁(也叫阻塞等待锁):当一个线程尝试获取一个已经被其他线程持有的锁时,该线程会被挂起,这个操作在内核态进行。这个过程涉及用户态和内核态的切换,线程的阻塞和唤醒,并且要保存该线程的上下文信息,会消耗性能,所以挂起等待锁是重量级锁的实现

1.1.4读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗,所以读写锁因此而产生
读写锁就是把读操作和写操作区分对待. Java 标准库提供了ReentrantReadWriteLock 类, 实现了读写锁.

  1.  ReentrantReadWriteLock.ReadLock类表示一个读锁,这个对象提供了 lock / unlock 方法进行加锁解锁
    
  2.  ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock /unlock 方法进行加锁解锁
    

其中:
读加锁和读加锁之间, 不互斥
写加锁和写加锁之间, 互斥
读加锁和写加锁之间, 互斥

1.1.5公平锁/非公平锁

公平锁: 线程陷入阻塞后进入阻塞队列(这需要额外的数据结构来实现,比如记录线程阻塞的时间,放进优先级队列中),在锁释放后按照先来后到的顺序获取锁
非公平锁:锁释放后操作系统会唤醒一个或者多个线程,这些线程同时竞争锁,不关心是哪个线程能获取锁

1.1.6可重入锁/不可重入锁

可重入锁:线程已经持有某个对象的锁,那么它可以再次获取该对象的锁,不会被阻塞。可重入锁通常会维护一个计数器,记录当前线程获取锁的次数。每次获取锁时,计数器加一;释放锁时,计数器减一。当计数器为零时,锁才真正被释放

不可重入锁:线程已经持有某个对象的锁,那么它可以再次获取该对象的锁,会被阻塞

1.2其他锁策略

锁消除
编译器+JVM 判断锁是否可消除.。如果可以,,就直接消除
什么是 “锁消除”
StringBuffer的代码中, 用到了 synchronized, 但单线程环境下不存在锁竞争,JVM就会自动消除synchronized
锁粗化
一段逻辑中如果出现多次加锁解锁,编译器 + JVM 会自动进行锁的粗化,例如
在这里插入图片描述

1.3Callable

Callable 是一个interface,相当于把线程封装了一个 “返回值”,方便程序员借助多线程的方式计算结果

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = () -> {
            int sum = 0;
            for (int i = 1; i <= 1000; i++) {
                sum += i;
            }
            return sum;
        };
        //FutureTask中的run方法会调用Callable中的call方法
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();
        //将call()方法的返回值保存起来,以便后续通过Future接口的get()方法获取
        System.out.println(futureTask.get());//futureTask.get() = 500500
    }
}

二.CAS

2.1什么是CAS?

CAS是Compare And Swap的缩写,意思是比较并交换。
CAS操作通过比较内存中的值(address)与预期值(expectedValue)是否相同,如果相同,则将内存中的值更新为新值;否则,不进行更新
下面是CAS的伪代码

boolean CAS(address, expectValue, swapValue) {
 if (address == expectedValue) {
   address = swapValue;
        return true;
   }
    return false;
}

当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线
程只会收到操作失败的信号

2.2CAS应用

2.2.1实现原子类

标准库中提供了java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的
典型的就是 AtomicInteger 类,其中的 getAndIncrement相当于 i++ 操作

AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();

伪代码实现原子类

class AtomicInteger {
	//内存中的值
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        //当value==oldValue时,oldValue+1并且结束循环
        //当value!=oldValue时,将value的值赋给oldValue,再次判断value是否等于oldValue
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}   

2.2.2实现自旋锁

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;
   }
}

2.3ABA问题

它指的是在多线程环境下,一个变量在某个时间点被修改为另一个值,然后又修改回原来的值。尽管变量的值最终没有变化,但这个过程中变量的状态可能已经发生了实质性的改变,而CAS操作无法检测到这种变化。
例如:
假设滑稽老哥有100存款,滑稽想从ATM取50 块钱,取款机创建了两个线程,并发的来执行-50
操作
我们期望一个线程执行-50成功,另一个线程-50失败
如果使用CAS的方式来完成这个扣款过程就可能出现问题
取款过程如下:

  1.  存款100:线程1获取到当前存款值为100, 期望更新为50;线程2获取到当前存款值为100,期望更新为 50
    
  2.  线程1执行扣款成功,存款被改成50;线程2阻塞等待中
    
  3.  在线程2执行之前,滑稽的朋友给他转账50,余额又变回100
    
  4.  轮到线程2执行了,发现当前存款为100,和之前读到的100相同,再次执行扣款操作
    

解决方案:引入版本号
版本1:
存款100:线程1获取到当前存款值为100, 期望更新为50;线程2获取到当前存款值为100,期望更新为 50
版本2:
线程1执行扣款成功,存款被改成50;线程2阻塞等待中
版本3:
在线程2执行之前,滑稽的朋友给他转账50,余额又变回100

轮到线程2 执行了, 发现当前存款为100,和之前读到的 100 相同。但是当前版本号为3, 之前读
到的版本号为1,版本小于当前版本,认为操作失败


网站公告

今日签到

点亮在社区的每一天
去签到