Mushroom Notes Mushroom Notes
🍄首页
  • JavaSE

    • 基础篇
    • 数据结构
    • IO流
    • Stream流
    • 函数式接口
    • JUC
    • 反射
    • 网络编程
    • 设计模式
  • JavaEE

    • Servlet
    • JDBC
    • 会话技术
    • 过滤器监听器
    • 三层架构
  • JDK

    • 总览
  • JVM

    • 总览
  • 常用mate
  • CSS
  • JavaScript
  • rds 数据库

    • MySQL
    • MySQL 进阶
    • MySQL 库表规范
  • nosql 数据库

    • Redis
    • Redis 进阶
    • Redis 底层
    • MongoDB
  • Spring生态

    • Spring
    • Spring MVC
    • Spring boot
    • Spring Validation
  • Spring Cloud生态

    • Spring Cloud
    • 服务治理
    • 远程调用
    • 网关路由
    • 服务保护
    • 分布式事务
    • 消息中间件
  • 数据库

    • Mybatis
    • Mybatis Plus
    • Elasticsearch
    • Redisson
  • 通信

    • Netty
📚技术
  • 方案专题
  • 算法专题
  • BUG专题
  • 安装专题
  • 网安专题
  • 面试专题
  • 常用网站
  • 后端常用
  • 前端常用
  • 分类
  • 标签
  • 归档

kinoko

一位兴趣使然的热心码农
🍄首页
  • JavaSE

    • 基础篇
    • 数据结构
    • IO流
    • Stream流
    • 函数式接口
    • JUC
    • 反射
    • 网络编程
    • 设计模式
  • JavaEE

    • Servlet
    • JDBC
    • 会话技术
    • 过滤器监听器
    • 三层架构
  • JDK

    • 总览
  • JVM

    • 总览
  • 常用mate
  • CSS
  • JavaScript
  • rds 数据库

    • MySQL
    • MySQL 进阶
    • MySQL 库表规范
  • nosql 数据库

    • Redis
    • Redis 进阶
    • Redis 底层
    • MongoDB
  • Spring生态

    • Spring
    • Spring MVC
    • Spring boot
    • Spring Validation
  • Spring Cloud生态

    • Spring Cloud
    • 服务治理
    • 远程调用
    • 网关路由
    • 服务保护
    • 分布式事务
    • 消息中间件
  • 数据库

    • Mybatis
    • Mybatis Plus
    • Elasticsearch
    • Redisson
  • 通信

    • Netty
📚技术
  • 方案专题
  • 算法专题
  • BUG专题
  • 安装专题
  • 网安专题
  • 面试专题
  • 常用网站
  • 后端常用
  • 前端常用
  • 分类
  • 标签
  • 归档
  • 方案专题

  • 算法专题

  • BUG专题

  • 安装专题

  • 网安专题

  • 面试专题

    • Java 基础篇
    • JUC篇
      • 线程池的核心参数
      • 线程池的四种创建方式?
      • 线程状态
      • 线程相关的基本方法?
      • 死锁
      • 你了解Java的锁吗?
      • synchronized的底层原理
      • Sleep与wait的区别
      • Lock与synchronized的区别
      • volatile能否保证线程安全
      • synchronized 和 volatile 的区别是什么?
      • 说说悲观锁和乐观锁,有哪些常见的悲观锁和乐观锁
      • 并发下的ArrayList
      • 并发下的Hashtable与ConcurrentHashMap
      • Runnable 和 Callable 的 区别?
      • 说一说雪花算法
      • 秒杀系统架构搭建思路
        • 详情页访问
      • 如何设计一个高并发系统?
      • 为什么要分库分表?(设计高并发系统的时候,数据库层面该如何设计?)
      • 用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?
      • 你们具体是如何对数据库如何进行垂直拆分或水平拆分的?
      • 如何解决详情页高并发访问的问题?
      • 知道ThreadLocal吗?讲讲你对ThreadLocal的理解
    • JVM篇
    • SSM篇
    • Springboot篇
    • SpringCloud篇
    • MQ篇
    • MySQL篇
    • Redis篇
    • 设计模式篇
    • Elasticsearch篇
  • 专题
  • 面试专题
kinoko
2023-12-19
目录

JUC篇面试题

# 线程池的核心参数


其实就是在问线程池的一个重要实现类ThreadPoolExecutor的参数
** **
ThreadPoolExecutor的API

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 
1
2
3
4
5
6
7

image.png

救急线程什么时候创建?

新任务提交时,若核心线程正忙,任务队列满了,并且允许创建救济线程,此时才会创建。

