1.Lock简介与初识AQS

1.1.Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?

Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。 它的优势有:

  • 可以使锁更公平
  • 可以使线程在等待锁的时候响应中断
  • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
  • 可以在不同的范围,以不同的顺序获取和释放锁

整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的 (tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。

1.2.乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一 种实现方式 CAS 实现的。
乐观锁的实现方式:

  1. 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
  2. java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值 (A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。

    1.3.什么是 CAS

    CAS 是 compare and swap 的缩写,即我们所说的比较交换。
    CAS是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通 过给记录加 version 来获取数据,性能较悲观锁有很大的提高。
    CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成B。CAS 是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。 java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的 (AtomicInteger,AtomicBoolean,AtomicLong)。

    1.4.CAS 的会产生什么问题?

    1.4.1.ABA 问题

    比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成A,这时候线程one 进行 CAS 操作发现内存中仍然是A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始JDK 的atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问 题。

    1.4.2.循环时间长开销大

    对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。

    1.4.3.只能保证一个共享变量的原子操作

    当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

    1.5.什么是死锁?

    当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b, 并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

    1.6.产生死锁的条件是什么?怎么防止死锁?

  • 产生死锁的必要条件:
  1. 互斥条件:所谓互斥就是进程在某一时间内独占资源。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。 理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、 预防和解除死锁。

  • 防止死锁可以采用以下的方法:
  1. 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、 ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
  2. 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
  3. 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
  4. 尽量减少同步的代码块。

    1.7.死锁与活锁的区别,死锁与饥饿的区别?

    死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成 的一种互相等待的现象,若无外力作用,它们都将无法推进下去。 活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败。
    活锁和死锁的区别在于:
  • 处于活锁的实体是在不断的改变状态,这就是所谓 的“活”, 而处于死锁的实体表现为等待;
  • 活锁有可能自行解开,死锁则不能。

饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
Java 中导致饥饿的原因:

  1. 高优先级线程吞噬所有的低优先级线程的 CPU 时间。
  2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之 前持续地对该同步块进行访问。
  3. 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

    1.8.多线程锁的升级原理是什么?

    在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级 锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。

    2.AQS(AbstractQueuedSynchronizer)详解与源码分析

    2.1.AQS介绍

    AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
    image.png
    AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

    2.2.AQS 原理概览

    AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。(CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。)
    看个AQS(AbstractQueuedSynchronizer)原理图:
    image.png
    AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
    1. private volatile int state; //共享变量,使用volatile修饰保证线程可见性
    状态信息通过protected类型的getState,setState,compareAndSetState进行操作
    1. // 返回同步状态的当前值
    2. protected final int getState(){
    3. return state;
    4. }
    5. // 设置同步状态的值
    6. protected final void setState(int newState){
    7. state = newState;
    8. }
    9. // 原子的(CAS操作)将同步状态的值设置为except(期望值)
    10. protected final boolean compareAndSetState(int expect,int update){
    11. return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    12. }

    2.3.AQS 对资源的共享方式

    AQS定义两种资源共享方式
  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可 分为公平锁和非公平锁:
    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行。