欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java面试之JUC

程序员文章站 2024-03-21 21:43:34
...

引言:juc,java并发编程工具包,高频面试考点。

参考:https://www.bilibili.com/video/av48961087

代码:https://gitee.com/machine4869/note-code/tree/master/note_juc
该文章转自我的博客:https://machine4869.gitee.io/20200221192831555/

文章目录

Juc框架概览

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A9tlY24W-1584804080925)(20200221192831555/JUC框架.png)]

Volatile

volatile是什么?为什么引入了volatile

volatile是java虚拟机提供的一种轻量级同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排(保证了有序性)

高并发下不用synchronized,因为并发性不好;用volatile和juc下面的类;

JMM抽象结构图描述

关于JMM: blog/2018/07/27/15326929363424/#2-3-JAVA内存模型(JMM)

结构图主要描述:

  • 主内存/工作内存/共享变量/拷贝副本
  • JMM是一种抽象概念,不真实存在,它描述的是一组规则/规范

JMM如何保证同步的?

  1. 解锁前,把工作内存的值刷到主内存
  2. 加锁时,从主内存读取最新值到工作内存
  3. 加锁解锁是同一把锁

JMM三大特性是什么?

  1. 可见性
  2. 原子性
  3. 有序性

三大特性使线程安全性获得保证

可见性是什么意思?

某线程如果修改了主内存的共享变量,对其他线程是可见的。

volatile保证可见性代码演示

demo

package com.mxx.juc;
 
import java.util.concurrent.TimeUnit;
 
/**
* 可见性验证:
* - a线程修改变量值
* - 对b线程(main)是否可见
* 结论:
* - 不加volatile,a线程的修改对b线程不可见
* - 加volatile,a线程的修改对b线程可见
*/
public class VolatileDemo {
 
// int num = 1;
volatile int num = 1;
public void changeNum(){
this.num = 30;
}
 
public static void main(String[] args) {
// 线程 操作 资源类
VolatileDemo vd = new VolatileDemo();
new Thread(()->{
System.out.println("进入线程"+Thread.currentThread().getName()+",num="+vd.num);
// 休息,等待num被其他线程读取到
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
 
vd.changeNum();
System.out.println("线程"+Thread.currentThread().getName()+"修改num,num="+vd.num);
},"a").start();
 
while (vd.num==1){
// 一直等待循环
}
 
System.out.println(Thread.currentThread().getName()+"线程结束,num="+vd.num);
 
// 加volatile:
//进入线程a,num=1
//线程a修改num,num=30
//main线程结束,num=30
}
}

原子性是什么意思?

某个线程在做具体业务时,要么整体成功,要么整体失败,保证数据一致性

volatile不保证原子性代码演示

demo

package com.mxx.juc;
import java.util.concurrent.TimeUnit;
public class VolatileDemo {

  // int num = 1;
  volatile int num = 1;
  public void addNum(){
    num++;
  }
  /**
   * 不保证原子性验证:
   * - 20个线程,每个线程1000次,做+1操作
   * - 20个线程全部跑完后,看main函数打印结果
   * 结论:
   * - 每次结果都不一样,小于20001
   *
   * @param args
   */
  public static void main(String[] args) {
    VolatileDemo vd = new VolatileDemo();
    for (int i = 0; i < 20; i++) {
      new Thread(()->{
        for (int j = 0; j < 1000; j++) {
          vd.addNum();
        }
      },"线程"+i).start();
    }
    // 主线程+GC线程
    while (Thread.activeCount()>2){
      // 线程礼让
      Thread.yield();
    }
    System.out.println(Thread.currentThread().getName()+"线程: num="+ vd.num);
  }
}

volatile不保证原子性理论解释(num++为什么不安全)

num++出现问题底层逻辑(javap -c):

  1. 获得:各线程从主内存读num到工作内存
  2. 修改:各线程在各自工作内存做+1操作,工作内存中的操作互相不可见。
  3. 写回:线程在写回前被挂起了,写回的时候相互覆盖,造成数值丢失。

volatile不保证原子性问题怎么解决?

  1. 加synchronized,并发性能不好
  2. juc的atomic,比如AtomicInteger的getAndIncrement()

AtomicInteger保证原子性代码演示

demo

AtomicInteger num2 = new AtomicInteger(1);
public void addNumByAtomic(){
    num2.getAndIncrement();
     // num2.incrementAndGet();
}
/**
 * volatile不保证原子性解决:
 * - AtomicInteger
 */
public static void main(String[] args) {
  // seeOkByVolatile();
  VolatileDemo vd = new VolatileDemo();
  for (int i = 0; i < 20; i++) {
    new Thread(()->{
      for (int j = 0; j < 1000; j++) {
        // 不安全
        vd.addNum();
        // 安全
        vd.addNumByAtomic();
      }
    },"线程"+i).start();
  }

  // 主线程+GC线程
  while (Thread.activeCount()>2){
    // 线程礼让
    Thread.yield();
  }

  System.out.println(Thread.currentThread().getName()+"线程: num="+ vd.num);
  System.out.println(Thread.currentThread().getName()+"线程: num2="+ vd.num2);
  // main线程: num=13808
  // main线程: num2=20001
}

* 有序性之Happens-before原则

java内存模型一个列出了八种Happens-before规则,如果两个操作的次序不能从这八种规则中推倒出来,则不能保证有序性

更详细的:blog/2018/07/28/15327607157902/#4-4-有序性

Volatile通过什么实现可见性?

通过加入 内存屏障禁止重排序优化来实现

什么是指令重排?造成什么问题?

  1. Java内存模型中,为提高性能,允许编译器和处理器对指令进行重排序。
  2. 重排时会考虑到指令间的数据依赖性
  3. 不会影响单线程环境下程序执行
  4. 多线程下,线程交替执行,由于优化重排的存在,两线程使用的变量能否保证一致性是无法确定的。结果无法预测。

* 指令重排造成的不安全举例

int a=0;
boolean flag = false;

public void m1(){
  a=1; // 1
  flag=true; // 2
}

public void m2() {
  if(flag){
    a = a+5; // 3
    System.out.println("revalue="+a);
  }
}

解释:

  1. 线程1执行m1,线程2执行m2
  2. 在不重排时,一定是按照123步骤执行,结果为6
  3. 如果发生重排,比如1和2交换了顺序,当m1执行完2时,线程切换,执行m1,这时可以进入if函数,a结果为5

* 什么是内存屏障?

详细:blog/2018/07/28/15327607157902/#4-3-可见性

关键字:

  • load屏障/store屏障
  • volatile读/volatile写
  • 内存屏障还能强制刷出cpu缓存,保证数据最新

如何保证有序性?

保证有序性:volatile、synchronized、Lock

你在哪些地方用到了volatile?

  1. 单例模式在多线程下不安全
  2. 读写锁/手写缓存
  3. cas底层源码分析

单例模式在多线程下不安全代码演示

demo

public class SingletonDemo {

  private static SingletonDemo instance = null;

  private SingletonDemo() {
    System.out.println(Thread.currentThread().getName()+"\t调用构造方法");
  }

  public static SingletonDemo getInstance() {
    if(instance == null){
      instance = new SingletonDemo();
    }

    return instance;
  }
  public static void main(String[] args) {
    // 单线程下,单例模式正常。
//    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
    //main 调用构造方法
    //true
    //true
    //true


    // 多线程下,单例模式不行
    for (int i = 0; i < 10; i++) {
      new Thread(()->{
        SingletonDemo.getInstance();
      },"线程"+i).start();
    }
    //线程0 调用构造方法
    //线程4 调用构造方法
    //线程3 调用构造方法
    //线程2 调用构造方法
    //线程1 调用构造方法
  }
}