什么时候会开始拒绝任务?

当核心线程和救急线程全都正在忙,并且任务队列满了,此时会根据拒绝策略拒绝任务。

# 线程池的四种创建方式?


面试话术
四种啊?我想想哦,我记得Executors可以点出挺多线程池的,像是newFixedThreadPool,固定核心线程数的线程池,newCachedThreadPool,缓存的线程池,这个我没用过,好像是可以没有核心线程上限的线程池,就是你来一个任务就创建一个线程,然后60s闲置线程就会被销毁,这种线程池我觉得比较适合处理执行时间小的任务。哦我记得还有个单线程的线程池,好像叫newSingleThreadExecutor来着,这个我也没用过,倒是好奇单线程为什么要用线程池来维护,然后还有个延迟队列的线程池吧,newScheduledThreadPool,呃,这个我也只是知道,但没用过,一般业务中我们好像不建议用Executors去创建队列的,这个点出来的队列都是帮你设置了一些默认参数的线程池,实际开发中一般是直接newThreadPoolExecutor,根据业务需求去设置调整线程池的参数。

# 线程状态


Java线程分成六种状态
image.png

操作系统层面分成五种状态

①分到CPU时间的:运行
②可以分到CPU时间的:就绪
③分不到CPU时间的:阻塞
image.png

**补充:**Java中的RUNNABLE涵盖了就绪、运行、阻塞I/O

# 线程相关的基本方法?


线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等

**1.线程等待(wait) **
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。

**2.线程睡眠(sleep) **
sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态.

**3.线程让步(yield) **
yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。

**4.线程中断(interrupt) **
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)

5.等待其他线程终止(Join)
join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸.

**6.线程唤醒(notify) **
Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。

# 死锁


什么是死锁?

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。
image.png

产生死锁的原因?
image.png

死锁产生的4个必要条件?
image.png

解决死锁的基本方法

预防死锁:
image.png
image.png
image.png

