> 文档中心 > 多线程(七)锁策略-cas和synchronized优化

多线程(七)锁策略-cas和synchronized优化

目录

  • 🍒 一,乐观锁和悲观锁
    • 1,乐观锁的理解
    • 2,悲观锁的理解
  • 🍌二,读写锁
    • 1,什么是读写锁
    • 2,读写锁的三种状态
    • 3,读写锁的实现
  • 🍇 三,公平锁和非公平锁
  • 🍉 四,可重入锁
    • 1,可重入锁
    • 2,可重入锁 VS 自旋锁
  • 🍓五,cas实现和synchronzed优化
    • 1,什么是CAS和底层实现原理
    • 2,CAS带来的三大问题
    • 3,synchronzed的优化

🍒 一,乐观锁和悲观锁

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的指令。这就是,仅仅一个线程争用锁的时候,就可以消除这个锁了,提升这段代码的执行的效率,因为可能就只有一个线程会来加锁,不涉及到多个线程竞争锁

郁金香导航