JUC-3.1-锁-锁的分类

Java 锁有很多种,从不同的角度来看,锁大概有以下几种分类
在这里插入图片描述

下面分别看一下这几种锁

悲观锁和乐观锁

简单来说,悲观锁就是面对同步资源的时候,首先认为会有别的线程会来修改数据,所以先上锁,锁住之后再去修改资源,上面提到的 synchronizedLock 就都是悲观锁

乐观锁就是先认为没有线程去修改这个资源,所以不上锁先去修改,等修改完成提交的时候再做检查,如果这个时间段有别的线程修改了,那就做其他的处理,没有的话就提交,比如我们的git仓库,也是提交的时候才去判断有没有别人修改,有的话解决冲突或者其他操作,没有的话直接提交成功

开销对比

悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁的时间就算越来越差,也不会对互斥锁的开销造成影响

乐观锁最开始的消耗是要比悲观锁小的,因为不用先去上锁,但是如果自旋时间很长,或者不停的重试,那么消耗的资源也会越来越多

使用场景

两种锁各有个的使用场景

  • 悲观锁: 适用于并发写入多,临界区持锁时间较长的情况,悲观锁可以避免大量的无用自旋操作,比如临界区有IO操作,临界区代码复杂或循环量大,或者线程竞争激烈的情况
  • 乐观锁: 适用于并发写入少,大部分是读取的场景,不加锁能让读取的性能大大提高

可重入锁与非可重入锁

可重入锁,也叫做递归锁,指的是同一个线程T在进入外层函数A获得锁L之后,T继续进入内层函数B时,如果仍然有获取该锁L的代码,在不释放锁L的情况下,依然可以重复获取该锁L。好处:1.避免死锁 2.提示封装性能
非可重入锁,对比上面,指的是同一个线程T在进入外层函数A获得锁L之后,T继续进入内层递归函数B时,仍然有获取该锁L的代码,必须要先释放进入函数A的锁L,才可以获取进入函数B的锁L。

简单来说就是同一个线程能不能重复获取自己已经获取到的锁.
在这里插入图片描述
可重入锁,上锁时先判断获取锁的次数是否为0,如果是0那么就给当前线程加锁;如果不为0那么判断是否为当前线程,如果是当前线程,那么获取锁次数+1然后返回true。
不可重入锁,上锁是直接加锁。

ReentrantLocksynchronized 就是可重入锁,可重入锁的作用其实就是为了防止死锁

常用方法
  • isHeldByCurrentThread(): 查看锁是否被当前线程持有
  • getQueueLength(): 返回当前正在等待这把锁的队列有多长

这两个方法一般在开发调试的时候用

公平锁和非公平锁

从名字上看,公平锁就是保障了各个线程获取锁都是按照顺序来的,先到的线程先获取锁,而非公平锁则不一定按照顺序,在一定情况下是可以插队的

在公平锁中,比如线程 1234 依次去获取锁, 线程1首先获取到了锁,然后它处理完成之后会释放,释放之后会唤醒下一个线程,依次获取锁

而非公平锁中,比如此时持有锁的是线程1,然后线程234尝试获取锁,就会进入一个等待队列。当线程1释放掉锁之后,唤醒线程2的这个过程中,如果有别的线程比如线程5尝试去请求锁,那么线程5是可以先获取到的,就是插队,因为线程2的唤醒需要CPU的上下文切换,这个需要一定的时间,线程1释放锁和线程2被唤醒的这段时间,锁是空闲的,所以在非公平锁中,就可以先让别的线程获取到,这样做的目的主要是利用锁的空档期,提高效率 。

ReentrantLock默认就是非公平锁,构造函数传入true就是公平锁。但是 tryLock()是特例,一但有线程释放了锁,那正在tryLock的线程就能取到锁,即使在它之前已经有其他线程在等待队列。

优缺点
类型优势劣势
公平锁各个线程公平平等,在等待一段时间之后总有执行的机会更慢,吞吐量小
非公平锁更快,吞吐量大可能会产生线程饥饿的问题(某些线程长时间等待但是得不到执行)
ReentrantLock 默认就是一个非公平锁,如果要设置为公平锁的话可以在构造种传入true new ReentrantLock(true)
在这里插入图片描述

共享锁和排他锁

典型的就是读写锁(ReentrantReadWriteLock),比如读操作可以有很多线程一起读,但是写操作只能有一个线程去写,而且在写入的时候,别的线程也不能进行读的操作

如果没有读写锁,只用 ReentrantLock 那么虽然可以保证线程安全,但是也会浪费一部分资源,因为多个读操作并没有线程安全问题,所以在读的地方使用读锁,在写的地方用写锁,可以提高程序执行效率

