Java面试之JUC

【Java面试经验】JUC部分(对应周阳老师的面试教程) 写在最前:此篇文章对应周阳老师的大厂面试教程,几乎包含了大厂所有JUC部分的面试题,虽然自己无缘大厂,但是这些知识也让我在面试中得到了不错的评价。需要找工作的同学可以搭配另一篇文章来使用 JUC java.util.concurrent的缩写 线程的6种状态 创建,可运行,阻塞,等待,计时等待,终结 Lambda表达式 java1.8之后允许接口中有部分方法的实现,需要用default关键字描述方法 @FunctionalInterface 注解表示函数式接口(仅有一个抽象方法) 常见异常总结 Concur 阅读详情

引言: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());
    //truecom.mxx.juc.User@4554617c
    //false  com.mxx.juc.User@4554617c

}

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.youkuaiyun.com/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.youkuaiyun.com/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. 提高线程客观理性。统一分配、监控、调优。

线程池相关类架构图

image-20200229220601618

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

  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、阻塞队列

线程池工作流程图:

image-20200229221227651

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

是什么?

  • 等待队列满了,池中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.

告别‘小爱同学’:用ESP32-S3和esp-sr SDK,从零打造一个离线语音助手(含完整代码) 本文详细介绍了如何利用ESP32-S3芯片和esp-sr SDK从零构建一个离线语音助手,涵盖硬件选型、声学设计、SDK配置及低功耗优化等关键步骤。通过实战案例和完整代码,帮助开发者实现自定义唤醒词和命令词,打造低成本、高隐私的本地语音控制解决方案。 阅读详情

相关推荐

Altium Designer 16安装教程(Windows11、小白教程)

Altium Designer 16在Windows11中的超详细安装教程

qq_47003770的博客 1万+

18个JUC常见面试题

ThreadLocal 是一种线程本地变量,可以允许多个线程并发访问同一个变量,但它是基于线程的隔离机制实现的,可以保证变量对每个线程都是独立的。但它只能实现独占锁,不能实现读写分离。Phaser(阶段器)是一种新的线程同步工具,主要用于分阶段的任务,它可以灵活的控制阶段的完成次数,当所有关联的线程都完成当前阶段时,才可以进入下一阶段,可以。CAS 的实现原理是,先读取变量的内存地址中的值,和期望的值进行比较,如果相同,则将新值写入这个内存地址,如果不同,则重试整个操作,直到交换成功为止。

2301_77181435的博客 4017

【python】epub的电子书繁体字与简体字转换

中文繁体与简体转换

xiexianyou666的博客 3090

并发编程——JUC并发面试问题

现如今,不管是应届毕业生还是工作三五年的工程师,在面试招聘的时候JUC并发编程是必须掌握的一个技能,否者你的面试结果可能会很差。本博文主要是整理JUC相关面试问题和答案。帮助大家在面试过程中能够回答面试官问题。本人将不断更新的JUC并发编程的相关面试问题。帮助小伙伴更好通过面试。

庄小焱 3660

基于机器学习和辐射传输的地表温度模拟方法对比

对于不同的地表类型和不同的年份,经验方法是实用的,虽然整体精度不如物理方法,但具有计算速度快、对模型内部结构考虑较少等优点。尽管深度学习等回归的经验方法不需要考虑辐射传输和能量平衡的具体过程, 但在其输入参数的选择上也不能是无序和随机的,而是应选择有关相应物理过程的参数。经验方法在处理不同年份相同地表类型或表现相对较好, RMSE 约为 1.50K,但在不同地表类型且不同年份模拟结果较差, RMSE 约为 3.0K, 虽然精度不如物理方法,但它具有计算速度快,不需过多考虑模型内部结构的优点。

2301_77907355的博客 1023

SAP AI Core 高阶能力实战参考

文章摘要:本文探讨了在企业SAP架构中整合GenAI技术的工程化实践,重点分析了SAP AI Core的高阶能力应用。作者指出,企业级AI应用需解决多轮对话管理、Git同步工作流、提示词治理等核心问题,而非简单接入大模型API。技术方案展示了如何通过SAP AI Core的orchestration service管理对话历史,利用GitOps实现AI资产版本控制,并强调prompt template在审计合规中的治理价值。全文以SAP技术栈(ABAP/RAP/Fiori)为背景,阐释了AI能力与企业核心系

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 104

java——juc篇

本篇文章汇总了java并发编程的常见八股文

Michael_Jay的博客 1703

基于应用谱减法进行语音去噪的算法

