多线程(七)锁策略 (乐观锁,悲观锁,读写锁,自旋锁,可重入锁)

本文深入探讨了并发编程中的锁机制,包括乐观锁和悲观锁的概念及其区别,读写锁的工作原理及状态,公平锁与非公平锁的特性,可重入锁的实现与自旋锁的对比,以及CAS(Compare And Swap)原语在同步优化中的作用和潜在问题。通过示例代码展示了这些锁在Java中的实际应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

🍒 一,乐观锁和悲观锁

1,乐观锁的理解

乐观锁认为数据一般情况下不会发生冲突,所以只有当在更新提交时,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。乐观锁的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度

2,悲观锁的理解

悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样 别人想拿这个数据就会阻塞直到它拿到锁。 悲观锁的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。
应用:synchronized、Lock 都是悲观锁

🍌二,读写锁

1,什么是读写锁

读写锁就是⼀把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,⽽写锁则是互斥锁,不允许多个线程同时获得(写锁),并且写操作和读操作也是互斥的
⭐总结来说,读写锁的特点是: 读读不互斥、读写互斥、写写互斥

2,读写锁的三种状态

  • 读模式下加锁(读锁)
  • 写模式下加锁(写锁)
  • 不加锁模式下

3,读写锁的实现

/**
 * 演示读写锁的使用
 */
public class ReadWriteLock1 {
    public static void main(String[] args) {
        // 创建读写锁
        final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        // 创建读锁
        final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        // 创建写锁
        final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
        // 线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5,
                0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
        // 启动新线程执行任务【读操作】
        executor.submit(() -> {
            // 加锁操作
            readLock.lock();
            try {
                // 执行业务逻辑
                System.out.println("执行读锁1:" + LocalDateTime.now());
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                readLock.unlock();
            }
        });

        // 创建新线程执行写任务
        executor.submit(() -> {
            // 加锁
            writeLock.lock();
            try {
                System.out.println("执行写锁1:" + LocalDateTime.now());
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException exception) {
                exception.printStackTrace();
            } finally {
                // 释放锁
                writeLock.unlock();
            }
        });
    }
}

🍇 三,公平锁和非公平锁

  • 如果多个线程都在等待一把锁的释放,当锁释放之后,恰好又来了一个新的线程也要获取锁
  • 公平锁:能保证之前先来的线程优先获取锁(先到先得)
  • 非公平锁:新来的线程直接获取到锁,之前的线程还得接着等

🍉 四,可重入锁

1,可重入锁

可重入锁是指可以重新进入的锁,一个线程针对一把锁,连续两次加锁不会出现死锁,这种就是可重入锁
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized
关键字锁都是可重入的

2,可重入锁 VS 自旋锁

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

🍓五,cas实现和synchronzed优化

1,什么是CAS和底层实现原理

CAS是一条CPU并发原语,它的功能是判断内存中某个位置的值,是否为预期值,如果是预期值,则更新这个值,这个过程是原子性的

底层实现原理:
调用 Unsafe 类中的 CAS 方法,JVM 会帮我们实现出 CAS 汇编指令 这是一种完全依赖于硬件的功能,通过它实现原子操作。 原语的执行必须是连续的,在执行过程中不允许被中断,CAS 是 CUP 的一条原子指令

CAS实现:

/**
 * 基于 AtomicInteger 实现多线程自增同一个变量
 * CAS的使用(乐观锁)
 */
public class AtomiclntegerDemo {
    private static int number = 0;
    private static AtomicInteger atomicInteger = new AtomicInteger(0);
    private final static int MAX_COUNT = 100000;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i <=MAX_COUNT; i++) {
                atomicInteger.getAndIncrement();//++i
                //number++;
            }
        });
        thread.start();
        thread.join();
        System.out.println("最终结果" + atomicInteger.get());
    }
}

2,CAS带来的三大问题

  • CAS长时间不成功,就会消耗大量的CPU资源,Java中实现一直是通过自旋的方式获得锁
  • 只能保证一个共享变量的原子性
  • 引出了ABA的问题

引出ABA问题:

例如 转账问题,X 给 Y 转账,系统卡顿点击了两次转账按钮,X 原来是 300,正常是转完账(100元)还剩下200,第⼀次转账成功之后变成了 200,此时 Z 给 X 又转了 100 元,余额⼜变成了 300,第⼆次CAS 判断(300,300,200)成功,于是⼜扣了 100 元

ABA问题演示:

/**
 * ABA 问题演示
 */
public class ABADemo1 {
    private static AtomicInteger money = new AtomicInteger(100);
    public static void main(String[] args) throws InterruptedException {
        // 第 1 次点击转账按钮(-50)
        Thread t1 = new Thread(() -> {
            int old_money = money.get(); // 先得到余额
            // 执行花费 2s
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            money.compareAndSet(old_money, old_money - 50);
        });
        t1.start();
        // 第 2 次点击转账按钮(-50)【不小心点击的,因为第一次点击之后没反应,所以不小心又点了一次】
        Thread t2 = new Thread(() -> {
            int old_money = money.get(); // 先得到余额
            money.compareAndSet(old_money, old_money - 50);
        });
        t2.start();
        // 给账户 +50 元
        Thread t3 = new Thread(() -> {
            // 执行花费 1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int old_money = money.get();
            money.compareAndSet(old_money, old_money + 50);
        });
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println("最终账号余额:" + money.get());
    }
}

只能保证一个共享变量原子性:

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个方法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a合并一下ij=2a,然后用CAS来操作ij。从java1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

3,synchronzed的优化

从JDk 1.6开始,JVM就对synchronized锁进行了很多的优化。synchronized说是锁,但是他的底层加锁的方式可能不同,偏向锁的方式来加锁,自旋锁的方式来加锁,轻量级锁的方式来加锁

锁消除对synchronized锁做的优化:

在编译的时候,JIT会通过逃逸分析技术,来分析synchronized锁对象,是不是只可能被一个线程来加锁,没有其他的线程来竞争加锁,这个时候编译就不用加入monitorenter和monitorexit的指令。这就是,仅仅一个线程争用锁的时候,就可以消除这个锁了,提升这段代码的执行的效率,因为可能就只有一个线程会来加锁,不涉及到多个线程竞争锁

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喜羊羊zz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值