大厂面试都在问的高并发问题汇总,附代码案例【精】

📄23届内推方式

内推链接:https://talent.alibaba.com/campus/qrcode/home?code=I9hkqELKqqeEeOzMLOL1_ja4LkB6ShZoLpwNJq0_QrA%3D
内推亮点:可以及时跟进流程,解答大家的任何问题。

在 java 中守护线程和本地线程区别?

任何的线程都可以设置为守护线程或者用户线程,通过thread.setDaemon就可以设置。这个设置必须要在thread.satrt之前设置,否则就会报错。

可以简单的理解为守护线程是jvm创建的(但这不绝对),用户线程是应用程序创建的。比如jvm的垃圾回收线程就是守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是 Java 虚拟机上仅剩的线程时,Java 虚拟机会自动离开。。Thread Dump 打印出来的线程信息,含有 daemon 字样的线程即为守护进程,可能会有:服务守护进程,编译守护进程,GC守护进程等等

线程与进程的区别?协程

进程是资源分配的最小单位,线程是操作系统调度的最小单位

一个进程至少有 一个进程,同一进程内的线程之间的资源共享

协程(用户态):

  • 因为函数(子程序)不是线程切换,而是由程序自身控制的,因此没有线程切换的开销;
  • 和多线程比,线程数量越多,协程的性能优势越明显

什么是多线程中的上下文切换?

多线程共同使用计算机上的一组CPU,当线程数量大于CPU数量的时候,为了让所有的线程都有机会执行,需要轮转切换cpu,当不同线程使用CPU时发生的数据切换就叫做上下文切换

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

死锁:多个线程因为互斥的抢占一个资源而互相等待,如果没有借助外力就不会 放弃

死锁产生的必要条件:

互斥:某一资源在同一时间内只能被一个线占有

循环等待:形成了一种首尾相连的环

请求并保持:不会放弃资源等待

不被剥夺条件:已经占有资源的线程,在未使用完前不能被剥夺

活锁:任务或者事件由于没有成功,会失败,重试,而不是不是一直等待下去,活锁可以自行解开资源 等待,就是破坏了死锁的一些必要条件

饥饿: 线程由于一直无法满足执行条件,导致一直无法执行,造成了饥饿。处于一直无法执行的状态、

java中导致饥饿的原因:

一直被高优先级的线程抢占资源,自身无法获取资源

线程等待一个自身处于等待完成的对象(比如调用这个对象的wait方法)

解决办法:选取比较合适的线程调度算法

什么是线程组,为什么在 Java 中不推荐使用?

线程组是ThreadGroup类,它在里面既可以有线程对象,也会有线程组对象,有点类似于树状结构 ,不用是因为有安全问题

为什么使用 Executor 框架?

传统的new thread方式创建销毁线程的开销很大,而且随着new thread 被无限制创建,会导致由于系统资源被耗尽而瘫痪的问题。这些线程缺乏管理,线程之前的交互也会销号较多的资源。Excutor Service较new thread有更好的服务支持,对开发者很友好,比如定时定期执行,中断

在 Java 中 Executor 和 Executors 的区别?

Executors 工具类可以根据我们的需求创建线程池满足业务需求

Executor 接口方法可以执行线程任务

ExcutorService继承了Excutor接口并且丰富了功能,通过这个服务我们可以管理线程,获取线程的执行状态或者返回值

例如 ThreadPoolExutor可以创建自定义线程池

Feture 表示异步计算结果方法,它可以清楚计算执行的状态和结果的返回,通过get方法获取计算的结果,通过then来完成后续需要这个结果的操作,是一个相当不错的在平时业务开发中会用到的特性。

什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?

原子操作是java中一个不被中断,不受其他线程操作影响的任务单元,CAS原语就是Compare&Swap

在atomic包下提供了很多一些原子操作类对int或者long都提供了原子操作类

还有针对一些结合也提供了原子操作 类。当线程调用这些原子对象的方法的时候,这些操作是排他的,不会被打断,当其他线程就会像自旋锁一样,等待这个对象执行完成,并且加入任务队列中,当前方法执行完成后,jvm会从队列中取出一个任务继续执行

缺陷:

ABA问题的原子操作类:AtomicMarkbleReference(通过加入一个boolean值来判断中间是否发生过变化)

AtomicStampedReference(通过加入一个int类型变量来判断中间是否发生过变化)