! 单例模式在多线程下不安全解决方案?

DCL双端检索机制+volatile

为什么只用DCL不能保证线程安全?

简答:在创建对象过程中发生指令重排。检测到intance不为null但对象却没有完全创建成功。

双端检索机制:在加锁前和加锁后都进行一次判断

demo:

public static SingletonDemo getInstance() {
  if(instance == null){
    synchronized (SingletonDemo.class){
      if(instance == null){
        instance = new SingletonDemo();
      }
    }
  }
  return instance;
}

为什么以上代码不一定安全?

  1. 因为有指令重排序的存在
  2. 原因在于:某线程读到instance不为nul时,instance的引用对象可能还没有完成初始化(new SingletonDemo()做到一半)
    instance = new SingletonDemo()分为以下步骤:
    1)分配对象内存空间
    2)初始化对象
    3)设置instance指向内存地址,此时instance != nul
    其中步骤2 3可能发生重排,
  3. 因此多线程下,当线程a访问instance!=null时,instance实例却未必初始化完成(还没做2);此时切到线程b,线程b直接取intance实例,这个实例是未完成初始化的实例。因此线程不安全。

如何解决?

private static volatile SingletonDemo instance = null;
// 告诉编译器禁止指令重排

CAS

提问线路:CAS—> Unsafe—> CAS底层原理 —> 原子引用更新 —> 如何规避ABA问题

compareAndSet怎么用?

比较并交换(compareAndSet)

/**
 * boolean compareAndSet(int expect, int update)
 * - 如果主内存的值=期待值expect,就将主内存值改为update
 * - 该方法可以检测线程a的操作变量X没有被其他线程修改过
 * - 保证了线程安全
 */
public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(5);
    System.out.println(atomicInteger.compareAndSet(5, 10)+ "\t" + atomicInteger);
    System.out.println(atomicInteger.compareAndSet(5, 20)+ "\t" + atomicInteger);
    //true	10
    //false	10
}

CAS底层原理简述?

  1. Compare-And-Swap。是一条CPU并发原语。(原语:操作系统范畴,依赖硬件,不被中断。)
  2. 功能是判断内存某个位置的值是否为预期值(Compare),是就更新(Swap),这个过程是原子的。
  3. 功能描述:
    1. 判断内存某个位置的值是否为预期值(Compare),是就更新(Swap),这个过程是原子的。
    2. cas有三个操作数,内存值V,旧预期值A,要更新的值B。仅当预期值A=内存值V时,才将内存值V修改为B,否则什么都不做。
  4. 自旋:比较并交换,直到比较成功
  5. 底层靠Unsafe类保证原子性。

getAndIncrement() 源码解析(用了cas保证线程安全)

/**
 * 参数说明:
 * this: atomicInteger对象
 * valueOffset:对象的内存地址
 * unsafe:sun.misc.Unsafe类
 * AtomicInteger中变量value使用volatile修饰,保证内存可见。
 * 结论:底层依赖CAS操作/Unsafe类
 */
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
 * compareAndSwapInt:即CAS
 * while: 如果修改失败,会一直尝试修改,直到成功。
 */
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

简述:

  1. 调用了Unsafe类的getAndAddInt
  2. getAndAddInt使用cas一直循环尝试修改主内存

对Unsafe的理解?

Unsave类

  1. 该类所有方法都是native修饰,直接调用底层资源。sun.misc包中。
  2. 可以像C的指针一样直接操作内存。java的CAS操作依赖Unsafe类的方法。

! CAS有哪些缺点?

  1. 循环时间长,开销大
    1. 如果cas失败,就一直do while尝试。如果长时间不成功,可能给CPU带来很大开销。
  2. 只能保证一个共享变量的原子操作
    1. 如果时多个共享变量,cas无法保证原子性,只能加锁,锁住代码段。
  3. 引出来ABA问题。

ABA问题

简述ABA问题和解决方案?

简述版:

ABA问题描述: 线程1做CAS操作将A改为B再改为A,而线程2再做CAS时修改成功了,这不符合设计思想。

怎么解决:AtomicStampReference时间戳原子引用

ABA问题描述?问题出在哪?

ABA问题描述:

  • 比如线程1从内存位置V中取出A,此时线程2也取出A。且线程2做了一次cas将值改为了B,然后又做了一次cas将值改回了A。此时线程1做cas发现内存中还是A,则线程1操作成功。这个时候实际上A值已经被其他线程改变过,这与设计思想是不符合的。

这个过程问题出在哪?

  • 如果只在乎结果,ABA不介意B的存在,没什么问题
  • 如果B的存在会造成影响,需要通过AtomicStampReference,加时间戳解决。

原子更新引用是啥?

AtomicStampReference,使用时间戳,解决cas中出现的ABA问题。

AtomicReference使用代码演示

demo

/**
 * 如果希望原子操作的变量是User,Book,此时需要使用AtomicReference类
 */
public static void main(String[] args) {
    User z3 = new User("z3",18);
    User l4 = new User("l4",19);
    AtomicReference<User> atomicReference = new AtomicReference<>(z3);
    System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
    System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
    //[email protected]
    //false  [email protected]

}

AtomicReference存在ABA问题代码验证

demo

AtomicReference atomicReference = new AtomicReference<Integer>(100);
/**
 * ABA问题验证:
 * 1--ABA
 * 2--A,C
 * @param args
 */
public static void main(String[] args) {
    ABADemo abaDemo = new ABADemo();

    new Thread(()->{
        abaDemo.atomicReference.compareAndSet(100,101);
        abaDemo.atomicReference.compareAndSet(101,100);
    },"1").start();

    new Thread(()->{
        // 睡1s等线程1执行完ABA
        try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace();}
        System.out.println(abaDemo.atomicReference.compareAndSet(100, 2020)+"\t"+abaDemo.atomicReference.get());
        //true  2020

    },"2").start();
}

AtomicStampReference解决ABA问题代码验证

解决思路:每次变量更新的时候,把变量的版本号加一,这样只要变量被某一个线程修改过,该变量版本号就会发生递增操作,从而解决了ABA变化

AtomicStampedReference atomicStampedReference = new AtomicStampedReference<Integer>(100,1);

public static void main(String[] args) {
    // ABAProblem();
    ABADemo abaDemo = new ABADemo();

    new Thread(()->{
        // 等线程2读到初始版本号的值
        try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace();}
        System.out.println("线程1在ABA前的版本号:"+abaDemo.atomicStampedReference.getStamp());
        abaDemo.atomicStampedReference.compareAndSet(100,101,abaDemo.atomicStampedReference.getStamp(),abaDemo.atomicStampedReference.getStamp()+1);
        abaDemo.atomicStampedReference.compareAndSet(101,100,abaDemo.atomicStampedReference.getStamp(),abaDemo.atomicStampedReference.getStamp()+1);
        System.out.println("线程1在ABA后的版本号:"+abaDemo.atomicStampedReference.getStamp());
    },"1").start();

    new Thread(()->{
        // 存一下修改前的版本号
        int stamp = abaDemo.atomicStampedReference.getStamp();
        System.out.println("线程2在修改操作前的版本号:"+stamp);
        // 睡1s等线程1执行完ABA
        try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) { e.printStackTrace();}
        System.out.println(abaDemo.atomicStampedReference.compareAndSet(100,2020,stamp,abaDemo.atomicStampedReference.getStamp()+1)+ "\t" + abaDemo.atomicStampedReference.getReference());
        //线程2在修改操作前的版本号:1
        //线程1在ABA前的版本号:1
        //线程1在ABA后的版本号:3
        //false 100

        },"2").start();

}

