Java中的锁机制



Java中的锁机制、悲观锁和乐观锁、公平锁和非公平锁、可重入锁和不可重入锁、自旋锁

锁的概念

一个进程可以包含多个线程,那么多个线程就会有竞争资源的问题出现,为了互相不打架,就引入了锁的概念。

没有做好并发控制,就可能导致 脏读、幻读和不可重复读 等问题

锁的分类

在这里插入图片描述

锁的介绍

第一种:悲观锁与乐观锁

什么是悲观锁?

悲观锁顾名思义就是悲观的,很怕出错,总是考虑到最坏的情况。

正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。

当我们对数据库的一条数据进行修改的时候,为了避免同时被其他人修改,我们最好的办法就是加锁,防止被同时修改。像这种在修改数据之前先锁定,再修改的方式就是悲观并发控制(Pessimistic Concurrency Control,简写“PCC” ,别名就是悲观锁)

悲观锁的流程

在这里插入图片描述

悲观锁的实现

第一步使用

select xxx for update

加锁 ,等操作完成之后使用commit来释放锁资源,然后INNODB引擎来默认行级锁,最后查不到数据时,则不锁表就行。

  1. 开始事务

    start transaction; (三者选一就可以)
    
  2. 查询出商品的状态信息

    select status from table where id=1 for update;
    
  3. 根据商品信息生成订单

    insert into table1 (id,goods_id) values (null,1);
    
  4. 修改商品状态为2

    update table set status=2 where id=1;
    
  5. 提交事务

    commit;
    
什么是乐观锁?

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。

乐观锁的流程

在这里插入图片描述

乐观锁的实现

乐观锁的实现方式有一种比较典型的就是Compare and Swap(CAS)比较并交换。乐观锁是一种思想。CAS是这种思想的一种实现方式。

CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)

我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。

区别:悲观锁和乐观锁的区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Eg4lYSVn-1610524997523)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210113103348117.png)]

第二种:公平锁和非公平锁

什么是公平锁?

顾名思义公平就是公平的。它是加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得

每一个线程抢占锁的顺序为先后调用lock方法的顺序依次获取锁,类似于排队做某些事。

公平锁的流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8r4DbF6j-1610524997525)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210113150704878.png)]

公平锁的实现

ReentrantLock的实现是基于其内部类**FairSync(公平锁)NonFairSync(非公平锁)**实现的。

其可重入性是基于**Thread.currentThread()**实现的: 如果当前线程已经获得了执行序列中的锁, 那执行序列之后的所有方法都可以获得这个锁。

/**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
什么是非公平锁?

顾名思义非公平锁肯定是不公平的,与公平锁相对,加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待

非公平锁的流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Im6X6Mh1-1610524997526)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210113153025096.png)]

非公平锁的实现

使用Java自带的关键字synchronized对相应的类或者方法以及代码块进行加锁是非公平锁

ReentrantLock的实现是基于其内部类**FairSync(公平锁)NonFairSync(非公平锁)**实现的。

/**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

第三种:可重入锁和不可重入锁

什么是可重入锁?

可重入锁就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁

可重入锁的实现

ReentrantLock和synchronized都是可重入锁但是二者是有区别的!

  1. Synchronized 内置的Java关键字, Lock 是一个Java类
  2. Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
  3. Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
  4. Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下 去;
  5. Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以 自己设置);
  6. Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!
  7. ReentrantLock 可以是公平锁也可以是非公平锁,而synchronized锁只能是非公平锁。
什么是不可重入锁?

与可重入锁相反。不可重入锁就是一个类的A、B两个方法,A、B都有获得统一把锁,当A方法调用时,获得锁,在A方法的锁还没有被释放时,调用B方法时,B方法也获得不了该锁,必须等A方法释放掉这个锁。

不可重入锁的实现

使用自旋锁来实现

public class UnreentrantLock {

private AtomicReference<Thread> owner = new AtomicReference<Thread>();

public void lock() {
    Thread current = Thread.currentThread();
    //这句是很经典的“自旋”语法,AtomicInteger中也有
    for (;;) {
        if (!owner.compareAndSet(null, current)) {
            return;
        }
    }
}

    public void unlock() {
        Thread current = Thread.currentThread();
        owner.compareAndSet(current, null);
    }
}

第四种:自旋锁

什么是自旋锁?

当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用(循环加锁 -> 等待)的机制被称为自旋锁(spinlock)

原理

如果持有锁的线程能在短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗。操作系统的内核经常使用自旋锁:只要是因为自旋锁避免了操作系统进程调度和线程切换,所以自旋锁通常适用在时间比较短的情况下。如果长时间上锁的话,自旋锁会非常耗费性能,它阻止了其他线程的运行和调度。线程持有锁的时间越长,则持有该锁的线程将被 OS 调度程序中断的风险越大。如果发生中断情况,那么其他线程将保持旋转状态(反复尝试获取锁),而持有该锁的线程并不打算释放锁,这样导致的是结果是无限期推迟,直到持有锁的线程可以完成并释放它为止。解决上面这种情况一个很好的方式是给自旋锁设定一个自旋时间,等时间一到立即释放自旋锁。自旋锁的目的是占着CPU资源不进行释放,等到获取锁立即进行处理。如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。因此自旋的周期选择非常重要!然而JDK在1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋时间不是固定的了,而是由前一次在同一个锁上的自旋时间以及锁拥有的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。

优缺点
  1. 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能可以大幅度提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!

  2. 但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成 cpu的浪费。所以这种情况下我们要关闭自旋锁;自旋锁时间阈值(1.6 引入了适应性自旋锁)。

  3. 自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!

  4. JVM 对于自旋周期的选择,Jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化,如果平均负载小于CPUs则一直自旋,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果 CPU 处于节电模式则停止自旋,自旋时间的最坏情况是CPU的存储延迟(CPU A 存储了一个数据,到CPU B得知这个数据直接的时间差),自旋时会适当放弃线程优先级之间的差异。

自旋锁的实现
public class SpinLockTest {

    private AtomicBoolean available = new AtomicBoolean(false);

    public void lock(){

        // 循环检测尝试获取锁
        while (!tryLock()){
            // doSomething...
        }

    }

    public boolean tryLock(){
        // 尝试获取锁,成功返回true,失败返回false
        return available.compareAndSet(false,true);
    }

    public void unLock(){
        if(!available.compareAndSet(true,false)){
            throw new RuntimeException("释放锁失败");
        }
    }

}
 尝试获取锁,成功返回true,失败返回false
        return available.compareAndSet(false,true);
    }

    public void unLock(){
        if(!available.compareAndSet(true,false)){
            throw new RuntimeException("释放锁失败");
        }
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值