cpu的空转

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

Lock接口较synchronized提供了更多丰富的功能,同步只支持非公平锁

而lock支持公平锁,轮询锁(try lock),无条件锁,定时锁,可中断锁

什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?

阻塞队列是一个支持两个附加操作的队列

支持当线程从队列里获取对象的时候,如果为空则阻塞。如果线程往队列里面丢数据的时候,如果阻塞队列满了就不可以 操作

java里提供了多种阻塞队列数据结构:

ArrayBlockingQueue: 阻塞队列数组

LinkedBlockingQueue

SynchronizedQueue

priorityBlockingQueue

在java 5版本之前为了实现生产者消费者。生产者模型,可以使用一个普通的集合,通过多个线程协作和同步实现目的,。在java5之后使用生产者,消费者模式通过阻塞队列就可以轻易实现

一个典型的应用场景就是,在Soket客户端的数据读取中,读取线程往阻塞队列生产数据,解析线程从阻塞队列消费数据

什么是 Callable 和 Future?

Callable和Runnable 类似,从名字就可以看出,不同的是,Runnable被线程执行后不会返回结果,并且不会抛出返回结果的异常。Callable可以返回线程执行的结果,这个返回的结果可以被Future通过get()方法获取。

准确点说Future可以获取异步任务的返回值。总体来说是Callable用于产生结果,而Future用于接收结果

什么是 FutureTask?

FutureTask是指一个异步可取消计算的任务。它提供了启动,取消计算,获取计算结果。如果计算还没有结束,调用get方***一直阻塞,直至结果返回,FutureTask对象可以对调用了Callbale和Runnable的对象进行封装。由于FutureTask也调用Runnable接口,所以可以交给ExcutorService执行

什么是并发容器的实现?

与并发容器相对的是同步容器。

同步容器在多线程的场景下他们会串行的执行 ,他们的线程安全版本就是在方法中加入syschronized关键字。java中的同步容器比较有代表性的是Vector,hashTable,同步容器是对整个容器对象加锁,所以其吞吐率并不高。

并发容器使用与同步容器不同的加锁策略来控制并发访问,例如在ConcurrentHashMap中使用了更加细粒度的分段锁,当有线程对对象方法进行调用时,通过只对部分加锁,来实现多线程并发地对容器进行读取和写入操作 ,提高吞吐量。

多线程同步和互斥有几种实现方法,都是什么?

线程同步指的是多个线程之间一种制约关系,一个线程地执行依赖另外一个线程地结果,当还没取得结果时线程应该处于阻塞状态。线程互斥指的多个线程访问同一进程内的共享资源时,当资源被一个线程获取了,其他要使用该线程的资源就必须等待。可以理解为互斥时一种特殊的同步。

线程同步方法大致分为两类:用户态和内核态。内核态指使用系统内核的单一性来进行同步,需要由用户态到内核态的切换。用户模式就就不需要

线程的同步方法大致分为:

用户态下的同步方法有:临界区,原子操作 内核态:信号量,管程,事件

什么是竞争条件?你怎样发现和解决竞争?

当多个进程或者线程对共享资源进行某种处理,但最后的结果又取决于线程执行顺序的时候,这时候我们就觉得这个发生了竞争

//    创建2个线程的线程池 
Executor executor =    Executors.newFixedThreadPool(2);
while(存在未对账订单){        
    //    计数器初始化为2        
    CountDownLatch latch = new CountDownLatch(2);        
    //    查询未对账订单        
    executor.execute(()->{
        pos = getPOrders();                
        latch.countDown();        
    });        
    //    查询派送单        
    executor.execute(()->{
        dos = getDOrders();                
        latch.countDown();        
    });                
    //    等待两个查询操作结束        
    latch.await();                
    //    执⾏对账操作        
    diff = check(pos, dos);        
    //    差异写⼊差异库        
    save(diff); 
}

你将如何使用 thread dump?你将如何分析 Thread dump?

runnable 就绪

blcoking 阻塞

running 运行

New 新建

在 Java 中 CycliBarriar 和 CountDownLatch有什么区别?

CountDownLatch 用游戏里的一句话就叫别浪等人到齐了再团,countDownLatch在初始化的时候会设置一个int值,在调用await的时候这个方***一直阻塞,直到变量变为0的时候,会释放所有的线程继续往下执行

