并发编程之锁(锁的种类,锁的升级,死锁)

本文详细介绍了Java并发编程中的锁类型,包括偏向锁、轻量级锁、重量级锁、可重入锁、非可重入锁、共享锁、独占锁、公平锁、非公平锁、悲观锁、乐观锁、自旋锁、非自旋锁、可中断锁和不可中断锁。此外,还讲解了死锁的概念、必要条件以及如何模拟死锁。通过对这些概念的理解,有助于提升Java并发编程的能力。

目录

1.锁的分类

偏向锁/轻量锁/重量锁(锁的升级)

可重入锁/非可重入锁

共享锁/独占锁

公平锁/非公平锁

悲观锁/乐观锁

自旋锁/非自旋锁

可中断锁/不可中断锁

2.死锁

什么是死锁?

发生死锁的 4 个必要条件

如何模拟一个死锁?


1.锁的分类

偏向锁/轻量锁/重量锁(锁的升级)

第一种分类是偏向锁/轻量级锁/重量级锁,这三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态。

偏向锁

    如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。

轻量级锁

    JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。

重量级锁

    重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。

通过上述从无锁到重量锁完成了锁的升级:

 

 总结:偏向锁性能最好,可以避免执行CAS操作;轻量锁利用自旋和CAS避免了重量锁带来的线程阻塞和唤醒,性能中等;重量级锁则会把获取不到锁的线程阻塞,性能最差。

可重入锁/非可重入锁

第 2 个分类是可重入锁和非可重入锁。可重入锁指的是线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。同理,不可重入锁指的是虽然线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取。

   对于可重入锁而言,最典型的就是 ReentrantLock 了,正如它的名字一样,reentrant 的意思就是可重入,它也是 Lock 接口最主要的一个实现类。

共享锁/独占锁

第 3 种分类标准是共享锁和独占锁。共享锁指的是我们同一把锁可以被多个线程同时获得,而独占锁指的就是,这把锁只能同时被一个线程获得。我们的读写锁,就最好地诠释了共享锁和独占锁的理念。读写锁中的读锁,是共享锁,而写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。

公平锁/非公平锁

第 4 种分类是公平锁和非公平锁。公平锁的公平的含义在于如果线程现在拿不到这把锁,那么线程就都会进入等待,开始排队,在等待队列里等待时间长的线程会优先拿到这把锁,有先来先得的意思。而非公平锁就不那么“完美”了,它会在一定情况下,忽略掉已经在排队的线程,发生插队现象。

悲观锁/乐观锁

第 5 种分类是悲观锁,以及与它对应的乐观锁。悲观锁的概念是在获取资源之前,必须先拿到锁,以便达到“独占”的状态,当前线程在操作资源的时候,其他线程由于不能拿到锁,所以其他线程不能来影响我。而乐观锁恰恰相反,它并不要求在获取资源前拿到锁,也不会锁住资源;相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改。

自旋锁/非自旋锁

第 6 种分类是自旋锁与非自旋锁。自旋锁的理念是如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”,就像是线程在“自我旋转”。相反,非自旋锁的理念就是没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。

可中断锁/不可中断锁

第 7 种分类是可中断锁和不可中断锁。在 Java 中,synchronized 关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路了,只能等到拿到锁以后才能进行其他的逻辑处理。而我们的 ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直傻等到获取到锁才离开。

2.死锁

什么是死锁?

    首先你要知道,死锁一定发生在并发场景中。我们为了保证线程安全,有时会给程序使用各种能保证并发安全的工具,尤其是锁,但是如果在使用过程中处理不得当,就有可能会导致发生死锁的情况。

    死锁是一种状态,当两个(或多个)线程(或进程)相互持有对方所需要的资源,却又都不主动释放自己手中所持有的资源,导致大家都获取不到自己想要的资源,所有相关的线程(或进程)都无法继续往下执行,在未改变这种状态之前都不能向前推进,我们就把这种状态称为死锁状态,认为它们发生了死锁。通俗的讲,死锁就是两个或多个线程(或进程)被无限期地阻塞,相互等待对方手中资源的一种状态。

发生死锁的 4 个必要条件

要想发生死锁有 4 个缺一不可的必要条件,我们一个个来看:

第 1 个叫互斥条件,它的意思是每个资源每次只能被一个线程(或进程,下同)使用,为什么资源不能同时被多个线程或进程使用呢?这是因为如果每个人都可以拿到想要的资源,那就不需要等待,所以是不可能发生死锁的。

第 2 个是请求与保持条件,它是指当一个线程因请求资源而阻塞时,则需对已获得的资源保持不放。如果在请求资源时阻塞了,并且会自动释放手中资源(例如锁)的话,那别人自然就能拿到我刚才释放的资源,也就不会形成死锁。

第 3 个是不剥夺条件,它是指线程已获得的资源,在未使用完之前,不会被强行剥夺。比如我们在上一课时中介绍的数据库的例子,它就有可能去强行剥夺某一个事务所持有的资源,这样就不会发生死锁了。所以要想发生死锁,必须满足不剥夺条件,也就是说当现在的线程获得了某一个资源后,别人就不能来剥夺这个资源,这才有可能形成死锁。

第 4 个是循环等待条件,只有若干线程之间形成一种头尾相接的循环等待资源关系时,才有可能形成死锁,比如在两个线程之间,这种“循环等待”就意味着它们互相持有对方所需的资源、互相等待;而在三个或更多线程中,则需要形成环路,例如依次请求下一个线程已持有的资源等。

如何模拟一个死锁?


public class DeadLock implements Runnable {

    public int flag;

    static Object o1 = new Object();

    static Object o2 = new Object();

    public void run() {

        System.out.println("线程"+Thread.currentThread().getName() + "的flag为" + flag);

        if (flag == 1) {

            synchronized (o1) {

                try {

                    Thread.sleep(500);

                } catch (Exception e) {

                    e.printStackTrace();

                }

                synchronized (o2) {

                    System.out.println("线程1获得了两把锁");

                }

            }

        }

        if (flag == 2) {

            synchronized (o2) {

                try {

                    Thread.sleep(500);

                } catch (Exception e) {

                    e.printStackTrace();

                }

                synchronized (o1) {

                    System.out.println("线程2获得了两把锁");

                }

            }

        }

    }



    public static void main(String[] argv) {

        DeadLock r1 = new DeadLock();

        DeadLock r2 = new DeadLock();

        r1.flag = 1;

        r2.flag = 2;

        Thread t1 = new Thread(r1, "t1");

        Thread t2 = new Thread(r2, "t2");

        t1.start();

        t2.start();

    }

 }
  • 此文基础概念偏多,学习了《Java 并发编程 78 讲》- 徐隆曦,把这些锁相关基础概念整理下,以便后续回顾,但发表此文碰巧有位老哥也是参考了《Java 并发编程 78 讲》- 徐隆曦, 他也整理这个基础概念,csdn系统就提示我们重复了,因为我是后续方便快速查找回顾,得把文章发出去,那姑且转载地址写他的也为他推广下:https://blog.youkuaiyun.com/xiewenfeng520/article/details/107309698

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值