总结锁策略, cas 和 synchronized 优化过程

本文详细介绍了并发编程中的各种锁策略,如乐观锁、悲观锁、读写锁、重量级锁/轻量级锁、自旋锁、公平锁/非公平锁和可重入锁,以及CAS(CompareandSwap)的概念及其应用。还讨论了Synchronized原理和锁消除/粗化的优化技术。
摘要由CSDN通过智能技术生成

一.常见锁策略

1.乐观锁和悲观锁

乐观锁:
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做

悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

2.读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。

一个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.

  • 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
  • 两个线程都要写一个数据, 有线程安全问题.
  • 一个线程读另外一个线程写, 也有线程安全问题.
  • ReentrantReadWriteLock.ReadLock 
    //类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁
    ReentrantReadWriteLock.WriteLock 
    //类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁

3.重量级锁&轻量级锁

  1. 重量级锁:重量级锁是一种基于操作系统的互斥量(Mutex)实现的锁,也称为内核级锁。在使用重量级锁时,当一个线程获取到锁时,会进入内核态,并将其它线程在用户态下挂起,直到拥有锁的线程释放锁。重量级锁的实现通常涉及系统调用,需要进行用户态与内核态之间的切换,因此开销相对较大。
  2. 轻量级锁:轻量级锁是针对多线程环境下的短暂互斥访问优化的锁机制。它通过在对象头中加入一些标志位来实现,避免了重量级锁的开销。轻量级锁的工作原理是,当线程尝试获取锁时,首先会尝试通过CAS(Compare and Swap)操作来将对象头中的锁标志位设置为自己的线程ID,如果成功则表示获取锁成功。如果CAS操作失败,可能意味着其他线程正在竞争锁,此时会膨胀为重量级锁。

4.自旋锁

  1. 自旋锁:是线程获取锁时不会立即阻塞,而是通过循环的方式去得到锁,这样做可以减少上下文的切换
  2. 读自旋锁的缺点:缺点其实非常明显,就是如果之前的假设(锁很快会被释放)没有满足,则线程其实是在消耗 CPU 资源

5.公平锁和非公平锁

公平锁: 遵守 "先来后到". 例如B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
非公平锁: 不遵守 "先来后到". 例如B 和 C 都有可能获取到锁

注意:

  • 操作系统内部的线程调度就可以视为是随机的。 如果不做任何额外的限制, 锁就是非公平锁。 如果要想实现公平锁, 就需要依赖额外的数据结构来记录线程们的先后顺序.
  • 公平锁和非公平锁没有所谓的好坏之分, 关键还是看适用场景
  • synchronized就是非公平锁。

6.可重入锁和不可重入锁

可重入锁是指可以重新进入的锁,一个线程针对一把锁,连续两次加锁不会出现死锁,这种就是可重入锁

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类。
synchronized关键字锁都是可重入的

二、CAS

CAS 可以视为是一种乐观锁.
全称 Compare and swap, 即 "比较并交换". 相当于通过一个原子的操作, 同时完成 "读取内存, 比
较是否相等, 修改内存" 这三个步骤. 本质上需要 CPU 指令的支撑.

CAS 有哪些应用

1) 实现原子类

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

public class AtomiclntegerDemo {
    private static int number = 0;
    private static AtomicInteger atomicInteger = new AtomicInteger(0);
 
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i <=100000; i++) {
                //number++;
                atomicInteger.getAndIncrement();//++i 
                //AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.
            }
        });
        thread.start();
        thread.join();
        System.out.println("最终结果" + atomicInteger.get());
    }
}
 

2) 实现自旋锁

基于 CAS 实现更灵活的锁, 获取到更多的控制权

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

 CAS 的 ABA 问题

ABA问题是指在并发环境中,一个变量的值经过多次修改后,最终又回到了原始值,但是这期间可能发生了其他的修改操作,导致操作的结果出现了意外。

以下是ABA问题的示例和解决方法:

假设有两个线程T1和T2同时对某个变量进行操作:

  1. 初始状态,变量的值为A。
  2. T1将变量的值从A修改为B。
  3. T1将变量的值从B修改回A(此时变量的值又回到了初始状态,虽然经历了AB两个值)。
  4. T2将变量的值从A修改为C。

在这个过程中,变量的值经历了ABA的改变,但是最终的结果看起来和初始状态一样,可能导致一些意外的问题。比如,一个线程在判断变量的值是否发生变化时,如果只关注变量的值是否和初始状态A一致,那么就有可能会忽略在中间经过了B的修改,而产生错误的判断。

为了解决ABA问题,可以使用以下方法:

  1. 版本号或时间戳:为变量引入版本号或时间戳,每次对变量的修改都会更新版本号或时间戳。当进行CAS操作时,不仅要比较值是否一致,还要比较版本号或时间戳是否一致,从而确保变量没有经历过其他的修改。

三、Synchronized 原理


 
public class 语法 {

    //synchronized语法一:同步代码块
        Object 某个对象 = new Object();
        synchronized (某个对象){
 
    //语法二:同步实例方法
    public synchronized void t1(){
 
    }
    //等同于: this是指
    // 当前线程(谁(当前类的某个实例对象)调用我(实例方法),this就是谁)
    public void t1_equals(){
        synchronized(this){
 
        }
    }
 
    //语法三:静态同步方法
    public static synchronized void t2(){
        //代码行:
    }
    //等同于
    public static void t2_equals(){
        synchronized(语法.class){
            //代码行
        }
    }
 
    //类加载:还会在堆中,生成一个类对象
    public static void main(String[] args) {
        Class c = String.class;
        //执行当前类的类加载:在堆中,会生成一个语法.class的类对象
        System.out.println(语法.class == 语法.class);
        //以上代码等同于
        Class<语法> c1 = 语法.class;
        Class<语法> c2 = 语法.class;
        System.out.println(c1 == c2);
 
       
 
        }
        //当然也可以使用类对象
        synchronized (语法.class){
 
        }
    }
}

基本特点

  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
  2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
  3. 实现轻量级锁的时候大概率用到的自旋锁策略
  4. 是一种不公平锁
  5. 是一种可重入锁
  6. 不是读写锁

优化:

锁消除

  编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.

package sync优化;
 
public class 锁消除 {
 
    public static void main(String[] args) {
        //局部变量只有当前方法执行的线程持有(不可能有其他线程持有)
        //也就不存再线程安全问题:jvm给append中synchronized加锁释放锁
        // 优化方案,就是“锁消除”=>不加锁
        StringBuffer sb = new StringBuffer();
        sb.append("a");
        sb.append("b");
        sb.append("c");
        System.out.println(sb.toString());
    }
}
 
 

此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加锁解锁操作是没有必要的, 白白浪费了一些资源开销。这就来到了锁粗化。

锁粗化

   一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化

for(int i = 0;i < 10000;i++) {
    synchronized (synchronized线程安全.class) {
        count++;
    }
}
 
synchronized (synchronized线程安全.class) { //加锁
    for(int i = 0;i < 10000;i++) {
        count++;
    }
}// 释放锁
 
 
    public static void main(String[] args) {
        StringBuffer sb = new StringBuffer();
        sb.append("a");
        sb.append("b");
        sb.append("c");
        //其中连续三次同时append,加锁---释放锁。
        System.out.println(sb.toString());
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值