集合类不安全

ArrayList线程不安全演示-什么故障?什么原因?怎么解决?

ArrayList底层是一个数组,默认大小10,超过就扩容,扩原值的一半10+5=15

线程不安全,因为add方法没有加锁。

不安全案例:

public class ContainerNotSafeDemo {

    /**
     * 1. 故障
     *      java.util.ConcurrentModificationException   并发修改异常
     *
     * 2. 原因
     *      线程并发修改导致,线程1正在写入,线程2抢占资源,导致数据不一致。
     *
     * 3. 解决
     *      1 new Vector(); add方法加锁,线程安全,并发性差。
     *      2 Collections.synchronizedList(new ArrayList<>()); 包装成安全的,还是加锁,并发性差。
     *      3 new CopyOnWriteArrayList<>();  juc的类,写时复制
     * 4. 优化
     */
    public static void main(String[] args) {
//        List<String> list = new ArrayList<>();
//        List<String> list = new Vector<>();
        List<String> list = Collections.synchronizedList(new ArrayList<>());
//        List<String> list = new CopyOnWriteArrayList<>();

        for (int i = 1; i <= 30; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,4));
                System.out.println(Thread.currentThread().getName()+"\t"+list);
            },String.valueOf(i)).start();
        }
    }
}

CopyOnWriteArrayList原理?它有什么好?

参考:https://blog.csdn.net/linsongbin1/article/details/54581787

add源码:

public boolean add(E e) {
    // 1. 加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 2. 拷贝数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 3. 新增元素到新数组
        newElements[len] = e;
        // 4. 将array引用指向新数组
        setArray(newElements);
        return true;
    } finally {
        // 解锁
        lock.unlock();
    }
}

解释写时复制:

  1. 写操作时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。整个add操作都是在锁的保护下进行的
  2. 读操作时,如果写完成且引用指向新数组,则读到的是最新数据;否则读到的是原始数组数据。可见读操作是不加锁的

CopyOnWriteArrayList 缺点&使用场合

  1. 消耗内存。写操作,拷贝数组,消耗内存,数组大的话可能导致gc
  2. 不能实时读。拷贝新增需要时间,读到的可能是旧数据,能保证最终一致性,但不满足实时要求。

因此,适合读多写少的场景。

CopyOnWriteArrayList透露的思想

  1. 读写分离,提高并发
  2. 最终一致性
  3. 通过另辟空间,来解决并发冲突

集合类不安全之Set-演示/故障/解决

Set同理

HashSet > Collections.synchronizedSet() > CopyOnWriteArraySet

且CopyOnWriteArraySet底层还是用的CopyOnWriteArrayList

HashSet底层是HashMap, add(key,一个常量)

集合类不安全之Map-演示/故障/解决

Map类似

HashMap > Collections.synchronizedMap() > ConcurrentHashMap

HashMap底层实现原理-jdk7

可参考:

  • 2018/10/30/20181030111752438/#10-4-HashMap与ConcurrentHashMap解析

  • HashMap源码分析:https://blog.csdn.net/weixin_36910300/article/details/79985197

map的存储结构是什么?

  1. Hash表(数组+链表)
  2. key-value构成一个entry对象
  3. 数组容量:默认16,始终保持 2^n(为了存取高效,减少碰撞,数据分配均匀)

new HashMap<>()底层发生了什么?

创建了长度=16的 Entry table。

源码分析

  • capacity : 当前数组容量
  • loadFactor:负载因子,默认为 0.75。
  • threshold:扩容的阈值,等于 capacity * loadFactor。当元素个数超过这个值就触发扩容。
  • MAXIMUM_CAPACITY = 1 << 30:最大的容量为 2 ^ 30
//调用无参构造器
public HashMap() {
    //默认初始容量大小为16,默认的加载因子为0.75f
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

public HashMap(int initialCapacity, float loadFactor) {
    //初始容量不能小于0
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //初始容量不能超过MAXIMUM_CAPACITY
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //加载因子不能小于等于0,或者加载因子不能是非数字   
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    //设置加载因子
    this.loadFactor = loadFactor;
    //设置临界值
    threshold = initialCapacity;
    //伪构造,里面没有代码
    init();
}

map.put(key1,value1)底层发生了什么?

  1. 求key1的hash值,调用hashCode(),该hash值用来计算Entry数组下标,得到存放位置。
  2. 如果该位置为空,则添加成功。
  3. 如果不为空,意味着该位置存在链表:
    1. 如果hash值与其他key不同,则添加成功。
    2. 如果key1的hash值有key与之相同,则调用equal(),继续比较:
      1. 如果equal返回false,则添加成功。
      2. 如果equal返回true,则使用value1替换旧值(修改操作)

map.get(key1)底层发生了什么?

  1. 根据key1计算hash,找到对应数组下标。
  2. 遍历该位置处的链表,找到key1.equal(key2)为true的entry,返回其value。

扩容的原理?

扩容后,数组大小为原来的 2 倍。

ConcurrentHashMap底层原理-jdk7

关键词:Segment数组/基于分段锁/提高并发

  1. 引入一个Segment数组。每个Segment单元都包含一个与HashMap结构差不多hash表

  2. 读取过程:

    1. 先取key的hash值,取高sshift位决定属于哪个Segment单元。
    2. 接着就是HashMap那一套
  3. Segment继承jucReetrantLock,上锁方便,即分段锁。因此segment[1]锁了,不影响其他Segment单元并发。

HashMap底层实现原理-jdk8

与jdk7的不同的地方:

  1. new HashMap()时,不创建长度为16的数组。

  2. 底层使用Node[], 而不是Entry[]

  3. 数组结构采用

    数组+链表+红黑树

    1. 触发时机:当某索引位置链表长度>8 且 数组长度>64时,次索引位置的链表改为红黑树
    2. 红黑树的关键性质: 从根到叶子最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。
    3. 目的:加快检索速度

! ConcurrentHashMap底层原理-jdk8

参考:https://www.jianshu.com/p/5dbaa6707017

底层结构

  • 和 1.8 HashMap 结构类似,也是数组+链表+红黑树的。取消了Segment 分段锁

那如何保证线程安全的?

  • CAS + synchronized + volatile 来保证并发安全性,具体的如下

put方法逻辑

源代码解析:

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    //1. 计算key的hash值
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //2. 判断Node[]数组是否初始化,没有则进行初始化操作
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //3. 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环。
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //4. 检查到内部正在扩容,就帮助它一块扩容。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 如果该坐标Node不为null且没有正在扩容,就加锁,进行链表/红黑树 节点添加操作
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //5. 当前为链表,在链表中插入新的键值对
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 6.当前为红黑树,将新的键值对插入到红黑树中
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // 7.判断链表长度已经达到临界值8(默认值),当节点超过这个值就需要把链表转换为树结构
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //8.对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就需要扩容 
    addCount(1L, binCount);
    return null;
}

解析:对当前的table进行无条件自循环直到put成功(cas自旋)

  1. 如果数组下标没有Node节点,就用CAS+自旋添加链表头节点。
  2. 如果有Node节点,就加synchronized,添加链表或红黑树节点。

get操作,由于数组被volatile修饰了,因此不用担心数组的可见性问题。

volatile Node<K,V>[] table;

Map类的各种对比

  • HashMap和ConcurrentHashMap对比
  • HashMap和HashTable的对比
  • HashTable和ConcurrentHashMap对比

Java锁

Java 15锁,列举一些?

1.公平锁 / 非公平锁
2.可重入锁 / 不可重入锁
3.独享锁 / 共享锁
4.互斥锁 / 读写锁
5.乐观锁 / 悲观锁
6.分段锁
7.偏向锁 / 轻量级锁 / 重量级锁
8.自旋锁