避免死锁:
image.png
银行家算法(Banker’s Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。

在银行中,客户申请贷款的数量是有限的,每个客户在第一次申请贷款时要声明完成该项目所需的最大资金量,在满足所有贷款要求时,客户应及时归还。银行家在客户申请的贷款数量不超过自己拥有的最大值时,都应尽量满足客户的需要。在这样的描述中,银行家就好比操作系统,资金就是资源,客户就相当于要申请资源的进程。
银行家算法是一种最有代表性的避免死锁的算法。在避免死锁方法中允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。为实现银行家算法,系统必须设置若干数据结构。
image.png
检测死锁
image.png
解除死锁:
image.png

面试话术
死锁的四个必要条件是:互斥、请求和保持、不可剥夺、环路等待,互斥就是说一个资源只能被一个线程占有,请求和保持就是指请求其他资源时,会保持自身资源,不释放,不可剥夺就是指其他线程不可强行剥夺当前线程占有的资源,环路等待就是多个线程互相等待对方保持的资源。
Java中产生死锁有三个条件:多个线程、多把锁、多个同步代码块嵌套
死锁的预防的话可以控制加锁顺序,只有当某个资源释放了才去加锁之类的,以及加锁时限,以锁为key存入redis然后设置过期时间,或者说给线程请求锁加上期限,超时就放弃,同时释放自己占有的锁。

# 你了解Java的锁吗?


面试话术
Java的锁啊,常用的就是synchronized和ReentrantLock吧,读锁和写锁没怎么用过,所以不是很了解。
synchronized是一个重量级锁嘛,是通过JVM的monitor对象监视器来实现方法的同步。我记得是有两个指令的,monitorenter加锁、monitorexit释放锁。这个monitor是依赖底层操作系统的Mutex Lock互斥锁来实现的嘛,操作系统实现线程切换需要从用户态转成内核态,转换成本比较大,所以也就是被称为重量级锁的原因。
ReentrantLock的话是基于AQS队列同步器来实现的嘛,通过维护一个volatile修饰的state属性来记录使用状态,然后通过CAS来改变这个state。ReentrantLock有公平锁和非公平锁,公平锁的上锁过程就是先判断state是否为0,0则是未占用,如果是未占用则尝试获取锁,获取锁的时候会判断队列中有没有等待线程,有的话则将当前线程加入等待队列,否则修改状态且设置当前线程为锁占用线程。ReentrantLock是支持锁重入的,重入上限是int最大值。非公平锁跟公平锁的最大区别是在加锁阶段是否需要判断队列中是否有等待线程。
释放锁的时候,就是用释放锁的次数去减这个state,如果为0了则释放锁,释放锁后会去唤醒队列中的下一个线程,如果头结点的下一个节点为空,则会从最后一个节点往前寻找第一个线程。

扩展ABA问题:CAS的话是存在一个ABA问题,解决方案就是采用一个version,就是版本号来控制变量,Java提供了AtomicStampedReference来解决。

# synchronized的底层原理


synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性、原子性、有序性。

**注:**synchronized保证的原子性是指“不可分”、拒绝多线程操作,被synchronized修饰的代码块同一时刻只允许一个线程操作。

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的class对象
  • 同步方法块,锁是括号里面的对象

# Sleep与wait的区别


  • **共同点:**wait(),wait(Long l)和sleep(long l)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态

  • 方法归属不同

    • sleep(long l)是Thread的静态方法
    • 而wait(),wait(Long l)都是Object的成员方法,每个对象都有
  • 醒来的时机不同

    • 执行sleep(long l)和wait(long l)的线程都会在等待相应毫秒后醒来
    • wait()和wait(Long l)还可以被notify()唤醒,wait()如果不唤醒就一直等下去
    • 它们都可以被打断唤醒
  • 锁特性不同

    • wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
    • wait方法执行后会释放对象锁,允许其他线程获得该对象锁
    • 而sleep如果在synchronized代码块中执行,并不会释放对象锁

# Lock与synchronized的区别


  • 语法层面

    • synchronized是关键字,源码在jvm中,用c++语言实现
    • Lock是接口,源码由JDK提供,用java语言实现
    • 使用synchronized时,退出同步代码块会自动释放,而使用Lock时,需要手动调用unlock方法释放锁
  • 功能层面

    • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
    • Lock提供了许多synchronized不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
    • Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock
  • 性能层面

    • 在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能不赖
    • 在竞争激烈时,Lock的实现通常会提供更好的性能

# volatile能否保证线程安全


线程安全要考虑三个方面

  • 可见性:一个线程对共享变量的修改,另一个线程能够看到最新的结果
  • 有序性:一个线程内代码按编写顺序执行
  • 原子性:指“不可分”、拒绝多线程操作,代码块同一时刻只允许一个线程操作成功。

volatile 能够保证共享变量的可见性与有序性,但并不能保证原子性

有序性深入

volatile有一个变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;以及以下这个案例:

首先需要知道instance = new Singleton4();这样一句代码在CPU中其实分为几个步骤:

  1. 创建对象(分配内存空间)
  2. 调用构造方法(初始化成员变量)
  3. 给静态变量赋值

然后CPU会对我们的代码执行顺序进行一个优化,对于没有因果先后关系的代码,可能会改变执行顺序,比如在这段代码中,调用构造方法和静态变量赋值就没有因果关系,这两个的执行顺序就有可能被颠倒。因此在多线程的环境下就有可能会出现以下的执行顺序:(注:橙蓝代表不同线程,return语句在if语句外,就是else的情况)
image.png
导致多线程的情况下出现创建对象还未初始化,这个对象就被返回了的问题,最终拿到的对象属性不完整,从而引发更多奇奇怪怪的错误。

而被volatile修饰的变量就会在该变量的赋值语句之后产生一个内存屏障,规定在这条语句之前的赋值语句不能越过屏障执行,从而保证了线程的有序性,防止CPU在进行创建对象分配内存空间及初始化对象,也就是调用构造方法时出现乱序。

可见性深入

volatile保证可见性本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;并且volatile标记的变量不会被编译器优化

static void foo(){
	int i = 0;
    while (!stop){
    	i++;
    }
    // 记录循环次数
    get().debug("stopped...c:{}",i);
}
1
2
3
4
5
6
7
8

这种情况下当创建一个新的线程去改变stop的值企图停止循环失败时,是因为JIT视这个循环是热点(当一段代码重复执行超过一定次数时就会被虚拟机视为热点)于是被JIT(即时编译器,将字节码文件解释成计算机语言,存在缓存中,用于优化代码在JVM中的运行**)**进行了优化,导致源码已经被修改保存在了当前线程的工作内存(缓存)里,下次直接读取的是工作内存中优化后的代码,不再读取主内存的源码了,所以就算改写了主内存中的stop值这个循环也读取不到了。

而volatile关键字修饰的变量被修改了,会令其他工作内存的副本失效,下次使用时重新从主内存中读取,也就是无视JIT的优化,使得每个线程使用这个变量时都需要从主内存中读取。

# synchronized 和 volatile 的区别是什么?


volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

# 说说悲观锁和乐观锁,有哪些常见的悲观锁和乐观锁


悲观锁的代表是synchronized和Lock锁

  • 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程都得停下来等待】
  • 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
  • 实际上、线程在获取synchronized和Lock锁时,如果锁已经被占用,都会做几次重试操作,减少阻塞机会

乐观锁的代表是Atomiclnteger,使用CAS(Compare And Set)来保证原子性

  • 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
  • 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
  • 需要多核cpu支持,且线程数不应超过cpu核数

**注:**CAS操作需要配合volatile使用以保证线程的可见性

Atomiclnteger实现乐观锁的表现如:

  • **compareAndSet()**:如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。这里需要注意的是这个方法的返回值实际上是是否成功修改,而与之前的值无关
  • **getAndIncrement()**:先获取当前值比较预期值,相等再进行自增
  • **getAndAdd()**:先获取当前值比较预期值,相等再加上指定值

代码示例

public static void main(String[] args) {
    // 先比较后修改
    boolean result = compareAndSet.compareAndSet(10,30);
    System.out.println("result:" + result);
    System.out.println("value:" + compareAndSet.get());
}

public static void main(String[] args) {
        // 先获取当前值再进行自增
        AtomicInteger getAndIncrement = new AtomicInteger(10);
        int newValue1 = getAndIncrement.getAndIncrement();
        System.out.println("newValue1:" + newValue1);
        System.out.println("value:" + getAndIncrement.get());
}

public static void main(String[] args) {
        // 先获取当前值再进行自减
        AtomicInteger getAndDecrement = new AtomicInteger(10);
        int newValue2 = getAndDecrement.getAndDecrement();
        System.out.println("newValue2:" + newValue2);
        System.out.println("value:" + getAndDecrement.get());
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

面试话术
悲观锁的话就是常规的加锁嘛,java中代表性的有synchronized啊、lock啊、然后MySQL的有表锁和行锁、以及Redis的分布式锁Redisson,然后悲观锁的话相对乐观锁来说就更加安全,对性能的消耗也更大,而且逻辑没处理好的话还有可能会出现死锁。
乐观锁的话就是不加锁,基于CAS思想对数据进行校对,java中比较有代表性的是原子类Atomic、MySQL的话就是简单粗暴的给表加上版本号字段,然后在修改数据的前后对版本号进行一个校对,实际其实就是修改前先查一次,然后在修改sql上拼一个比如version = 1,这个1就是修改前查出来的版本号,如果此时被其他线程修改了,那这个sql的where条件就会失败,就改不成数据,然后就可以写个while循环不断重试。乐观锁对性能的消耗就小很多了,但是相对的在超高并发的环境中也不安全。
Java中的一个CAS实现底层是通过volatile来保证变量的可见性,加上volatile修饰的变量,会使其工作内存中的变量值无效,多线程情况下每一个线程都会拷贝一份主内存的变量值到工作内存中,正常的话就是修改了工作内存中的值直接覆盖,而使用volatile修饰之后,就会使其工作内存中的变量失效,每一次操作都需要获取和比较主内存中的值,比如第一次拷贝主内存中的变量做一个备份,然后修改后再次获取主内存的变量与修改前的变量进行比较,若一致则删除,若不一致则重新从主内存获取最新的值,重新修改直到成功。

# 并发下的ArrayList


并发环境下的ArrayList会出现以下问题:

  1. 数据丢失
  2. 元素为null;

ArrayList的add()方法源码

private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)// 判断是否达到数组容量
            elementData = grow();// 达到则扩容
        elementData[s] = e;// 否则直接放入元素
        size = s + 1;// 元素+1(指针右移一位)
    }
