JAVA并发——公平锁,非公平锁,悲观锁,乐观锁,死锁

个人博客:haichenyi.com。感谢关注

前言

  这几个锁都可以从前面一篇线程同步器AQS里面找到影子,我先把前面一篇的加锁流程图拿过来用一用。

加锁流程图.png

  上面这个流程图是上一篇最开始讲的时候的一张流程图,后面写的时候,后面的流程图都没有画。这一片我们来画一下后面的流程图。

公平锁,非公平锁

  前面一篇讲的时候,我说过了,我们当时做的是一个公平锁。这个公平锁和非公平锁的主要区别就是在这个队列。

  我们前文讲过了,线程1拿到了锁,线程2,3,4就全部放进队列中等待,那么,流程图如下:

等待流程图1.png

  如上图,我们理想状态是:线程1释放锁的时候,队列中的第一个元素,也就是线程2拿到锁,然后,开始执行。

  但是,往往不如意,谁规定的一共就只有4个线程呢?如果,我们正当1释放锁的同时,又有一个线程5进来了,我们要怎么操作呢?流程图如下:

等待流程图2.png

  公平锁和非公平锁的区别就在这里:

  1. 公平锁会把线程5放进队列中,放到线程4的后面,线程2获取到锁,然后执行自己的任务
  2. 非公平锁则是,线程1释放锁之后,状态变成了0,线程5去竞争锁,获取到锁之后,状态state又变成了1,线程2被唤醒之后,正准备去获取锁的时候,一看,状态state是1,又进入等待状态。

  所以,公平锁就是释放锁之后,谁等待得时间长,谁先执行。非公平锁则是,释放锁之后,谁先获取到锁,谁先执行。可能后进的执行,也可能先进的先执行。

  ReentrantLock,初始化的时候传true就是公平锁,传false就是非公平锁,默认是非公平锁。下面就是ReentrantLock的构造方法。

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new 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();
    }

悲观锁锁,乐观锁

  悲观锁:操作之前加锁,操作完成之后解锁。我们前文讲的buy方法就是悲观锁,进入方法就加锁,方法执行完就解锁。还有我们常用的synchronized关键字,就是悲观锁的典型代表。

  乐观锁:乐观锁是一种思想,比方说,我们前文提到的CAS机制,就是乐观锁的一种实现。当我们操作一个变量做加减操作的时候,我们多个线程可以同时做这个操作,但是到具体更新这个值的时候,去判断。典型代表就是Atomic原子类。这个原子类的实现也是CAS机制。

  性能问题:多个线程同时执行,悲观锁,就只有一个线程操作,其他线程挂起等待,释放锁之后,再切换回来。乐观锁,所有的线程都一起执行,最后执行冲突检测和数据更新操作。没有挂起等待,上下文的切换,所以,乐观锁的性能肯定比悲观锁好。但是,实际上真的是这样吗?答案是否定的。乐观锁的性能不一定比悲观锁好。

  前面,我们说到乐观锁是在最后更新得时候,去判断。那么怎么判断呢?早期1.5版本之前的CAS操作是有3个参数内存位置(V)、原值(A)、新值(B)。我们在更新的时候,先判断A是否满足,满足就更新成B上一篇文章已经说过了。不满足,那就再循环一边重复判断。极端情况下,要是线程足够的多,并且一直不满足,那是不是一直循环判断(CAS自旋)?那就一直占用CPU。这样性能肯定不好。

  synchronized在JDK1.5之前的确性能很差,但是在1.6的时候就已经做了优化了,从无锁状态,到偏向锁状态,再到轻量级锁状态,最后到重量级锁状态。这几个状态会随着竞争情况逐渐升级(锁不但可以升级还可以降级)。现在synchronized的性能跟ReentrantLock差不多。

  所以,悲观锁的性能不一定比乐观锁差,乐观锁的性能不一定比悲观锁好。根据实际情况去选择悲观锁和乐观锁。那到底怎么选择呢?

  之前在网上看到过这么一组数据,启用多个线程进行计数相加到一亿,首先是synchronized方式

synchronized时间图.png

  当线程数为8时,性能明显提升,但是8到32个线程来说,每个线程的平均时间基本差不多,基本没有提升,到了64个线程的时候,性能又有一点提升。

如果换成CAS实现多线程累加数为一亿,时间又会怎么样呢?

CAS时间图.png

  在线程数相对较少的时候,CAS实现比较快,性能优于synchronized,当线程数多于8后,CAS实现明显开始下降,反而时间消耗高于synchronized;

  总结:synchronized是java提供的又简单方便,性能优化又非常好的功能,建议大家常用;CAS的话,线程数大于一定数量的话,多个线程在循环调用CAS接口,虽然不会让其他线程阻塞,但是这个时候竞争激烈,会导致CPU到达100%,同时比较耗时间,所以性能就不如synchronized了。

死锁

概念

  死锁是指:多个进程在运行过程中因争夺某一种资源,而造成的僵持状态,若无外力作用,他们都将无法向前推进。

举个栗子

小明在看电视,小红在玩手机,小明对小红说:你把手机给我玩,我把点视给你看;小红却说:你把点视给我看,我再把手机给你玩。

分析

  1. 电视,手机都可以看作一种资源。
  2. 小明在看电视,小红在玩手机:表示电视分配给小明了,小明对电视持有锁;手机分配给小红了,小红对手机持有锁
  3. 小明对小红说,你把手机给我玩,我把点视给你看:小明想获取到手机的锁之后,再释放自己电视的锁。
  4. 小红却说,你把点视给我看,我再把手机给你玩:小红想获取电视的锁之后,再释放自己手机的锁

  所以,小明和小红都在等待对方释放锁,自己拿到想要的资源之后,释放自己资源的锁。这里谁都拿不到锁,就无线的等待下去。这就是死锁。

产生条件

  1. 互斥条件:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。(就是这里的电视只能给小明看,手机只能给小红玩)
  2. 占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。(小明等着小红释放手机资源,小红等着小明释放电视资源)
  3. 不剥夺条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来(小明在看电视的时候,小红不能说,我要看电视,你给我看。小红在玩手机的时候,小明不能说,我要玩手机,你把手机给我玩。我们要讲文明,不能耍流氓)
  4. 环路等待条件:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源(小明等着小红释放手机资源,小红等着小明释放电视资源)

解决办法

  如果产生死锁只能重启。所以,我们在开发过程中要尽量避免死锁,比方说:著名的银行家算法。只要上面四种中的任意一种不满足,就不可能造成死锁:比方说占有等待,我们可以用共享锁的方式AQS里面每个加锁的方法都有一个try开头的方法。可以看一下acquire和tryAcquire的区别。这就破坏了第二个条件,等待。

你可能感兴趣的:(JAVA并发——公平锁,非公平锁,悲观锁,乐观锁,死锁)