CycliBarriar 是多个线程互相等待,直到所有的线程到达屏障后会一起执行。等待线程都释放后,这个barrier又可以重用

而信号量适用于有固定的资源,需要已经占有资源的线程释放了才可以用用。

cyclicBarrier

{

    // 创建 CyclicBarrier 实例,计数器的值设置为2
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        int breakCount = 0;

        // 将线程提交到线程池
        executorService.submit(() -> {
            try {
                 System.out.println(Thread.currentThread() + "第一回合");
                Thread.sleep(1000);
                cyclicBarrier.await();

                 System.out.println(Thread.currentThread() + "第二回合");
                Thread.sleep(2000);
                cyclicBarrier.await();

                 System.out.println(Thread.currentThread() + "第三回合");
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        });

        executorService.submit(() -> {
            try {
                 System.out.println(Thread.currentThread() + "第一回合");
                Thread.sleep(2000);
                cyclicBarrier.await();

                 System.out.println(Thread.currentThread() + "第二回合");
                Thread.sleep(1000);
                cyclicBarrier.await();

                 System.out.println(Thread.currentThread() + "第三回合");
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        });

        executorService.shutdown();
    }

}
Thread[pool-1-thread-1,5,main]第一回合
Thread[pool-1-thread-2,5,main]第一回合
Thread[pool-1-thread-1,5,main]第二回合
Thread[pool-1-thread-2,5,main]第二回合
Thread[pool-1-thread-1,5,main]第三回合
Thread[pool-1-thread-2,5,main]第三回合

countDownLatch

  final CountDownLatch latch = new CountDownLatch(2);

  new Thread() {
      public void run() {
          System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");
          Thread.sleep(3000);
          System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");
          latch.countDown();
      }
  }.start();

  new Thread() {
      public void run() {
          System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");
          Thread.sleep(3000);
          System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");
          latch.countDown();
      }
  }.start();

  System.out.println("等待 2 个子线程执行完毕...");
  latch.await();
  System.out.println("2 个子线程已经执行完毕");
  System.out.println("继续执行主线程");
等待 2 个子线程执行完毕...
子线程Thread-0正在执行
子线程Thread-1正在执行
子线程Thread-0执行完毕
子线程Thread-1执行完毕
2 个子线程已经执行完毕
继续执行主线程

什么是不可变对象,它对写并发应用有什么帮助?

一个对象一旦被创建后就不能被改变,包括对象的数据也即对象的属性不能随意改变

Java中提供的String,基本类型的包装类,BigInteger,BigDecimal

他们是线程安全,属性的设置是在构造函数中完成的

Java 中用到的线程调度算法是什么?

计算机中的cpu在同一时刻只能执行一条机器指令,线程只有获得CPU得使用权才能进入运行状态。线程调度算法目的就是让在运行池中的多个线程轮流获得cpu得使用权执行各自得任务。java虚拟机得一项任务,就是根据特性得调度机制,为线程分配cpu使用权。

cpu调度算法大致分为:分时调度或者抢占式调度

分时调度:大概就是cpu轮询的为每个就绪的线程分配cpu时间片,平均分配每个cpu的使用时间

抢占式调度:让运行池中的线程具有高优先级的线程先获得cpu的使用权,低优先级的后获取

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

悲观锁:每次进行拿数据的时候,总以为会修改,所以每次在拿数据的时候要先拿到锁,其他的线程就必须等待直到获得这个锁才可以访问,早期的数据库的很多操作都是基于乐观锁和悲观锁实现的,例如:表锁、行锁、读锁、写锁。synchronzied关键字就是通过悲观锁实现的

乐观锁:乐观锁是在每次进行修改的时候,拿数据的时候不觉得会修改所以不会上锁,但是在更新的时候会检查这期间有没有人更新数据,通常的做法是加个版本号来解决,这样可以提高吞吐量。在java atomic包下面的原子类就是通过乐观锁的CAS的方式实现的

乐观锁的实现方式:

1.提供一个版本号来判断查到的数据和修改的数据是否一致,如果不一致提供失败或者重试的机制

2.java中的CAS,当java中多个线程尝试修改CAS变得时候的时候,只有一个线程能够线程,其他的线程并不是因此而终止,而是被告知在这次竞争中失败,可以再次尝试。在CAS中有三个值,要修改的内存位置,预期比较值,拟写新值,会比较前面说的两个,如果不一样就可以更新

CAS缺点:

针对ABA的问题,但是JDK后面引入了两个类 一个是AtomicRemarkableReference解决和AtomicStampedReference解决

循环开销大

不支持对多个变量执行操作

CopyOnWriteArrayList 可以用于什么应用场景

最终一致性,读写分离

CopyOnWriteArrayList是无锁容器,当容器进行修改操作的时候,会复制一份副本,读取线程可以安全的操作。所以不会出现ConcureentModificationException并发修改异常的问题

这个将会导致下面的问题:

  1. 由于读取和写入在 不同的对象上 进行,需要拷贝数组,会导致占用内存,如果内存占用过高将会导致频繁发生,会影响性能

  2. 不具备实时性,调用set之后数据可能还是旧的,能做到最终一致性

CopyOnwriteArrayList的一些思想:

  1. 读写分离

  2. 最终一致性

  3. 开辟新的空间解决并发修改问题

什么叫线程安全?servlet 是线程安全吗?

多线程安全指的是,多个线程调用函数或者函数包,多线程之间能够正确处理他们的共享变量,使程序能正确的运行

java中 原子性,内存可见性,禁止指令重排

severlet是单实例多线程的,当多个线程访问同一个方法的时候,是不保证线程安全性的

volatile 有什么用?能否用一句话说明下 volatile 的应用场景?

volatile保证多线程下的内存可见性和禁止指令重排

volatile用于多线程环境下的单次读或者单次写操作

为什么代码会重排序

在程序 执行期间,编译器和处理器为了性能会对指令进行重排,但是排序不是任何重排的需要满足以下条件:

1.单线程执行不影响最终结果的正确性

2.存在数据依赖的不允许指令重排

重排可能会影响多程序执行的语义

为什么 wait, notify 和 notifyAll 这些方法不在 thread类里面?

很显然他们是对象级的锁,每个对象都有锁,wait和notify以及notifyall都是对象及的锁

什么是 ThreadLocal 变量

每个线程都有一个ThreadLocal变量,也就说每个线程都有自己的一个独立变量,线程竞争不安全就完全消除了。它为创建代价高昂对象提供了线程安全的方法,例如说可以用ThreadLocal让SimpelDataFormat变成线程安全的,因为那个对象创建现需要创建多个实例,代价高昂所以不值得在局部范围去使用他,如果为每个变量,将大大提高效率。通过复用减少代价高昂对象的创建个数,其次你在不使用代价高昂的同步的情况下就实现了线程安全

什么是线程池? 为什么要使用它?

创建线程要消耗大量的资源,如果一个任务来了才创建线程,那么这个任务的相应时间将会变得很长,而且进程能创建的线程十分有限。创建线程池的主要目的就是资源复用和管理。线程池就是在进程启动的时候,创建若干线程来处理响应,里面的线程又叫工作线程,

Java 中的 ReadWriteLock 是什么?

读写锁是用来提升并发程序性能的锁分离技术的成果。读支持并行,只要有写还是会互斥

copyon writelist 就是

volatile 变量和 atomic 变量有什么不同

前者不具备原子性 后者具备原子性

你如何确保 main()方法所在的线程是 Java 程序最后结束的线程?

通过Thread.join()方法确保所有创建的线程在main方法退出前结束

线程之间是如何通信的?

volatile 通过内存一致性才控制 :共享变量

中断

join

对象锁:notiy wait notifyall

管道

信号量

如何确保线程安全?

使用同步

原子类

并发容器

使用线程安全类

使用不变类

阻塞队列的底层原理,手动实现一个

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

public class MyBlockingQueue {
    /**
     * 容量
    **/
    int capacity;
    /**
     * 队列
    **/
    List<String> list;


    public MyBlockingQueue(int capacity){
        this.capacity = capacity;
        this.list = new LinkedList<>();
    }

    public synchronized void put(String s){
        while(list.size()>=capacity){
            try {
                this.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }

        }
        list.add(s);
        this.notifyAll();
    }

    public synchronized String take(){
        while(list.size()<=0){
            try{
                this.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        this.notifyAll();
        return list.remove(0);
    }

}

volatile关键字

volatile是一种轻量级在有限资源条件下保障线程安全的技术,它保证了修饰变量的内存可见性和有序性,但非原子性。当对于syschronized更加高效,常常和synchronized混用。使用场景是在不执行非原子操作的场景中。

java内存模型.png

线程更新变量过程:

  1. 主存存储线程需要操作的变量,但是线程不直接操作主存
  2. 线程需要操作变量时都是先从主存中拷贝一份到线程工作内存中
  3. 线程操作完之后再写回主存中

volatile通过在使用时必须执行读取和加载内存,工作内存操作完之后必须同步到主存中,由于禁止了指令重排,保证了操作的连续性。所以这个流程看上去是连续的,保证了内存的及时更新。

ReentrantLock和synchronize的区别

https://juejin.cn/post/6844903695298068487

联系,两个都是通过获得锁还进行同步,syschronized是隐式锁,线程不能操控锁。但是rentrantlock是显示锁,提供了获取锁,尝试获取锁 ,中断,释放,条件监控变量,控制当前lock线程的wait和notify,例如阻塞队列LinkedBlockingQueue就是实现原理 ,也很大成都上依托了reentrantlock的这些特性。

api层面;syschronized可以直接作用于代码块,reentrantlock需要通过lock 和finally unlock来控制锁的获取与释放。同步支持锁住代码块和方法,但是reentrantlock只支持锁住代码块。

中断方面;reentrantlock 支持中断,如果等待了很长时间任然获取不到,可以中断等待去做其他的事情,但是syschronized不会

公平性: 就是按照线程的等待顺序获取锁这个叫公平的,syschronized只支持非公平锁,但是reentrantlock 即可实现公平的也可以实现非公平的。

锁定条件: syschronzied中通过wait和notufy以及notufyall可以实现一个隐含的条件。reentrantlock可以通过newCondition绑定多个锁定条件 ,进行多个条件之间的交互。

自旋锁、读写锁、互斥锁的区别

互斥锁: 互斥锁加锁之后,线程会释放cpu,给其他线程。获取线程资源是由cpu控制的,而不是用户态,内核自动去控制唤醒

自旋锁: 自旋锁加锁失败之后,线程会忙等待,直到它拿到锁,是在用户态实现的,由cas实现,如果确定等待的时间很短可以用,没有cpu开销

读写锁:

读写锁从字面意思我们也可以知道,它由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。

公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。

sychronized的自旋锁、偏向锁、轻量级锁、重量级锁,分别介绍和联系

https://juejin.cn/post/6937494977228308511#heading-8

无锁:

就是一个资源在同一个时刻只能被一个线程访问和修改

偏向锁:

第一次执行syschronized代码块的时候,偏向锁就是偏向于第一个获得该获取它的线程的锁。执行完同步代码块之后并不会主动释放偏向锁。线程访问完资源之后,不会主动释放锁,第二次访问资源的时候,会去检查获取锁的线程是否是自身,如果是不需要加锁

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,此时持syschronized升级为自旋锁。

自旋锁

自旋锁是一种轻量级锁

自旋锁启用的时期有偏向锁关闭的时候,或者多个线程竞争偏向锁的时候,升级为自旋锁

什么叫线程竞争: 就是线程在访问共享资源的时候,不存在阻塞的状态。当线程获取锁的时候发现,锁已经被其他的线程占用,只能等待,这时候就发生了竞争。

通过在用户 态进行自旋地去获取锁,获取锁地过程其实就是循环访问和修改CAS的过程

自旋的过程十分消耗cpu,一个线程在持有锁,其他线程在原地自旋,为了防止长时间的自旋,虚拟机默认允许10次自旋,超过这个自旋次数,就会升级为重量级锁

重量级锁

如果锁竞争情况严重,达到某个最大自旋次数的某个线程,会将轻量级的锁升级为重量级锁。当后续的线程再来获取锁的时候,如果已经被其他线程获取锁不再是自旋,而是暂时挂起,等待被唤醒。就是将锁的获取过程交给操作系统来管理,通过线程调度算法来变更

#实习面经##Java##学习路径#
全部评论
up
点赞 回复
分享
发布于 2022-03-06 18:15
很详细  写很不错
点赞 回复
分享
发布于 2022-03-06 23:26
滴滴
校招火热招聘中
官网直投
收藏了
点赞 回复
分享
发布于 2022-03-17 13:14

相关推荐

点赞 评论 收藏
转发
4 16 评论
分享
牛客网
牛客企业服务