1
2
3
4
5
6

数据丢失的原因是因为两个线程同时操作同一个数组索引位置,比如:线程A添加一个元素到elementData[0]的位置,于此同时,线程B也拿到了0索引进入了方法内,同样的也添加了一个元素到了elementData[0]这个位置,最终就导致线程A添加的数据被线程B覆盖了,从而导致了数据丢失

元素为null是建立在数据丢失的情况下,两个线程同时操作一个位置的数据,但是最终都让size进行了自增,于是就导致只添加了一个元素进入集合,但是实际上指针却移动了两位,即从0移动到了2索引,elementData[1]就被跳过了,于是遍历时就会出现null的情况。

以此也证明了ArrayList是线程不安全的。

线程安全的List集合有:

  • **Vector:**通过同步锁锁住读写方法达到线程安全,由于效率太低,被淘汰
  • **Collections.synchronizedList(List< T> list):**通过同步代码块锁住调用读写方法的代码块,同样是效率太低
  • **CopyOnWriteArrayList:**在面临写操作的时候,CopyOnWriteArrayList会先复制原来的数组并且在新数组上进行修改,最后再将原数组覆盖。如果写操作的过程中发生了线程切换,并且切换到读线程,因为此时数组并未发生覆盖,读操作读取的还是原数组。换句话说,就是读操作和写操作位于不同的数组上,因此它们不会发生安全问题。