公平和非公平锁是什么?两者区别(优缺点)?两种锁举例?

是什么?

  • 公平锁:多个线程按照申请锁的顺序来获取锁。
  • 非公平锁:多个线程获取锁的顺序不是按照申请舒顺序来的,有可能后申请的线程比先申请的线程优先获取锁。
    高并发下,有可能造成优先级反转或者饥饿现象。

区别:

  • 公平锁:保证顺序(队列,FIFO),性能下降。
  • 非公平:先尝试直接占有锁,如果尝试失败,再采用类似公平锁的方式。优点在于吞吐量比公平锁大。

举例:

  • ReentrantLock可以指定创建公平锁或非公平锁,无参构造默认创建非公平锁。
  • synchronized是非公平的。

可重入锁是什么?与不可重入的区别?可重入锁举例?作用?实现原理?

是什么?

  • 也叫递归锁
  • 当一个线程获取某个对象锁后,可以再次获取同一把对象锁。
  • 即,线程可进入任何他所拥有的对象锁所同步着的代码块。

区别?

public synchronized  void m1(){
      m1();
}
  • 可重入锁:某线程进入外层m1后,可以再次进入递归m1方法。也叫递归锁。
  • 不可重入锁:某线程进入外部m1后,不可再进如内部m1,必须等待锁释放。这里就造成了死锁。

举例:

  • ReentrantLock/synchronized就是典型的可重入锁

作用:

  • 避免死锁。案例:递归

实现原理:

  • 计数器:进入最外层计数器=1,每递归一次,计数器+1,每退出一层,计数器-1,直到计数器=0,说明退出了最外层,此时该线程释放锁对象,其他线程才能获取该锁。

可重入锁代码验证

synchronized

public class SynchronizedLockDemo {

    public synchronized void m1(){
        System.out.println(Thread.currentThread().getName() + "----m1----");
        m2();
    }

    public synchronized void m2(){
        System.out.println(Thread.currentThread().getName() + "----m2----");
    }

    /**
     * 可以看出,m1,m2是同一把锁,只有线程释放最外层锁,其他线程才能占用该锁。
     * @param args
     */
    public static void main(String[] args) {
        SynchronizedLockDemo rd = new SynchronizedLockDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                rd.m1();
            },String.valueOf(i)).start();
        }
    }
    //0----m1----
    //0----m2----
    //4----m1----
    //4----m2----
    //3----m1----
    //3----m2----
    //2----m1----
    //2----m2----
    //1----m1----
    //1----m2----
}

ReentrantLock

public class ReentrantLockDemo {

    Lock lock = new ReentrantLock();

    public void m1(){
        lock.lock();
//        lock.lock();
        try {

            System.out.println(Thread.currentThread().getName() + "----m1----");
            m2();

        }finally {
            lock.unlock();
//            lock.unlock();
        }
    }

    public void m2(){
        lock.lock();
        try {

            System.out.println(Thread.currentThread().getName() + "----m2----");

        }finally {
            lock.unlock();
        }
    }

    /**
     * 锁要成对出现,否则:
     *  - 多一个lock.lock()会造成锁无法释放,程序卡住
     *  - 多一个lock.unlock()直接报错
     */
    public static void main(String[] args) {
        ReentrantLockDemo rd = new ReentrantLockDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                rd.m1();
            },String.valueOf(i)).start();
        }
        //0----m1----
        //0----m2----
        //2----m1----
        //2----m2----
        //1----m1----
        //1----m2----
        //3----m1----
        //3----m2----
        //4----m1----
        //4----m2----
    }
}

自旋锁是什么?优点?缺点?

  • 自旋锁(spinlock)是指尝试获取锁的对象不会立即阻塞,而是采用循环的方式取尝试获取锁。好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

手写一个自旋锁

public class SpinLockDemo {

    /**
     * 手写自旋锁
     * 自旋锁的核心:while+cas+原子引用线程
     *
     * A线程加锁,一顿操作5秒钟,解锁。B线程一直自旋等待A线程释放锁,然后获取锁。
     * 打印结果:
     * A尝试获取锁
     * A成功获取锁
     * A一顿操作5秒...
     * B尝试获取锁
     * A释放锁
     * B成功获取锁
     * B一顿操作
     * B释放锁
     */
    public static void main(String[] args) {
        SpinLockDemo sDemo = new SpinLockDemo();

        new Thread(()->{
            sDemo.myLock();

            System.out.println(Thread.currentThread().getName()+"一顿操作5秒...");
            try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}

            sDemo.myUnLock();

        },"A").start();

        // 保证线程A先上的锁
        try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}

        new Thread(()->{
            sDemo.myLock();

            System.out.println(Thread.currentThread().getName()+"一顿操作");

            sDemo.myUnLock();

        },"B").start();

    }

    AtomicReference atomicThreadRef = new AtomicReference<Thread>();
    // 加锁
    public void myLock(){
        // 线程A进来,发现是null值,成功操作cas把自己放进去
        // 线程B进来,发现不是null值,一直自旋等待线程A释放锁
        System.out.println(Thread.currentThread().getName()+"尝试获取锁");
        while (!atomicThreadRef.compareAndSet(null,Thread.currentThread())){

        }
        System.out.println(Thread.currentThread().getName()+"成功获取锁");
    }

    // 释放锁
    public void myUnLock(){
        // 线程A发现原子引用是自己,于是cas成功修改为null值,即释放锁
        atomicThreadRef.compareAndSet(Thread.currentThread(),null);
        System.out.println(Thread.currentThread().getName()+"释放锁");
    }
}

独占锁和共享锁是什么?举例?优缺点比较?

是什么?

  • 独占锁:写锁,该锁只能被一个线程所持有。
  • 共享锁:读锁,该锁可被多个线程所持有。

举例

  • ReentrantLock和sychronized都是独占锁。
  • ReentrantReadWriteLock,其读锁是共享锁,写锁是独占锁。

优缺:

  • 共享锁保证并发读是非常高效的;读写、写读、写写过程是互斥的。

验证读写锁ReentrantReadWriteLock

并发读写不安全演示

public class ReadWriteUnsafeDemo {

    Map<Integer,String> map = new HashMap<>();

    public void myPut(Integer key, String value){
        System.out.println(Thread.currentThread().getName() + "\t" + "正在写入:"+key);
        map.put(key ,value);
        System.out.println(Thread.currentThread().getName() + "\t" + "写入完成:"+key);
    }

    public void myGet(Integer key){
        System.out.println(Thread.currentThread().getName() + "\t" + "正在读取:"+key);
        String value = map.get(key);
        System.out.println(Thread.currentThread().getName() + "\t" + "读取完成:"+key+","+value);
    }

    /**
     * 并发读写不安全演示
     * 打印:
     * 0	正在写入:0
     * 2	正在写入:2
     * 4	正在写入:4
     * 3	正在写入:3
     * 1	正在写入:1
     * 3	写入完成:3
     * 0	正在读取:0
     * 4	写入完成:4
     * 0	读取完成:0,0
     * 2	写入完成:2
     * 0	写入完成:0
     * ...
     * 结论:写入不是原子操作,线程不安全
     *
     * 只锁put不锁get会发生什么?
     * 2	正在写入
     * 0	正在读取
     * 2	写入完成:2
     * 造成写时读,不安全。没有保证写的原子性。
     */
    public static void main(String[] args) {
        ReadWriteUnsafeDemo demo = new ReadWriteUnsafeDemo();
        for(int i=0; i<5; ++i){
            final int tmp = i;
            new Thread(()->{
                demo.myPut(tmp,String.valueOf(tmp));
            },String.valueOf(i)).start();
        }

        for(int i=0; i<5; ++i){
            final int tmp = i;
            new Thread(()->{
                demo.myGet(tmp);
            },String.valueOf(i)).start();
        }
    }
}