在上述代码中首先通过读取语音信号和噪声信号,将两个信号都转换为频域信号,然后计算出语音信号在各频率下的功率谱和噪声信号在各频率下的功率谱,并基于此计算信噪比,接着根据信噪比计算出阈值,并将低于阈值的能量对应的频段进行清零操作,最终实现去噪处理,最后将处理后的信号转换回时域信号。该算法采用谱减法进行降噪处理,能有效地去除语音信号中的噪声,但是在实际应用中,该方法也存在一些问题,如会对语音信号的高频部分产生影响,所以需要在应用过程中进行适当的调整。

weixin_50547796的博客 193

面试题篇-02-Java-Juc相关面试题

ConcurrentHashMap 在JDK 1.7 中使用的数组 加 链表的结构,其中数组分为两类,大树组Segment 和 小数组 HashEntry,而加锁是通过给Segment 添加ReentrantLock 重入锁来保证线程安全的。ConcurrentHashMap 在JDK1.8 中使用的是数组 加 链表 加 红黑树的方式实现,它是通过CAS或者来保证线程安全的,并且缩小了锁的粒度,查询 性能也更高。JMM。

Alan0517 787

天地图下载带审图号(审图号: GS(2024)0650号)的 “行政区划” 数据(含shp文件下载地址)