CopyOnWriteArrayList的add()方法

public boolean add(E e) {
		//获取锁
        final ReentrantLock lock = this.lock;
        //加锁
        lock.lock();
        try {
        	//获取到当前List集合保存数据的数组
            Object[] elements = getArray();
            //获取该数组的长度(这是一个伏笔,同时len也是新数组的最后一个元素的索引值)
            int len = elements.length;
            //将当前数组拷贝一份的同时,让其长度加1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //将加入的元素放在新数组最后一位
            newElements[len] = e;
            //替换引用,将数组的引用指向给新数组的地址
            setArray(newElements);
            return true;
        } finally {
        	//释放锁
            lock.unlock();
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

由此可见,CopyOnWriteArrayList加入了一个互斥锁保证了写操作的同步,并且获取操作索引的代码也在同步代码块内,因此可以避免上述的两种情况。

CopyOnWriteArrayList的get()方法

public E get(int index) {
        return get(getArray(), index);
    }
1
2
3

与Vector和Collections.synchronizedList相比,CopyOnWriteArrayList没有将读方法加上锁,并且使用了fail-safe的数据保护机制,牺牲一致性来保证遍历的运行。而前两者使用的都fail-fast数据保护机制,遍历时一旦发现有其他线程或其他非当前迭代器对象修改数据,则会立即抛出异常。CopyOnWriteArrayList由此做到读写分离,所以效率要优于前两者。

优点:

  1. 线程安全。
  2. 读写分离,提高性能。

缺点

  1. 复制副本,内存占用较多,可能频繁触发gc
  2. 数据时效性差,读线程读取时,写线程修改集合,读线程读取的数据失效

# 并发下的Hashtable与ConcurrentHashMap


  1. Hashtable 与 CoucurrentHashMap 都是线程安全的Map集合
  2. Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
  3. 1.8之前 ConcurrentHashMap 使用了 Segment数组 + 数组 + 链表的结构,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
  4. 1.8开始ConcurrentHashMap将数组的每个头结点作为锁,如果多个线程访问的头结点不同,则不会冲突

Hashtable的结构是数组+链表
image.png
1.7ConcurrentHashMap的结构是Segment数组+数组+链表的结构
image.png

  • ConcurrentHashMap有三个参数(容量,扩容因子,并发度)
  • Segment数组下标计算取二次hash的高四位,小数组的下标计算取二次hash的最低位
  • Segment[0]是Segment数组下的小数组的原型,决定小数组被创建时的默认容量
  • 小数组容量计算公式(容量/并发度)扩容阈值(元素个数大于容量x扩容因子时)
  • 1.7ConcurrentHashMap属于饿汉式单例,在创建对象时就会开辟内存空间。

1.8后变成数组+链表 | 红黑树的结构
image.png

  • 1.8ConcurrentHashMap属于懒汉式单例,在调用put方法时才会开辟内存空间。
  • 1.8ConcurrentHashMap扩容时是从数组尾部开始检查的,一个节点一个节点的进行迁移,迁移完的节点会有标记,当扩容时线程进行了get方法,对于已迁移完的节点,就需要去迁移后的新数组中找,而这里就会牵扯到部分链表对象重新创建,防止地址指向出现问题;当扩容时线程调用了put方法,若是put在未迁移的位置,则可以并发执行,若是put在正在迁移的位置则会阻塞,要等待数据迁移结束,若是put已经迁移完的位置则要帮忙进行迁移,等数据全部迁移到新数组后才能put。

# Runnable 和 Callable 的 区别?


面试话术
首先最大的区别是Runnable没有返回值,Callable有返回值,然后Callable的run方法允许向上抛出异常,Runnable只能在内部try catch,呃,然后Runnable可以作为Thread构造器参数通过Thread.start方法来开启线程,也可以通过线程池来提交线程任务开启,Callable只能通过线程池执行线程任务。

# 说一说雪花算法


雪花算法是Twitter公司提出的一个分布式id生成算法,绝对唯一的id。

雪花算法生成的id是64位的二进制数据,最后再转成十进制,主要分为四个部分
image.png

  • 1bit:固定为0。是二进制中的最高位符号位,1表示负数,0表示正数。生成的id一般是用正数,所以最高位固定为0。
  • 41bit:毫秒级的时间戳。是生成出来的id自增的原因之一。
  • 10bit:工作机器id。每台电脑都有一个机器号的,并且机器号是唯一的。
  • 12bit:序列号。用来记录同毫秒内产生的不同id。

同一台机器,同一毫秒内,这两个条件达成,前52bit就相同了,然后在这一毫秒内又可以在0~4095范围内生成随机数,所以可以断定id绝对不可能重复

基于这个算法就可以生成一个固定长度的,自增的,绝对唯一的id
**特点:**定长,全是数字,连续增长,存储是连续的,查询速度快

详细资料:雪花算法(SnowFlake)Java实现.pdf (opens new window)

# 接口幂等性怎么保证?


面试话术
基本思路都是插入去重表,用MySQL也可以实现,但是不适用高并发的场景,所以推荐使用Redis,由于存放的一般是token,所以会比较占用内存, 所以应该根据业务场景设置适当的过期时间。利用Redis的分布式锁,setnx指令,将token存入,如果能存入则说明是第一次请求,则执行业务,否则直接返回成功。

# 秒杀系统架构搭建思路


# 详情页访问

核心思想就是尽可能的减少访问mysql的次数

  1. 首先是最基本的三层架构,用户通过浏览器发送请求访问后端服务器,服务器调用接口访问mysql查询数据返回。
  2. 这里高并发下mysql肯定承受不住压力,所以需要加入Redis作为缓存缓解mysql压力,请求先从Redis中查询数据,若Redis中有则返回前端,没有则访问mysql。
  3. 但是查询Redis的话怎么都还是会有网络请求,避免不了一些网络IO以及带宽的一些浪费,这里就可以使用多级缓存,在活动开始前就先将热点数据放到JVM内存中,以及Redis也可以做一个缓冲预热,然后这个时候就直接走的JVM内存没有网络IO这些,会相对更快一些。由于JVM的内存资源比较珍贵,所以尽可能的只放热点数据吧,否则可能会使JVM内存中产生脏数据,影响性能,当JVM内存中没有的时候才去查Redis,Redis没有才查数据库。
  4. 但是即使是JVM并发量也是有限的,那假设我一台机器可以扛1w并发,那我就搞十台机器来扛10w并发,所以可以扩展服务器节点,搭建集群,通过Nginx反向代理用一些轮询算法或者是加权轮询算法做负载均衡。
  5. 想要让系统越快,就得将缓存放在离用户越近的地方,由于现在浏览器最优先访问的是Nginx,所以可以考虑让Nginx直接去访问Redis,这样就不需要将请求打到后端服务器上了
  6. 并且防止Redis宕机造成缓存雪崩,Redis也可以建立集群,来提高服务的稳定性

# 如何设计一个高并发系统?

为什么要问这个问题

真正干过高并发的知道,脱离了业务的系统架构都是在纸上谈兵,因为高并发系统一天流量几十亿,那么一定会仔细盘系统架构:怎么部署?部署了多少台机器?缓存咋用的?MQ咋用的?数据库咋用的?深挖到底如何抗下高并发的。所以如果有面试官问你个问题说,如何设计一个高并发系统?其实是面试官看出你实际上没干过高并发系统,简历就没啥出彩的,感觉就不咋地,就会问问你如何设计一个高并发系统?其实说白了本质就是看看你有没有自己研究过,有没有一定的知识积累。最好的当然是招聘个真正干过高并发的哥儿们咯,但是这种哥儿们人数稀缺,不好招。所以可能次一点的就是招一个自己研究过的哥儿们,总比招一个傻也不会的哥儿们好吧!

所以这个时候你必须得做一把个人秀了,秀出你所有关于高并发的知识!其实大部分公司,真正看重的,不是说你掌握高并发相关的一些基本的架构知识,架构中的一些技术,RocketMQ、Kafka、Redis、Elasticsearch,高并发这一块,次一等的人才。对一个有几十万行代码的复杂的分布式系统,一步一步架构、设计以及实践过高并发架构的人,这个经验是难能可贵的。

为啥会有高并发?为啥高并发就很牛逼?

所以很多公司刚开始技术比较low,刚开始系统都是连接数据库的,要知道数据库QPS只有2-3千,结果业务发展太快,当数据库瞬间承载每秒5000,8000,甚至上万的并发,系统扛不住宕机。

现在很多app、网站、系统承载的都是高并发请求,可能高峰期每秒并发量几千,很正常的。如果是什么双十一了之类的,每秒并发几万几十万都有可能。

高并发系统的架构组成

image.png
PS:高并发这块真正厉害的,不是弄明白一些技术或大概知道高并发系统应该长什么样?实际上真正的复杂的高并发业务系统远比这个图复杂几十倍到上百倍。我们需要考虑哪些需要分库分表?哪些不需要分库分表?单库单表跟分库分表如何join?哪些数据要放到缓存里去啊?放哪些数据再可以抗掉高并发的请求?需要完成对一个复杂业务系统的分析之后,然后逐步逐步的加入高并发的系统架构的改造,这个过程是务必复杂的,一旦做过一次,一旦做好了,你在这个市场上就会非常的吃香。

如何设计一个高并发系统

如果让我设计一个高并发系统,我会考虑一下几个方面,(每个部分要注意哪些问题,都可以阐述阐述)
(1)系统拆分,将一个系统拆分为多个子系统,每个系统连一个数据库,降低单机mysql的压力。用dubbo来搞,这样本来就一个库,现在多个数据库,不也可以抗高并发么。

(2)加缓存解决大量的并发读请求,在数据库和缓存里都写一份,读的时候大量走缓存。大部分的高并发场景都是读多写少,而redis轻轻松松单机几万的并发。主要考虑哪些需要承载读场景,怎么用缓存来抗高并发。

(3)加入MQ解决高并发写的场景,就算有大量的写请求那就灌入MQ里排队,后边系统消费后慢慢写,控制在mysql承载范围之内。打个比方,一个业务操作里要频繁搞数据库几十次,增删改增删改。那高并发绝对搞挂系统,如果用redis来承载写那不行,人家是缓存,数据随时就被LRU了,数据格式还无比简单,没有事务支持。所以该用mysql还得用mysql啊。所以加入MQ排队咯,需要考虑承载复杂写业务逻辑的场景里,如何用MQ来异步写,提升并发性。MQ单机抗几万并发。

(4)分库分表,将一个数据库拆分为多个库,多个库来抗更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高sql跑的性能。但是主要是到了后面数据库层面还是免不了抗高并发的要求才做。

(5)读写分离,做一个主从架构保证主库写入从库读取。读流量太多的时候,还可以加更多的从库。这个就是说大部分时候数据库可能也是读多写少

(6)可以考虑用分布式Elasticsearch, 一些比较简单的查询、统计类的操作,可以考虑用es来承载,还有一些全文搜索类的操作,也可以考虑用es来承载。es是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来抗更高的并发。

# 为什么要分库分表?(设计高并发系统的时候,数据库层面该如何设计?)

扯到高并发,分库分表一定是为了支撑高并发、数据量大两个问题的
注意:分库分表是两回事儿,可能光分库不分表也可能是光分表不分库。

单库单表随着业务发展的演变

image.png

实际上这是跟着公司业务发展走的,业务发展越好,用户就越多,数据量越大,请求量越大,单个数据库一定扛不住。
时间 注册用户 每天活跃用户 每天单表数据量 高峰期QPS 程序员的压力
刚兴起部门 20万 1万 1000 10 无压力,随便搞
过了几个月 2000万 100万 10万条 1000 线上部署了几台机器,负载均衡搞了一下,数据库撑1000 QPS勉强还能撑着
又过了几个月 1亿 上千万 50万 5000~8000 表总数据量都已经达到了两三千万了,系统支撑不到现在,已经挂掉了
单表危害 数据量太大,会极大影响你的sql执行的性能,到了后面你的sql可能就跑的很慢了。
一般来说,单表到几百万的时候,性能就会相对差一些了,就得分表了。
分表 将一个表的数据放到多个表中,查询的时候你就查一个表。
比如按照用户id来分表,将一个用户的数据就放在一个表中。操作时你对一个用户就操作那个表就好了。
这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在200万以内。
分库 一个库一般最多支撑到并发2000,一定要扩容了。
一个健康的单库并发值最好保持在每秒1000左右,不要太大。
可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

# 用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?

考点:

了解哪些分库分表的中间件?各个中间件的优缺点是啥?用过哪些分库分表的中间件?

常见的分库分表中间件比对

基本上中间件可以做到你分库分表之后根据你指定的某个字段值,比如说userid,自动路由到对应的库上去,然后再自动路由到对应的表里去。

组件名 出品方 优缺点
cobar 阿里b2b团队开发和开源的proxy层方案 缺点 :没人维护,不支持读写分离、存储过程、跨库join和分页
TDDL 淘宝团队
client层方案
优点:
可以实现基本的crud语法
支持读写分离
缺点:
不支持join多表查询等语法,
依赖淘宝的diamond配置管理系统
atlas 360开源
proxy层方案
没人维护
sharding-jdbc 当当开源的
client层方案
优点:
这种client层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高。
SQL语法支持比较多,支持分库分表、读写分离、分布式id生成、柔性事务(最大努力送达型事务、TCC事务)
缺点:
遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合sharding-jdbc的依赖;
mycat 基于cobar改造
proxy层方案
优点:
功能完善,对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。
缺点:需要部署,自己及运维一套中间件,运维成本高
考虑使用sharding-jdbc和mycat
中小型公司选用sharding-jdbc,client层方案轻便,而且维护成本低,不需要额外增派人手,而且中小型公司系统复杂度会低一些,项目也没那么多;中大型公司最好还是选用mycat这类proxy层方案,因为可能大公司系统和项目非常多,团队很大,人员充足,那么最好是专门弄个人来研究和维护mycat,然后大量项目直接透明使用即可;也可以自研的。

# 你们具体是如何对数据库如何进行垂直拆分或水平拆分的?

image.png

含义 作用
水平拆分 把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。 将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。
垂直拆分 把一个有很多字段的表给拆分成多个表,或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。访问频率高的字段放到一个表里,访问频率低的字段放到另外一个表里去。 数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。
项目里该如何分库分表 垂直拆分,在表层面做,对一些字段特别多的表做一下拆分;
水平拆分,并发承载不了或者是数据量太大容量承载不了;
分表哪怕是拆到每个库里去,并发和容量都ok了,但是每个库的表还是太大了,就害得分表直到每个表的数据量并不是很大。
两种分库分表的方式 1)range来分
定义:每个库一段连续的数据,一般是按比如时间范围来的,一般较少用,
好处:后面扩容容易,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用range,要看场景,你的用户不是仅仅访问最新的数据,而是均匀的访问现在的数据以及历史的数据
坏处:很容易产生热点问题,大量的流量都打在最新的数据上了;
2)hash分法
定义:按照某个字段hash一下均匀分散,这个较为常用
好处:平均分配没给库的数据量和请求压力;
坏处:扩容起来比较麻烦,会有一个数据迁移的这么一个过程