验证读写锁ReentrantReadWriteLock

public class ReadWriteLockDemo {

    // 缓存资源都加一下volatile,保证线程间可见
    volatile Map<Integer,String> map = new HashMap<>();
    ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void myPut(Integer key, String value){

        rwLock.writeLock().lock();
        try{
            System.out.println(Thread.currentThread().getName() + "\t" + "正在写入");
            map.put(key ,value);
            System.out.println(Thread.currentThread().getName() + "\t" + "写入完成:"+key);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            rwLock.writeLock().unlock();
        }
    }

    public void myGet(Integer key){

        rwLock.readLock().lock();
        try{
            System.out.println(Thread.currentThread().getName() + "\t" + "正在读取");
            String value = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t" + "读取完成:"+value);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            rwLock.readLock().unlock();
        }
    }

    /**
     * 要求:
     * - 读可并发
     * - 写和任何操作互斥
     * 核心:ReentrantReadWriteLock + volatile
     * 打印:
     * 2	正在写入
     * 2	写入完成:2
     * 3	正在写入
     * 3	写入完成:3
     * 4	正在写入
     * 4	写入完成:4
     * 4	正在读取
     * 2	正在读取
     * 1	正在读取
     * 1	读取完成:1
     * 3	正在读取
     * 结论:读写锁保证了写原子性,读并发
     */
    public static void main(String[] args) {
        ReadWriteLockDemo demo = new ReadWriteLockDemo();
        for(int i=0; i<5; ++i){
            final int tmp = i;
            new Thread(()->{
                demo.myPut(tmp,String.valueOf(tmp));
            },String.valueOf(i)).start();
        }

        for(int i=0; i<5; ++i){
            final int tmp = i;
            new Thread(()->{
                demo.myGet(tmp);
            },String.valueOf(i)).start();
        }
    }
}

什么是乐观锁/悲观锁?举例?

悲观锁

  • 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现

乐观锁

  • 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,采用版本号+cas的方式去保证线程安全。乐观锁适用于多读写少的应用类型,这样可以提高吞吐量。atomic包的类就是基于cas实现乐观锁的。

什么是乐观读/悲观读?

  • 悲观读:在没有任何读写锁的时候才能取得写入的锁,可用于实现悲观读取(读优先,没有读时才能写),读多写少的场景下可能会出现线程饥饿。
  • 乐观读:如果读多写少,就乐观的认为读写同时发生的情况少,因此不采用完全锁定的方式,而是采用cas实现乐观锁。

ReentrantReadWriteLock是乐观读还是悲观读?

读优先:在没有任何读写锁的时候才能取得写入的锁,可用于实现悲观读取(读优先,没有读时才能写),读多写少的场景下可能会出现线程饥饿。

! StempedLock作用?

参考:blog/2018/10/27/20181027234153307/#7-5-ReentrantLock与锁

它控制锁有三种模式(写、悲观读、乐观读)。

核心代码:

private final StampedLock sl = new StampedLock();
long stamp = sl.tryOptimisticRead(); // 获得一个乐观读锁
// stamp=0表示没有写锁入侵,
long stamp = sl.readLock(); // 获取悲观读锁
long stamp = lock.writeLock();// 获得写锁

ReentrantReadWriteLock是悲观读,读优先,而StempedLock可以指定。

synchronized和Lock的区别是什么?

  1. 原始构成
    synchronized是关键字,属于JVM层面(底层通过monitor对象完成,monitorenter\monitorexit)
    Lock是juc里具体的类,是API层面
  2. 使用方法
    synchronized不需要手动释放锁,代码块执行完就自动释放了
    ReentrantLock必须手动释放锁,否则可能导致死锁
  3. 等待是否可中断
    synchronized不可中断,除非抛异常或正常执行完毕
    ReentrantLock可中断
    1 lock.tryLock(long timeout, TimeUnit unit)
    2 lock.lockInterruptibly() 直接中断锁
  4. 加锁是否公平
    synchronized非公平
    ReentrantLock可以指定,默认非公平。
  5. 锁能否绑定多个条件(Condition)
    synchronized没有,要么随机唤醒一个,要么唤醒全部
    ReentrantLock实现分组唤醒,可精确唤醒。(详见AQS的Condition小节)
    Condition condition_pro = lock.newCondition();
    Condition condition_con = lock.newCondition();

AQS

什么是AQS?

  • AbstractQueuedSynchronizer类 - AQS

  • 是juc实现锁或其他同步工具的基础框架,数据结构是队列

  • 基于AQS的同步组件:CountDownLatch、Semaphore、CyclicBarrier、ReentrantLock、Condition

CountDownLatch -简述?应用场景?应用案例?常用方法?

public class CountDownLatchDemo {

    CountDownLatch countDownLatch = new CountDownLatch(6);

    /**
     * 一个需求:某线程要在前面线程都执行完任务后才能执行。
     * CountLatch应用场景:阻塞某线程直到前面所有线程执行完才被唤醒。
     * demo.countDownLatch.await();
     *      等待前面线程执行完
     * countDownLatch.await(10, TimeUnit.MILLISECONDS);
     *      只等待指定时间,超过就不继续等待。
     * 打印:
     * 0	执行任务
     * 4	执行任务
     * 3	执行任务
     * 2	执行任务
     * 1	执行任务
     * 5	执行任务
     * main	最后执行执行任务
     */
    public static void main(String[] args) throws InterruptedException {

        CountDownLatchDemo demo = new CountDownLatchDemo();

        for(int i=0; i<6; ++i){
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"\t"+"执行任务");
                // 一个线程执行完任务就-1
                demo.countDownLatch.countDown();
            },String.valueOf(i)).start();
        }

        // 阻塞,等待countDownLatch变为0,即所有任务线程执行完。
        demo.countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"\t"+"最后执行执行任务");
    }
}

CyclicBarrier- 简述?应用场景?应用案例?常用方法?

/**
 * CyclicBarrier
 *  - 功能描述:使一组线程到达一个屏障(Barrier)时被阻塞,直到最后一个线程到达屏障时,所有被屏障拦截的线程才能全部被唤醒继续操作。
 *  - 使用方法:线程调用await(),会进入等待状态,且计数器-1,直到计数器=0时,所有线程被唤醒。
 *  - 应用场景:5个线程执行完ready任务后都相互等待,直到5个线程的ready任务都执行完毕再执行continue任务。
 *  - 常用方法:
 *      barrier.await();
 *          等待其他线程
 *      barrier.await(2000, TimeUnit.MILLISECONDS);
 *          等待指定时间后会抛异常。
 *      new CyclicBarrier(5, () -> {})
 *          在ready和continue之间会执行的代码放在此处
 *  - 计数器重复使用(Cyclic):cyclicBarrier.reset();
 *
 * 打印:
 * 0	执行ready
 * 2	执行ready
 * 3	执行ready
 * 1	执行ready
 * 4	执行ready
 * 集中点,此时所有线程完成ready操作
 * 4	执行continue
 * 3	执行continue
 * 0	执行continue
 * 1	执行continue
 * 2	执行continue
 */
public class CyclicBarrierDemo {

    CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
        System.out.println("集中点,此时所有线程完成ready操作");
    });

    public static void main(String[] args) {
        CyclicBarrierDemo demo = new CyclicBarrierDemo();

        for(int i=0; i<5; ++i){
            new Thread(()->{

                System.out.println(Thread.currentThread().getName()+"\t"+"执行ready");
                try {
                    demo.cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"执行continue");
            },String.valueOf(i)).start();
        }

        // 循环使用计数器
        demo.cyclicBarrier.reset();

        // 下一波操作
    }
}