读写锁的规则
  1. 多个线程申请读锁,都可以申请到
  2. 如果有一个线程已经占用了读锁,则此时其他线程如果申请写锁,则申请写锁的线程会一直等读锁被释放
  3. 如果有一个线程获取到了写锁,则其他线程不管申请读锁还是写锁,都得等当前的写锁被释放

总结一下,要么一个或多个线程同时有读锁,要么一个线程有写锁,但是两者不会同时出现。

使用方式
private static ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock();
// 读锁
private static ReentrantReadWriteLock.ReadLock readLock = reentrantLock.readLock();
// 写锁
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantLock.writeLock();
读锁插队策略

在非公平锁的情况下,假设有这样一个场景,线程2和线程4正在读,线程3想要写入,但是拿不到锁,于是在等待队列里面等待,线程5不在等待队列里,但是它想要读,那么线程5能插队直接获取到读锁吗

这里无非有两种情况,第一种可以插队,这样效率高,但是读请求多的情况下写线程就会一直获取不到锁, 第二种情况就是不能插队,等写线程写完再获取

ReentrantReadWriteLock 实现的就是第二种情况

ReentrantReadWriteLock 的插队策略就是

  • 公平锁: 不允许插队
  • 非公平锁: 写锁可以插队,但是根据读写锁的策略,如果已经有锁,写锁插队的时候是获取不到锁的最后还是会进入队列。读锁仅在等待队列头节点不是写锁的时候可以插队
升降级策略

锁降级是指把持住当前拥有的写锁的同时,再获取到读锁,随后释放写锁的过程。

  • 写锁可以降级为读锁可以提高效率;读锁不能升级为写锁,主要是为了防止死锁,除非能各种代码,业务之类的保证只有一个线程升级,同时升级的时候其他线程都释放锁。
    引用
适用场景

读写锁适用于读多写少的情况,合理使用可以提高并发效率

自旋锁和阻塞锁

阻塞或唤醒一个Java线程是需要CPU切换状态来完成的也就是上下文切换,这种状态转换需要耗费处理器时间。如果同步代码中的内容很简单,转换的时间可能比用户代码执行的时间还要长。
自旋锁是采用让当前线程不停的在循环体内执行实现,当循环的条件被其它线程改变时才能进入临界区,用代码举个例子

public class SpinLock {

    private AtomicReference<Thread> sign = new AtomicReference<>();
    
    private void lock(){
        Thread current = Thread.currentThread();
        //循环获取锁 获取成功跳出循环
        while (!sign.compareAndSet(null, current)) {
        	System.out.println("自旋锁获取失败");
        }
    }
    
    private void unLock(){
        Thread current = Thread.currentThread();
        sign.compareAndSet(current, null);
    }
}

这里就是一个自旋锁,上锁的时候,一直循环,直到上锁成功

如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁

而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁,

而阻塞锁,就是改变线程的状态,让线程进入等待的状态,等待唤醒,之后再去竞争锁
juc的atomic包下的类基本都是自旋锁的实现。ReentrantLock.lock() 内部同样使用CAS实现自旋锁。

优缺点分析
  • 自旋锁优点: 由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。
  • 自旋锁缺点: 在自旋的过程中是会消耗CPU的,虽然自旋锁的起始开销低于悲观锁,但是随着自旋的时间增长,比如锁被占用很长时间或者线程增多,开销也是线性增长。
使用场景

自旋锁一般用于多核服务器,在并发度不是特别高或占用CPU时间不是很长的情况下,比阻塞锁的效率高。

可中断锁和不可中断锁

Java中 synchronized 就是一个不可中断锁,而 ReentrantLock 就是一个可中断锁,之前介绍的 tryLock(long time, TimeUnit unit)lockInterruptibly() 方法都可以中断锁的获取

简单来说在获取锁的过程中能放弃获取锁,中断获取锁的过程,那就是可中断锁,否则就是不可中断锁

Java虚拟机对锁的优化

  • 自旋锁和自适应:自旋锁也不是完全的埋头自旋,比如自旋个二十次三十次还获取不到锁,那么自旋锁会转为阻塞锁;比如这次自旋成功了,那么下次会继续自旋,如果没自旋成功那么下次可能就直接阻塞了。这个就是虚拟机对与自旋锁的自适应,自旋次数是可以在jvm配置中修改的。
  • 锁消除:比如说所有同步的东西都是在方法内部的,根本没有外人可以访问到里面的东西,虚拟机就会分析出来都是私有的根本没必要加锁,那就把锁消除。
  • 锁粗化:比如一系列操作都是对一个对象加锁解锁,那么jvm会动态检测如果相邻的代码块使用的是同一个锁对象,那么就会合为一个

总结

关于锁就先介绍到这里,掌握这几种锁的特点,在开发过程中选择最合适的使用

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值