# 如何解决详情页高并发访问的问题?


场景:文章详情页,商品详情页,秒杀页面

面试话术
首先是可以将几乎不会变化的数据做成静态页面,然后存到文件存储系统中,我之前参与的资讯项目就用的freemarker来做页面静态化,然后使用minio做存储,以资讯文章为例吧,在发布文章的时候就可以生成这个静态页面然后存储到minio,用户访问的时候直接访问minio中的页面地址。然后的话,一些热点数据,比如文章的点赞数啊,啊不过点赞可能没那么重要,商品的库存这种可以存到Redis,这种数据就可以单独写个Ajax去请求Redis。
其他还有能做的话,搭建集群吧,然后对热点数据做多级缓存之类的。

# 知道ThreadLocal吗?讲讲你对ThreadLocal的理解


面试话术
知道,我们项目中就用到了这个,ThreadLocal在我们项目中的作用是存储用户信息,我们项目用了无状态认证嘛,然后我们会在网关里对用户访问携带的token做一个解密,将其中的用户信息提取出来存到ThreadLocal里面,选择使用ThreadLocal的原因是它能够保证线程间的一个数据安全问题。ThreadLocal底层本质是一个Map结构,他会以当前线程对象为key来进行一个存储,获取的时候也是通过当前线程对象去获取的,我们都知道不同的请求是不同的线程,所以这就保证了请求间,也就是线程间的一个共享数据的安全。啊不过用这个的时候有点要注意一下,就是他可能会造成内存泄漏,就是可能会OOM,因为我们一般会将ThreadLocal抽成一个工具类来用嘛,会创建一个static final 修饰的ThreadLocal对象,所以所有请求线程都会在这个对象的map结构中进行存储,如果请求在执行结束后没有移除其在map中的数据的话就会越攒越多直到OOM。所以要记得在请求执行结束后进行一个key的移除。

#java#juc#面试
上次更新: 2023/12/29 11:32:56
Java 基础篇
JVM篇

← Java 基础篇 JVM篇→

最近更新
01
JVM 底层
09-13
02
JVM 理论
09-13
03
JVM 应用
09-13
更多文章>
Theme by Vdoing | Copyright © 2022-2024 kinoko | MIT License | 粤ICP备2024165634号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式