CyclicBarrier和CountDownLatch的使用区别?

想它们的代码案例

Semaphore- 简述?应用场景?应用案例?常用方法?

/**
 * Semaphore
 *  - 使用场景:
 *      demo1-共享资源互斥(吃海底捞,座位有限,但是可以等待别人吃完,再进去,不用离开)
 *      demo2-控制并发数(线程池,超过并发数,就抛弃连接)
 *  demo1-打印:
 * 0   占到座位
 * 1   占到座位
 * 2   占到座位
 * 0   释放座位
 * 2   释放座位
 * 3   占到座位
 * 4   占到座位
 * 1   释放座位
 * 5   占到座位
 * 3   释放座位
 * 5   释放座位
 * 4   释放座位
 *  demo2-打印:
 * 0   尝试获取连接
 * 2   尝试获取连接
 * 3   尝试获取连接
 * 1   尝试获取连接
 * 5   尝试获取连接
 * 4   尝试获取连接
 * 5   连接成功
 * 1   连接成功
 * 4   连接成功
 * 3   连接失败
 * 0   连接失败
 * 2   连接失败
 */
public class SemaphoreDemo {

    public static void main(String[] args) {
        // demo1
        haidilao();
        // demo2
        xianchengchi();
    }

    // 线程池:最大连接数为3,超过就抛弃连接。
    private static void xianchengchi() {
        Semaphore semaphore = new Semaphore(3);
        for(int i=0; i<6; ++i){
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+"\t"+"尝试获取连接");
                    // 只等200毫秒,等不到就抛弃,连接失败
                    if(semaphore.tryAcquire(200, TimeUnit.MILLISECONDS)){
                        System.out.println(Thread.currentThread().getName()+"\t"+"连接成功");
                        TimeUnit.SECONDS.sleep(1);
                    }else {
                        System.out.println(Thread.currentThread().getName()+"\t"+"连接失败");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }

    // 海底捞:3个座位6个人
    private static void haidilao() {
        Semaphore semaphore = new Semaphore(3);
        for(int i=0; i<6; ++i){
            new Thread(()->{
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"\t"+"占到座位");
                    // 吃一会儿
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    System.out.println(Thread.currentThread().getName()+"\t"+"释放座位");
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

Condition是什么?核心方法?使用案例?

Condition:多线程间协调通信的工具类
核心:

 Condition condition = reentrantLock.newCondition();
 condition.await();
 condition.signalAll();

使用案例:

2018/10/27/20181027234153307/#7-5-ReentrantLock与锁

Condition分组唤醒是什么?锁绑定多个Condition使用案例?

核心代码:

// 生产者组
Condition condition_pro = lock.newCondition();
// 消费者组
Condition condition_con = lock.newCondition();
// 阻塞当前生产者线程
condition_pro.await();
// 唤醒所有生产者
condition_pro.signalAll();
// 阻塞当前消费者线程
condition_con.await();
// 唤醒所有消费者
condition_con.signalAll();

锁绑定多个Condition使用案例:

/**
 * 锁绑定多个条件(Condition) 案例
 * 要求:A打印3次,通知B打印4次,通知C打印5次;循环5轮;
 *
 * 线程   操作  资源类
 * 判断   干活  通知
 * 防止假唤醒while
 */
public class MultiConditionDemo {

    public static void main(String[] args) {
        Resources rs = new Resources();
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                rs.pa3();
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                rs.pb4();
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                rs.pc5();
            }
        },"C").start();
    }
}

class Resources{
    Lock lock = new ReentrantLock();
    String thread = "A";    // 当前线程
    Condition cA = lock.newCondition();
    Condition cB = lock.newCondition();
    Condition cC = lock.newCondition();

    public void pa3(){
        lock.lock();
        try{
            while(!thread.equals("A")){
                cA.await();
            }
            for (int i = 0; i < 3; i++) {
                System.out.println("A打印");
            }
            // A做完了通知B
            thread="B";
            cB.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void pb4(){
        lock.lock();
        try{
            while(!thread.equals("B")){
                cB.await();
            }
            for (int i = 0; i < 4; i++) {
                System.out.println("B打印");
            }
            // B做完了通知C
            thread="C";
            cC.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void pc5(){
        lock.lock();
        try{
            while(!thread.equals("C")){
                cC.await();
            }
            for (int i = 0; i < 5; i++) {
                System.out.println("C打印");
            }
            // C做完了通知A
            thread="A";
            cA.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

阻塞队列

什么是阻塞队列?什么情况会导致阻塞是?主要应用场景?

队列:FILO

以下情况发生阻塞:

  • 队满时入队
  • 队空时出队

应用场景:生产者消费者模式

为什么要用阻塞队列?有什么好?

阻塞就是线程挂起,当满足条件后,又被唤醒。

为什么需要BlockingQueue?

  • 不再需要关心线程阻塞和唤醒的时机,因为BlockingQueue包办了这个细节
  • 在Juc出现前,程序员必须手动控制这些细节,兼顾效率和线程安全,增大了开发难度。

BlockingQueue的架构图?有哪些实现类?

架构图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KCUsapF3-1584804080927)(20200221192831555/BlockingQueue架构图.png)]

实现类:

  • *ArrayBlockingQueue: 由数组结构组成的有界阻塞队列
  • *LinkedBlockingQueue: 由链表结构组成的有界阻塞队列(但默认大小为Integer.MAX_VALUE,大小配置可选)
  • PriorityBlockingQueue: 支持优先级排序的*阻塞队列
  • DelayQueue: 使用优先级队列实现的延迟*阻塞队列(内部采用PriorityQueue(排序)与ReentrantLock(锁)实现)
  • *SynchronousQueue: 队列只插入一个元素,同步队列
  • LinkedTransferQueue: 由链表结构组成的*阻塞队列
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列

带*是重要的,是线程池的底层实现。

! BlockingQueue的核心方法?

2018/10/29/20181029185403603/#7-3-BlockingQueue

三套操作:插入、移除、检查。

四套处理:抛异常,返回特殊值,阻塞,超时

一个图表

SynchronousQueue使用案例?

/**
 * SynchronousQueue案例
 * 打印:
 * 消费者	take a
 * 生产者	put a
 * 消费者	take b
 * 生产者	put b
 * 消费者	take c
 * 生产者	put c
 */
public class BlockingQueueDemo {

    public static void main(String[] args) {
        BlockingQueue blockingQueue = new SynchronousQueue();

        new Thread(()->{
            try {
                blockingQueue.put("a");
                System.out.println(Thread.currentThread().getName()+"\tput a");
                blockingQueue.put("b");
                System.out.println(Thread.currentThread().getName()+"\tput b");
                blockingQueue.put("c");
                System.out.println(Thread.currentThread().getName()+"\tput c");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"生产者").start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName()+"\ttake "+blockingQueue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName()+"\ttake "+blockingQueue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName()+"\ttake "+blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"消费者").start();
    }
}

阻塞队列的应用场景举例?

1、生产者消费者模式

2、线程池底层原理

3、消息中间件

生产者消费者模式的传统版实现?(synchronized版和Lock版)

1、sync+wait+notify

/**
 * 传统版1:sync+wait+notify
 * 题目:变量初始值为0,两个线程交替操作,一个加1一个减1,进行5轮。(线程1,线程2,线程1,线程2...)
 * 打印:
 * A	plus	num=1
 * B	reduce	num=0
 * A	plus	num=1
 * B	reduce	num=0
 * A	plus	num=1
 * B	reduce	num=0
 * A	plus	num=1
 * B	reduce	num=0
 * A	plus	num=1
 * B	reduce	num=0
 */
public class ProdConsumer_TDemo1 {

    public static void main(String[] args) {
        Number1 number = new Number1();

        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                number.plus();
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                number.reduce();
            }
        },"B").start();
    }
}

class Number1{
    private int num = 0;

    public synchronized void plus(){

        // 使用while不用if,防止线程虚假唤醒。当前线程被唤醒后,会重新走验证程序,不会直接往下执行。
        while( num!=0 ){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num++;
        System.out.println(Thread.currentThread().getName()+"\tplus"+"\tnum="+num);
        this.notify();
    }

    public synchronized void reduce(){

        // 使用while不用if,防止线程虚假唤醒。当前线程被唤醒后,会重新走验证程序,不会直接往下执行。
        while( num==0 ){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num--;
        System.out.println(Thread.currentThread().getName()+"\treduce"+"\tnum="+num);
        this.notify();
    }
}

2、lock+await+signalAll(Condition)

/**
 * 传统版2:lock+await+signalAll(Condition)
 * 题目:变量初始值为0,两个线程交替操作,一个加1一个减1,进行5轮。(线程1,线程2,线程1,线程2...)
 * 打印:
 * A	plus	num=1
 * B	reduce	num=0
 * A	plus	num=1
 * B	reduce	num=0
 * A	plus	num=1
 * B	reduce	num=0
 * A	plus	num=1
 * B	reduce	num=0
 * A	plus	num=1
 * B	reduce	num=0
 */
public class ProdConsumer_TDemo2 {

    public static void main(String[] args) {
        Number2 number = new Number2();

        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                number.plus();
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                number.reduce();
            }
        },"B").start();
    }


}

class Number2{
    private int num = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void plus(){
        // 线程1调用了lock方法,加入到了AQS的等待队里里面去
        lock.lock();
        try{
            // 使用while不用if,防止线程虚假唤醒。当前线程被唤醒后,会重新走验证程序,不会直接往下执行。
            while( num!=0 ){
                // 调用await方法后,从AQS队列里移除了,进入到了condition队列里面去,等待一个信号
                condition.await();
            }
            num++;
            System.out.println(Thread.currentThread().getName()+"\tplus"+"\tnum="+num);
            // 调用signalAll发送信号的方法,Condition节点的线程1节点元素被取出,放在了AQS等待队列里(注意并没有被唤醒)
            condition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 线程1释放锁,这时候AQS队列中只剩下线程2,线程2开始执行
            lock.unlock();
        }

    }

    public void reduce(){
        lock.lock();
        try{
            // 使用while不用if,防止线程虚假唤醒。当前线程被唤醒后,会重新走验证程序,不会直接往下执行。
            while( num==0 ){
                condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName()+"\treduce"+"\tnum="+num);
            condition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }

    }

}

生产者消费者模式的阻塞队列版实现?

/**
 * 使用阻塞队列实现生产者消费者(不使用Lock/Condition相关的东西)。
 * 题目:当flag=true时,启动生产者和消费者线程,一直循环做生产消费,当flag=false时,停止所有线程,程序运行结束。
 * 测试:开启一个生产者和一个消费者,5秒钟修改flag,看执行效果。
 * 核心:
 *  num++操作改成 AtomicInteger;
 *  BlockingQueue不写具体类,留接口给外部;
 *  flag标记对所有线程可见,所以需要volatile。
 *  打印:
 *  启动生产者
 * 启动消费者
 * 生产者	成功入队	蛋糕1
 * 消费者	成功出队	蛋糕1
 * 生产者	成功入队	蛋糕2
 * 消费者	成功出队	蛋糕2
 * 生产者	成功入队	蛋糕3
 * 消费者	成功出队	蛋糕3
 * 生产者	成功入队	蛋糕4
 * 消费者	成功出队	蛋糕4
 * 生产者	成功入队	蛋糕5
 * 消费者	成功出队	蛋糕5
 * 消费者	消费等待超过2秒,消费失败
 */
public class ProdConsumer_BDemo {
    public static void main(String[] args) {
        // 如果生产速度快,就把容量弄大点,免得经常入队失败
        MyResource rs = new MyResource(new ArrayBlockingQueue<String>(3));

        new Thread(()->{
            System.out.println("启动"+Thread.currentThread().getName());
            try {
                rs.myProducer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"生产者").start();

        new Thread(()->{
            System.out.println("启动"+Thread.currentThread().getName());
            try {
                rs.myConsumer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"消费者").start();

        // 5s后修改flag,结束程序
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        rs.STOP();

    }
}

class MyResource{

    private AtomicInteger atomicInteger = new AtomicInteger(0);
    private BlockingQueue<String> blockingQueue = null;
    private volatile boolean FLAG=true;


    public MyResource(BlockingQueue<String> blockingQueue) {
        this.blockingQueue = blockingQueue;
    }

    public void STOP(){
        FLAG=false;
    }

    public void myProducer() throws InterruptedException {
        int id;
        String cake;
        boolean result;
        while (FLAG){
            // 创建蛋糕
            id = atomicInteger.incrementAndGet();// 蛋糕id
            cake = "蛋糕"+id;
            // 蛋糕入队
            result = blockingQueue.offer(cake, 2, TimeUnit.SECONDS);
            if(result){
                System.out.println(Thread.currentThread().getName()+"\t成功入队\t"+cake);
            }else {
                // 说明队列满了
                System.out.println(Thread.currentThread().getName()+"\t入队失败\t"+cake);
            }

            // 生产者产的慢,产一个消费者就吃一个。
            // 生产者产的快,就会入队失败,等消费者来吃。
            TimeUnit.SECONDS.sleep(1);
        }
    }

    public void myConsumer() throws InterruptedException {
        while (FLAG){
            // 消费蛋糕
            String cake = blockingQueue.poll(2, TimeUnit.SECONDS);
            if(cake==null || cake.equals("")){
                System.out.println(Thread.currentThread().getName()+"\t消费等待超过2秒,消费失败");
                // 通知生产者也退出,不然生产者一直循环尝试入队。
//                FLAG=false;
//                return;
            }else{
                System.out.println(Thread.currentThread().getName()+"\t成功出队\t"+cake);
            }
            // TimeUnit.SECONDS.sleep(2);
        }
    }
}

线程池

获得多线程的方式有几种?

1、new Thread()

2、实现Runable接口

3、实现Callable接口

4、线程池

Runable Vs Callable

  1. Callabla有返回值
  2. Callable会抛异常
  3. Callable是call()方法

Callable基本使用(demo)

class MyThread implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"\t调用call方法");
        TimeUnit.SECONDS.sleep(1);
        return 1024;
    }
}

/**
 * Callable的基本使用
 * 打印:
 * main    调用
 * A   调用call方法
 * call返回:1024
 */
public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //FutureTask(Callable<V> callable)
        //FutureTask<V> implements RunnableFuture<V>
        FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());

        new Thread(futureTask,"A").start();

        System.out.println(Thread.currentThread().getName()+"\t调用1");

        // 如果没有计算完成,就不能执行下面代码
        while (!futureTask.isDone()){

        }

        // get方法建议放在最后调用:因为
        // get方法会要求返回Callable计算结果,如果没有完成,会导致其他线程阻塞,直到计算出结果返回。
        Integer result = futureTask.get();
        System.out.println("call返回:"+result);

        System.out.println(Thread.currentThread().getName()+"\t调用2");

    }
}

线程池主要工作?特点?为什么用?优势?

主要工作:控制运行线程的数量,将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过规定的最大数量,超出的线程将排队等候,等其他线程执行完毕,再从队列中取出任务执行。

主要特点:线程复用;控制最大并发数;管理线程;

优势:

  1. 降低资源消耗。重复利用已创建的线程。
  2. 提高响应速度。任务到达时,不需要等线程创建,直接从池里拿。
  3. 提高线程客观理性。统一分配、监控、调优。

线程池相关类架构图

Java面试之JUC

! 线程池创建方式有多少种?

  1. Executors.newScheduledThreadPool() // 和调度相关
  2. Executors.newWorkStealingPool() // java8新增,可用处理器作为它的并行级别
    三个常用实现方式:
  3. Executors.newFixedThreadPool() // 固定个数线程。应用场景:执行长期任务,性能好。
  4. Executors.newSingleThreadExecutor(); // 线程池就一个线程。应用场景:任务需要一个个执行。
  5. Executors.newCachedThreadPool(); // 可扩容的,怎么扩由内部机制决定。应用场景:执行很多短期异步小程序 或 负载较轻的服务器。

线程池3个常用的创建方式(demo)? 及底层实现?

demo

public class ExecutorDemo {
    /**
     * 模拟10个用户交给线程池的5个线程处理
     *
     * newFixedThreadPool打印:
     * pool-1-thread-1 办理业务
     * pool-1-thread-5 办理业务
     * pool-1-thread-1 办理业务
     * pool-1-thread-4 办理业务
     * pool-1-thread-2 办理业务
     * pool-1-thread-3 办理业务
     * pool-1-thread-2 办理业务
     * pool-1-thread-4 办理业务
     * pool-1-thread-1 办理业务
     * pool-1-thread-5 办理业务
     *
     * newSingleThreadExecutor打印:
     * pool-1-thread-1 办理业务
     * pool-1-thread-1 办理业务
     * pool-1-thread-1 办理业务
     * pool-1-thread-1 办理业务
     * pool-1-thread-1 办理业务
     * pool-1-thread-1 办理业务
     * pool-1-thread-1 办理业务
     * pool-1-thread-1 办理业务
     * pool-1-thread-1 办理业务
     * pool-1-thread-1 办理业务
     *
     * newCachedThreadPool打印:
     * pool-1-thread-1 办理业务
     * pool-1-thread-4 办理业务
     * pool-1-thread-3 办理业务
     * pool-1-thread-2 办理业务
     * pool-1-thread-6 办理业务
     * pool-1-thread-5 办理业务
     * pool-1-thread-8 办理业务
     * pool-1-thread-7 办理业务
     * pool-1-thread-9 办理业务
     * pool-1-thread-2 办理业务
     */
    public static void main(String[] args) {

//        ExecutorService threadPool = Executors.newFixedThreadPool(5);
//        ExecutorService threadPool = Executors.newSingleThreadExecutor();
//        ExecutorService threadPool = Executors.newCachedThreadPool();
//        Executors.newScheduledThreadPool()

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,
                5,
                1,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardPolicy()   // 如果是CallerRunsPolicy,打印 main    办理业务,说明将任务回退给了调用者main线程
        );

        try{
            for (int i = 0; i < 11; i++) {
                // 执行
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"\t办理业务");
                });
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            // 关闭
            threadPool.shutdown();
        }
    }
}