在科研投稿论文过程中,多数期刊杂志要求我们制作涉及国界的图件,必须使用自然资源部地图技术审查中心发布的标准地图底图。如地理学报、地理科学等期刊的投稿指南中写到:非标准底图本刊不予发表!(如图1.1所示)如果一定要用,凡涉国界图件(国内部分地区、全国、世界部分地区、全球)必须使用自然资源部标准地图服务系统标准地图底图(http://bzdt.ch.mnr.gov.cn/)(如图1.1所示)或国家测绘局地理信息局标准地图底图(如图1.2所示),所用底图边界要完全无修改(包括南海诸岛位置)。

JData_Engineer的博客 2304

使用IGV软件可视化染色质可及性

染色质可及性(chromatin accessibility)是指DNA序列在植物细胞核中的可访问程度。染色质是由DNA和蛋白质组成的复杂结构,它紧密地包裹在细胞核中,并影响基因的表达和功能。染色质可及性研究着重于理解染色质是否容易被转录因子和其他调控蛋白访问,从而影响基因的表达。文献中,常常会有下面这种图片来表示基因组上的一些区域或基因的染色质开放性强弱。一般情况下,我们可以使用IGV软件或GSA-man软件进行可视化,两者的操作相似。这里,以IGV软件为例,演示如何对染色质可及性进行可视化。

沉香best的博客 3724

JUC包常见面试题及答案

synchronized 是 Java 中的关键字,它是 JVM 内置的同步机制,可以用于修饰代码块或方法,当线程执行 synchronized 代码时,会自动获取对象的锁,其他线程需要等待锁的释放后才能执行。在低并发情况下,两者的性能差别不大,但在高并发情况下,LongAdder 的性能更优,因为它采用了分段锁的方式,将一个变量分成多个部分进行计数,避免了多个线程同时竞争同一个变量的情况,从而提高了并发性能。线程池是一种管理线程的机制,可以避免频繁地创建和销毁线程,提高代码的效率和稳定性。

lonely_baby的博客 1027

cartographer运行错误集合

播放bag的时候错误 Check failed: sensor_to_tracking->translation().norm() < 1e-5 The IMU frame must be colocated with the tracking frame. Transforming linear acceleration into the tracking frame will otherwise be imprecise tracking frame不是imu的link 修改lua文件中的.

Studying…… 2029

java juc面试题一般问什么?juc面试分析

对程序员来说,最重要的一个门槛就是面试了,虽然常有面试造核弹,工作拧螺丝的说法,但该要的面试还是要的,这里整理了一些java中juc相关面试题,快来了解下吧。 一、juc具体是? Java 5.0 提供了java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常见的实用工具类,用于定义类似于编程的自定义子系统,包括线程池、异步IO和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文的Collection实现等。 二、什么是CAS? CAS(Compare-A

javase1085的博客 2962

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

Java大厂面试:JUC深度解析——张小明与Kevin的对决

这篇文章讲述了在互联网大厂Java求职面试中,两位应聘者张小明和Kevin关于JUC相关知识的面试过程。面试官从简单的volatile关键字开始提问,逐渐深入到原子性、锁机制、线程池等知识,两位应聘者的回答水平逐渐拉开差距,最后面试官让张小明回家等通知,Kevin表现得到认可。文章还详细解析了每个问题的答案涉及的业务场景和技术点,适合小白学习

joshua的博客 1127

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

《互联网大厂面试:Java 核心、JUC、JVM 等知识大考验》

在互联网大厂的一间安静面试室内,严肃的面试官坐在桌前,对面坐着紧张又期待的求职者王铁牛。面试开始,一场对 Java 多项核心知识的考验拉开帷幕。:“我们先从基础的 Java 核心知识开始。你能说说 Java 中多态的实现方式有哪些吗?:“多态的实现方式主要有两种,一种是方法重载,在同一个类中,方法名相同但参数列表不同;另一种是方法重写,子类重写父类的方法。:“回答得不错。那说说 Java 的访问修饰符有哪些,分别有什么作用?:“Java 有四种访问修饰符,public 表示公共的,任何类都可以访问;

hjc_1732的博客 835

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

JAVA基础面试之JUC辅助计数器

好想去看海 196

【Java面试——JUC全局观、原子类、锁、集合类、线程池、工具类】

CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

abclyq的博客 1219

《互联网大厂面试:Java核心、JUC、JVM等知识大考验》

在互联网大厂的一间明亮面试室内,严肃的面试官正对面坐着紧张的求职者王铁牛。一场关于 Java 核心知识的面试即将展开。:首先问你几个基础问题。Java 中多态的实现方式有哪些?:多态的实现方式主要有方法重载和方法重写。方法重载是在一个类中,有多个方法名相同,但参数列表不同的情况;方法重写是子类对父类中允许访问的方法进行重新编写。:不错,回答得很准确。那说说 HashMap 的底层数据结构是什么?

hjc_1732的博客 737

Java面试大厂必问:从JUC到Redis的实战对话

(突然认真)基于动态代理,如果是接口就用JDK代理,没有接口用CGLIB!:(摸头)您好,我是范小旭,一个在互联网行业摸爬滚打的Java程序员,虽然有时候...(突然正经)不过我对技术还是很有热情的!:(慌张)哦哦哦,有Abort、CallerRuns、Discard...(突然)还有...还有...(看向窗外):(挠头)这个...它应该用到了AQS,通过state变量来控制线程等待?:(点头)最后一个问题:Redis的ZIPList和HT结构在什么场景下使用?:(起身)好的,今天就到这里。

wangpeng0722的博客 379

《互联网大厂 Java 面试:核心知识、JUC、JVM 等深度考察》

在互联网大厂的一间安静的面试室内,严肃的面试官坐在桌前,对面是略显紧张的求职者王铁牛。一场关于 Java 技术的严峻考验即将展开。:首先,我们来聊聊 Java 核心知识。Java 中多态的实现方式有哪些?:多态的实现方式主要有两种,一种是方法重载,在同一个类中,方法名相同但参数列表不同;另一种是方法重写,子类重写父类的方法。:回答得不错。那 Java 的访问修饰符有哪些,分别有什么作用?

hjc_1732的博客 927

Java 大厂面试集合(juc jvm linux)

Java 面试集合 JUC volatile 并行:同时做A,B两件事 并发:同时有很多线程访问某个资源 volatile: 是java虚拟机提供的轻量级的同步机制 保证可见性 不保证原子性 禁止指令冲排序 JMM Java内存模型:Java Memory Model,它描述的是一组规范或者规范定义了内存中各个变量。主内存是共享内存区域,线程对变量操作,会先把数据读取到自己的工作内存中,然后对变量进行操作,然后写回主内存,各个线程的工作内存中存储着主内存的变量副本拷贝,线程间通信必须通过主内存来完成。

村里的原振侠的博客 396

《互联网大厂面试:Java核心、JUC、JVM等技术大考验》

严肃的面试官坐在桌前,面前放着王铁牛的简历,表情认真。王铁牛则有些紧张地坐在对面,搓着手。

hjc_1732的博客 562

《互联网大厂 Java 面试:核心知识、JUC、JVM 等全方位考察》

王铁牛怀揣着对互联网大厂的憧憬,坐在了面试房间里,对面是表情严肃的面试官,一场激烈的技术交锋即将开始。:首先问几个 Java 核心知识的问题。Java 中基本数据类型有哪些?:这个我知道,有 byte、short、int、long、float、double、char、boolean。:回答正确,不错。那说说 Java 中面向对象的四大特性是什么?:是封装、继承、多态和抽象。:很好,基础很扎实。那在 Java 里,重载和重写的区别是什么?:重载是在一个类中,方法名相同但参数列表不同;

hjc_1732的博客 836
上一篇: Electron-跨平台桌面开发
下一篇: java面试之JVM和GC
Machine4869
博客等级 码龄10年 9粉丝 14原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值