关于Java中的锁

关于Java中的锁,画图简单了解一下

1、乐观锁和悲观锁

1.1 释义

  • 悲观锁
    • 见名知意,悲观锁就像悲观的人,总是想事情会往坏的方向发展,获得的东西或机会常常会牢牢抓在手中。
    • 悲观锁就是这样,给某个资源加悲观锁,当线程每次去获取资源时,会加设其他线程参与竞争,所以每次操作前都会上锁,其他线程只能阻塞
  • 乐观锁
    • 乐观锁在操作数据时不会上锁,在更新的时候会判断一下在此期间是否有其他线程去更新这个数据。

1.2 看图

在这里插入图片描述

在这里插入图片描述

1.3 关于CAS算法的说明

  • CAS算法:Compare And Swap,比较与交换

  • 说明:

    1. 如果线程的期望值跟物理内存的真实值一样,就更新值到物理内存当中,并返回true
    2. 如果线程的期望值跟物理内存的真实值不一样,返回false,那么本次修改失败,那么此时需要重新获得主物理内存的新值
  • 看图:

    • 主物理内存有一个共享变量值为5,有两个线程A和B,都有自己的工作内存,并且有变量的拷贝(快照5)
    • A线程现在把值改为10,然后写回主物理内存并通知其它线程可见(加volatile)
    • 这个过程中,A的期望值为5,跟主物理内存的值5进行对比,如果相同,说明没有其它线程改变,则将主物理内存的值改为10,并返回true
    • 这时,B线程也将自己工作内存的值改为了20,当写回主物理内存时,发现自己的期望值(5),与现在的主物理内存(10)不一样了,就会写入失败,返回false,此时需要重新获得主物理内存的新值

1.4 使用场景

  • 乐观锁适用于写操作比较少的场景。省去获得释放锁的开销
  • 悲观锁适用于写多读少的场景。写操作频繁,加锁免去频繁重试

2、独占锁和共享锁(也叫写锁和读锁)

2.1 独占锁(排它锁)

  • 锁每次次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。

  • 图示

2.2 共享锁(共享的是读操作、施加共享锁的操作)

  • 锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。

  • 图示

3、互斥锁和读写锁

3.1、互斥锁

  • 独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性

3.2、读写锁

  • 是共享锁的一种具体实现。读写锁管理一组锁,一个是只读的锁,一个是写锁。

    1. 读锁可以在没有写锁的时候被多个线程同时持有
    2. 写锁是独占的。写锁的优先级要高于读锁,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
  • 读写锁比互斥锁并发程度更高,每次只有一个写线程,但是同时可以有多个线程并发读。

  • 图示

在这里插入图片描述

4、公平锁和非公平锁

4.1、公平锁

  • 多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来后到

  • 图示

在这里插入图片描述

  • /**
    * 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
    */
    Lock lock = new ReentrantLock(true);
    

4.2、非公平锁

  • 多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁

  • 在高并发环境下,有可能造成优先级翻转,或者饥饿的状态(某个线程一直得不到锁)。

  • 图示
    在这里插入图片描述

  • /**
    * 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
    */
    Lock lock = new ReentrantLock(false);
    

5、可重入锁(一定程度避免死锁)

  • 又称之为递归锁,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。

  • 图示
    在这里插入图片描述

  • ReentrantLock和Synchronized都是是一个可重入锁。

  • public synchronized void A() throws Exception{
     B();
    }
     
    public synchronized void B() throws Exception{
    }
    //A调用B,如果一个线程调用A已经获取了锁再去调用B,就不需要再次获取锁了,这就是可重入锁的特性。
    //如果不是可重入锁的话,B可能不会被当前线程执行,可能造成死锁。
    

6、自旋锁

  • 线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是自旋

  • 自旋锁是为了减少线程被挂起的几率,因为挂起和唤醒也都耗资源

  • 如果锁被另一个线程长时间占用,即使自旋了之后当前线程还是会被挂起,就会变成浪费系统资源的操作。因此自旋锁是不适应锁占用时间长的并发情况的。

  • 图示
    在这里插入图片描述

  • CAS操作就是在做自旋操作

  • DK1.6又引入了自适应自旋锁

    • 自旋时间不再固定
    • 如果虚拟机认为自旋有可能成功那就会持续,如果自旋很少成功,可能就直接省略掉自旋过程

7、分段锁

  • 一种设计,不是具体的锁

  • 将锁的粒度进一步细化,当前操作不需要更新整个数组的时候,只针对数组中的一项进行加锁操作

  • 图示
    在这里插入图片描述

8、锁升级(随多线程竞争而升级,不能降级)

8.1 无锁

  • 其实就是上面讲的乐观锁

8.2 偏向锁

  • 偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。

  • 偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。

8.3 轻量级锁

  • 当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁
  • 轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋的方式等待上一个线程释放锁。

8.4 重量级锁

  • 线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时,升级为重量级锁
  • 其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态
  • 其实synchronized 关键字内部实现原理就是锁升级的过程

9、锁优化

9.1 锁粗化

  • 将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求

  • 举例

    private static final Object LOCK = new Object();
    for(int i = 0;i < 10; i++) {
        synchronized(LOCK){
            ...
        }
    }
    //锁粗化之后
     synchronized(LOCK){
         for(int i = 0;i < 100; i++) {
             ...
        }
    }
    

9.2 锁消除

  • 虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除

可见JVM基础回顾

10、通过问题简单总结

  1. 线程是否要锁住同步资源?
    • 悲观锁——锁住
    • 乐观锁——不锁
  2. 锁住同步资源失败,线程阻塞吗?
    • 阻塞
    • 不阻塞——自旋锁、适应性自旋锁
  3. 同一个线程竞争同步资源的过程有什么区别?
    • 无锁——不锁住资源,多个线程只能有一个操作资源成功,其他线程会重试
    • 偏向锁——同一个线程操作同步资源时将自动获取
    • 轻量级锁——多个线程竞争,没有获得资源的线程自旋等待
    • 重量级锁——自旋过长,阻塞等待唤醒
  4. 多个线程竞争资源是否排队?
    • 公平锁——排队
    • 非公平锁——不好说
  5. 一个线程中的多个流程能够获取同一把锁?
    • 可重入锁——能
    • 非可重入锁——不能
  6. 多个线程能否共享一把锁?
    • 共享锁——能
    • 排它锁——不能
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值