底层:

底层实现:


newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}


newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}




newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}


ThreadPoolExecutor 7参数介绍?

7大参数:


public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)


介绍:

  1. corePoolSize: 线程池中的常驻核心线程数。
    1)任务来了,会安排线程池中的线程去执行任务
    2)当线程池中线程数到达corePoolSize,就会把到达的任务放到缓存队列中。
  2. maximumPoolSize:线程池能容纳同时执行的最大线程数(>=1)。
  3. keepAliveTime:多余空闲线程的存活时间。
    当前线程池数>corePoolSize 且 空闲时间>keepAliveTime时,多余空闲线程会被销毁直到剩下corePoolSize个线程。
  4. unit:keepAliveTime单位
  5. workQueue:任务队列,被提交但未被执行的任务。
  6. threadFactory:线程工厂,用于创建线程,一般用默认的即可
  7. handler:拒绝策略。当队满 且 工作线程>maximumPoolSize时,如何拒绝请求执行的runnable策略。

线程池底层工作原理?处理流程描述?

核心:

1、ThreadPoolExecutor 7参数

2、阻塞队列

线程池工作流程图:

Java面试之JUC

线程池拒绝策略时什么?有哪些?分别是什么意思?

是什么?

  • 等待队列满了,池中max线程也到了,这时就调用拒绝策略处理这个问题。

