大白话解释一波多线程里面的各种“锁”

锁:解决资源占用的问题;保证同一时间一个对象只有一个线程在访问;

锁机制的作用:有些业务逻辑在执行过程中要求对数据进行排他性的访问,于是需要通过一些机制保证在此过程中数据被锁住不会被外界修改,这就是所谓的锁机制。

饥饿:是指如果线程T1占用了资源R,线程T2又请求封锁R,于是T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求资源R,当T3释放了R上的封锁之后,系统又批准了T4的请求......,T2可能永远等待。(就好比食堂打饭,刷卡的优先打饭,付现金的要等刷卡的打完了才能打,可是拿着现金的很早就在那儿准备好了,可以刷卡的那条队伍却一直来了一个又一个,来个没完,拿现金的只好饿死。这也就是ReentrantLock显示锁里提供的不公平锁机制(当然了,ReentrantLock也提供了公平锁的机制,由用户根据具体的使用场景而决定到底使用哪种锁策略),不公平锁能够提高吞吐量但不可避免的会造成某些线程的饥饿。)

死锁:在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。尽管死锁很少发生,但一旦发生就会造成应用的停止响应(就像夫妻吵架,都等着对方先道歉,就会造成死锁)

活锁:是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。

互斥锁:对共享资源的访问必须是顺序的,也就是说当多个线程对共享资源访问的时候,只能有一个线程可以获得该共享资源的锁,当线程A尝试获取线程B的锁时,线程A必须等待或者阻塞,直到线程B释放该锁为止,否则线程A将一直等待下去,因此java内置锁也称作互斥锁,也即是说锁实际上是一种互斥机制。

死锁和饥饿的区别:

·死锁进程等待永远不会被释放的资源,饿死进程等待会被释放但却不会分配给自己的资源,表现为等待时限没有上界(排队等待或忙式等待);

·死锁一定发生了循环等待,而饿死则不然。这也表明通过资源分配图可以检测死锁存在与否,但却不能检测是否有进程饿死;

·死锁一定涉及多个进程,而饥饿或被饿死的进程可能只有一个。

·在饥饿的情形下,系统中有至少一个进程能正常运行,只是饥饿进程得不到执行机会。而死锁则可能会最终使整个系统陷入死锁并崩溃

可重入锁:

如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。什么是可重入性?举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。如下面的代码:

class MyClass {

    public synchronized void method1() {

        method2();

    }

    public synchronized void method2() {

    }

}

method1和method2都是synchronized修饰的方法,在method1里面调用method2的时候,不需要重新申请锁,可以直接调用就行了(其实可以反过来想一想,如果synchronized不具有重入性当我调用了method1的时候,得申请锁,申请好了之后那么method1就拥有了这个锁,那么调用method2的时候,又要重新申请锁,而锁在method1的手上,这时候又要重新申请锁,显然是不可能得到的,这不科学。所以,synchronize和lock都是具有可重入性的)

可中断锁:如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

非公平锁:刚刚讲到的食堂打饭的例子,就是一个不公平锁的例子;synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。这样就可能导致某个或者一些线程永远获取不到锁。

公平锁:公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。

读写锁:就是将一个资源的访问分成两个锁,一个读锁,一个写锁;正因为有了读写锁,才使得多个线程之间的读写操作不会发生冲突。ReadWriteLock就是读写锁,可以通过readLock()获取读锁,通过writeLock()获取写锁。

自旋锁:举个例子:获取到资源的线程A对这个资源加锁,其他线程比如B要访问这个资源首先要获得锁,而此时A持有这个资源的锁,只有等待线程A逻辑执行完,释放锁,这个时候B才能获取到资源的锁进而获取到该资源。这个过程中,A一直持有着资源的锁,那么没有获取到锁的其他线程比如B怎么办?通常就会有两种方式:

1. 一种是没有获得锁的进程就直接进入阻塞(BLOCKING),这种就是互斥锁

2. 另外一种就是没有获得锁的进程,不进入阻塞,而是一直循环着,看是否能够等到A释放了资源的锁,这种就是自旋锁

什么时候用自旋锁比较好?如果A线程占用锁的时间比较短,这个时候用自旋锁比较好,可以节省CPU在不同线程间切换花费的时间开销;如果A线程占用锁的时间比较长,那么使用自旋锁的话,B线程就会长时间浪费CPU的时间而得不到执行(要执行一个线程需要CPU,并且需要获得锁),这个时候不建议使用自旋锁;还有递归的时候尽量不要使用自旋锁,可能会造成死锁。

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。这样可以保证每次都只有一个线程在访问这个数据;传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

乐观锁:很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,那么就会有很多对象可以同时访问这个锁里面的数据,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。

适用场景:

悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值