Java~今日学习各种锁策略(乐观锁 悲观锁 读写锁等等)、CAS机制和synchronize的原理及其优化机制(锁消除 偏向锁 自旋锁 膨胀锁 锁粗化)

锁策略

乐观锁 VS 悲观锁

  • 乐观锁:乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
  • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

悲观锁的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。
乐观锁的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度。

读写锁

  • 把加锁操作分成了俩种 一是读锁二是写锁 也就是说在读和读之间是没有互斥的 但是在读写和写写之间就会存在互斥
  • 如果一个场景是一写多度 那么使用这个效率就会很高

重量级锁 VS 轻量级锁

  • 首先我们要知道加锁有一个很重要的特性就是保证原子性 原子性的功能其实来源于硬件(硬件提供了相关的原子操作的指令, 操作系统把这些指令统一封装成一个原子操作的接口, 应用程序才能使用这样的操作)
  • 所以在加锁过程中 如果整个加锁逻辑都是依赖于操作系统内核 那此时就是重量级锁(代码在内核中的开销会很大) 如果大多数操作都是用户自己完成的 少数由操作系统内核完成 这种就是轻量级锁

挂起等待锁 VS 自旋锁

  • 挂起等待锁表示当前获取锁失败后, 对应的线程就要在内核中挂起等待 (放弃CPU进入等待队列) 需要在锁对象释放之后由操作系统唤醒 (通常都是重量级锁)
  • 自旋锁表示当前获取锁失败后 不是立刻放弃CPU 而是快速频繁的再次访问锁的持有状态, 一旦锁对象被释放就能立刻获取到锁(通常都是轻量级锁)

自旋锁的效率更高, 但是会浪费一些CPU资源 (自旋相当于CPU在那空转)

公平锁 VS 非公平锁

  • 这种情况就是如果已经有多个线程在等待一把锁的释放 当释放之后, 恰好又来了一个新的线程也要获取锁
  • 公平锁: 保证之前先来的线程优先获取锁
  • 非公平锁: 新来的线程直接获取到锁, 之前的线程还得接着等待

实现公平锁就需要付出一些额外的代价 所以公平锁的效率是略低于非公平锁的

可重入锁

  • 一个线程针对同一把锁连续加锁俩次, 不会死锁, 这种就是可重入锁

可重入锁这就像是大门的三保险锁一样 我锁一层再锁一层 这种并不会造成我们死锁住自己 因为当我们想出去的时候又可以一层一层的开锁

死锁

  • 死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
  • 我们常说的死锁有三个经典场景
  1. 一个线程一把锁 连续加锁俩次才 (保证使用的不是可重入锁)
  2. 俩个线程, 俩把锁, 相互获取对方的锁
  3. n个线程, n把锁, 哲学家就餐问题
  • 死锁产生的四个必要条件:(较为理论简单了解)

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

CAS

  • CAS的全称是 compare and swap(字面意思就是比较交换) 他是基于硬件提供的一种基础指令, 也是基于这样的指令, 就可以实现一些特殊的功能(实现锁)
  • 针对不同的操作系统,JVM 用到了不同的 CAS 实现原理
  • 简而言之,是因为硬件予以了支持,软件层面才能做到。

我们假设内存中的原数据val,旧的预期值new,需要修改的新值tmp。

  1. 比较 new 与 val 是否相等。(比较)
  2. 如果比较相等,将 tmp 写入 val。(交换)
  3. 返回操作是否成功。
  • 可见当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。

CAS的使用

在这里插入图片描述
上图所示就是使用的CAS封装了一些原子类如下面代码示例第一个使用CSA的锁第二个不使用 显然结果第一个是线程安全的第二个线程是不安全的

import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created with IntelliJ IDEA.
 * Description: If you don't work hard, you will a loser.
 * User: Listen-Y.
 * Date: 2020-08-04
 * Time: 20:40
 */
public class Demo2 {

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger();

        Thread thread = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    atomicInteger.addAndGet(1);
                }
            }
        };

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    atomicInteger.addAndGet(1);
                }
            }
        };

        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        System.out.println(atomicInteger.get());
    }

}