jdk内置4中拒绝策略:

RejectedExecutionHandler

  • AbortPolicy:默认。直接抛出RejectedExecutionException异常阻止系统正常运行。
  • CallerRunsPolicy:“调用者运行”。不抛弃任务也不抛异常,而是将任务回退给调用者,从而降低新任务流量。
  • DiscardOldestPolicy:丢弃等待最久的任务。然后将当前任务加入队列,尝试再次提交当前任务。
  • DiscardPolicy:直接丢弃任务。不做处理,不抛异常。如果允许任务丢弃,这是最好的策略。

线程池在实际生产中用哪一个?

jdk提供了内置的(Executors返回的),一般生产环境不用的,只能使用自定义的。

为什么生产中不用jdk自带的线程池?

(答案来自阿里开发手册)

首先线程不允许显式创建,必须使用线程池。其次,不用jdk自带的线程池,原因如下:
1)FixedThreadPool和SingleThreadExecutor
底层是LinkedBlockingQueue,允许请求队列长度为Integer.MAX_VALUE,可能造成大量请求堆积,导致OOM
2)CachedThreadPool和ScheduledThreadPool
maximumPoolSize设置的是Integer.MAX_VALUE,可能造成大量请求堆积,导致OOM

自定义线程池(demo)

demo


ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
        2,
        5,
        1,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(3),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.DiscardPolicy()   // 如果是CallerRunsPolicy,打印 main    办理业务,说明将任务回退给了调用者main线程
);

合理配置线程池参数,如何考虑?

分情况讨论:

  1. cpu密集型。
    1. 即大量计算,没有阻塞。
    2. 一般公式:CPU核数+1 个线程的线程池
  2. io密集型
    1. 并不是一直在跑计算,很可能有很多阻塞等待。
    2. 尽可能多开,公式:cpu核数*2。
    3. 公式2:cpu核数/(1-阻塞系数),阻塞系数在0.8~0.9之间。8核就是8/(1-0.9) = 80个线程。

死锁是什么?产生死锁的主要原因?

多线程抢资源造成互相等待

原因:

  1. 系统资源不足
  2. 进程推荐顺序不合适
  3. 资源分配不当

死锁案例(demo)

class HoldLockThread implements Runnable{
    private String lockA;
    private String lockB;

    public HoldLockThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }


    @Override
    public void run() {
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName()+"\t持有"+lockA+"\t等待"+lockB);

            try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace(); }
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+"\t持有"+lockB+"\t等待"+lockA);
            }

        }
    }
}

/**
 * 死锁案例
 * 打印:
 * A   持有钥匙1  等待钥匙2
 * B   持有钥匙2  等待钥匙1
 */
public class DeadLockDemo {

    public static void main(String[] args) {
        new Thread(new HoldLockThread("钥匙1","钥匙2"),"A").start();
        new Thread(new HoldLockThread("钥匙2","钥匙1"),"B").start();

    }
}

怎么排查死锁问题?使用过哪些命令行工具?

解决:
jps -l // 查看java进程
jstack 进程号 // 查看堆栈信息
打印:Found 1 deadlock.

相关标签: Java面试专栏