/**
 * Created with IntelliJ IDEA.
 * Description: If you don't work hard, you will a loser.
 * User: Listen-Y.
 * Date: 2020-08-04
 * Time: 20:53
 */
public class Demo3 {
    
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        
        Thread thread = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 5000; i++) {
                    count++;
                }
            }
        };
         Thread thread1 = new Thread() {
             @Override
             public void run() {
                 for (int i = 0; i < 5000; i++) {
                     count++;
                 }
             }
         };
         thread.start();
         thread1.start();
         thread.join();
         thread1.join();
        System.out.println(count);
        
    }
}

CAS的缺陷 ABA问题

  • 这个问题就是加入现在有个num为0 有一个线程把他修改为1, 然后紧接着又有一个线程把他修改为0了 那此时仅仅通过CAS的比较是无法区分的
  • 解决这个问题就需要引入额外的信息 (给变量加一个版本号 每次进行修改 都递增版本号)

synchronize的原理

  • synchronize是java中的关键字,可以用来修饰实例方法、静态方法、还有代码块;主要有三种作用:可以确保原子性、可见性、有序性,原子性就是能够保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等该线程处理完数据后才能进行;可见性就是当一个线程在修改共享数据时,其他线程能够看到,保证可见性,volatile关键字也有这个功能;有序性就是,被synchronize锁住后的线程相当于单线程,在单线程环境jvm的重排序是不会改变程序运行结果的,可以防止重排序对多线程的影响。

以synchronize为例学习锁优化

编辑器和JVM配合进行的

锁消除

  • 锁消除本质是以编辑器和JVM代码运行的情况智能的判断当前的锁有没有必要加 如果没有必要, 就会直接把锁干掉
/**
 * Created with IntelliJ IDEA.
 * Description: If you don't work hard, you will a loser.
 * User: Listen-Y.
 * Date: 2020-08-04
 * Time: 21:21
 */
public class Demo4 {

    public static void main(String[] args) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("listen");
        buffer.append("listen");
        buffer.append("listen");
        buffer.append("listen");
        System.out.println(buffer);
    }

}

在这里插入图片描述

  • 到库中我们可以发现StringBuffer是加锁线程安全的 但是在我们上面写的代码中完全不用考虑线程安全问题 所以在实际运行的时候就把锁消除了

偏向锁

  • 第一个尝试加锁的线程 不会真正的加锁 而是进入偏向锁(一种很轻量的锁) 知道其他线程也来竞争这把锁的时候 才会取消偏向锁的状态 真正的进行加锁
  • 这个很像我去球馆打球的时候借用人家球馆里的球的时候 当人多有人和我竞争的时候我就得去花钱租 人少的时候我就可以登记一下直接玩
  • 总而言之上述这俩个优化机制就是能不加锁就不加锁

自旋锁

  • 当有很多线程竞争锁的时候, 偏向锁状态被消除 此时没有得到锁的线程并不会直接直接挂起放弃 而是使用自旋锁 的方式来尝试去再次获取锁
  • 自旋锁能保证让其他想竞争锁的线程尽快得到锁 但是也相应付出了一定的cpu资源
  • 还是上面我去球馆打球的例子 如果此时就一个人来和我竞争这个篮球 我不会立马放弃 而是会稍微等会 看我是不是快回家了

锁膨胀

  • 当锁竞争更加激烈的时候 此时就会从自旋状态膨胀成重量级锁(挂起等待锁)
  • 还是我去球馆打球 竞争太激烈的时候 等待的人就会回家了 不玩了

锁粗化

  • 如果一段路基中 需要多次加锁 解锁 并且在解锁的时候没有其他线程来竞争 此时就会把多组的锁操作合并在一起 (合并后的锁的粒度很比较粗 所以叫锁粗化)
  • 还是我去打球的例子 比如我正在玩突然想去卫生间 然后此时还没有人和我竞争这个篮球 我就没必要把他放回去 上完卫生间再去拿回来 我直接抱着篮球上卫生间多省事

猜你喜欢

转载自blog.csdn.net/Shangxingya/article/details/107793869