首页 > 其他分享 >多线程面试要点

多线程面试要点

时间:2024-04-09 14:59:39浏览次数:19  
标签:队列 对象 面试 线程 内存 要点 多线程 public wait

一、线程的基础知识

1、线程和进程的区别

一个线程就是一个指令流,将指令流中的一条条指令以一定顺序交给CPU执行

一个进程之内可以分为一到多个线程。

二者对比

进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务。

不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间。

线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换是指从一个线程切换到另外一个线程)

2、并行和并发的区别

单核CPU

  • 单核CPU下线程实际还是串行执行的
  • 操作系统中有一个组件叫作任务调度器,将CPU的时间片分给不同的程序使用,只是由于CPU在线程间(时间片很短)切换的非常快,人类的感觉是同时运行的。
  • 一般会将这种线程轮流使用CPU的做法称为并发(concurrent)

并发:(concurrent)是同一时间应对(dealing with)多件事情的能力,多个线程轮流使用一个或多个CPU

并发:(parallel)是同一时间动手做(doing)多件事的能力,4个CPU同时处理4个线程。

3、创建线程的方式有哪些

1)继承Thread类

public class MyThread extends Thread{

    @Override
    public void run(){
        System.out.println();
    }

    public static void main(String[]args){
        //创建线程
        MyThread my1 = new MyThread();
        my1.start();
    }
}

2)实现runable接口

public class MyRunable implements Runnable{
    @Override
    public void run(){
        System.out.println("");
    }
    public static void main(String[]args){
        MyRunable my = new MyRunable();
        my.start();
    }
}

3)实现Callable接口

public class MyCallable implements Callable<String>{

    @Override
    public String call() throws Exception(){
        System.out.println(System.currentThread().getName());
        return "OK";
    }

    public static void main(String[]args){
        MyCallable my = new MyCallable();
        FutureTask<String> ft = new FutureTask<String>(my);
        Thread t1 = new Thread(ft);
        t1.start();
        String result = ft.get();
        System.out.println(result);
    }
}

4)线程池创建线程

public class MyExecutors implements Runnable{

    @Override
    public void run(){
        System.out.println("MyRunnable ... run ...");
    }

    public static void main(String[]args){
        //创建线程池对象
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        threadPool.submit(new MyExecutors());

        //关闭线程池
        threadPool.shutdown();
    }
}

2、Runnable 和 Callable有什么区别?

Runnable 接口run方法没有返回值

Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以获取异步执行的结果。

Callable接口的call方法允许抛出异常;而Runnable接口的run不能抛出异常。

3、启动线程的时候,可以使用run方法吗?run()和start()方法执行有什么不同

可以使用run方法。

run():只是一个普通方法可以执行多次。

start() :用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑。start只能执行一次。

4、线程间包括哪些状态,状态之间是如何变化的

5、新建T1、T2、T3 三个线程,如何保证它们按顺序执行

可以使用线程中的join方法解决

join() 等待线程运行结束。

t.join() 阻塞调用此方法的线程进入 timed_waiting。

直到线程t执行完成后,此线程再继续执行。

6、notify()和notifyAll() 有什么区别

notify() :随机唤醒一个处于wait状态下的线程。

notifyAll():唤醒全部处于wait状态下的线程。

7、java中wait和sleep方法的不同

共同点

wait(),wait(long)和sleep(long) 的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态。

不同点

1、方法归属不同

  • sleep(long) 是Thread的静态方法。
  • 而wait(),wait(long)都是Object的成员方法,每个对象都有。

2、醒来时机不同

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

3、锁特性不同(重点)

  • wait 方法的调用必须先获取wait对象的锁,而sleep则无此限制。
  • wait方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,其它线程可以用)。
  • 而sleep如果再synchronized代码中执行,并不会释放对象锁(我放弃cpu,其它线程也不能用)。

8、如何停止一个正在运行的线程

有三种方式可以停止线程

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  • 使用stop方法强行终止(不推荐,方法已作废)。
  • 使用interrupt方法中断线程
    1. 打断阻塞的线程(sleep,wait,join)的线程,线程会抛出InterruptedException异常
    2. 打断正常线程,可以根据打断状态来标记是否退出线程。

二、线程中并发安全

1、Synchronized 互斥锁

Synchronized 对象锁,采用互斥的方式让同一时刻至多只有一个线程能持有 对象锁,其它线程再获取这个对象锁时就会阻塞住。

Monitor

Monitor被翻译为监视器,是由jvm提供,c++语言实现。

Owner:存储当前获取锁的线程,只能有一个线程可以获取

EntryList:关联没有抢到锁的线程,处于Blocked状态的线程。

WaitSet: 关联调用了wait方法的线程,处于waiting状态的线程。

2、Synchronized 关键字的底层原理。

基础

  • Synchronized[对象锁]采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】。
  • 它的底层由monitor实现的,monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor。
  • 在monitor内部有三个属性,分别是owner、entrylist、waitset。
  • 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist管理的是处于阻塞状态的线程,waitset是线程调用wait(),处于waiting状态的线程。

进阶

  • Monitor实现的锁属于重量级锁,锁升级过程。
  • Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能较低。
  • 在JDK1.6引入了两种新型锁机制: 偏向锁和轻量级锁,它们的引入是为了在没有多线程竞争或基本没有竞争的场景下因使用传统所机制带来的性能开销问题。
3、对象的内存结构

在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充。

MarkWord

4、Monitor重量级锁

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Wrod中就被重置指向Monitor对象的指针。

5、轻量级锁

在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁都是没必要的。因此JVM引入了轻量级锁的概念。

1、加锁流程

1)在线程栈中创建一个Lock Record,将其obj字段指向锁对象。

2)通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。

3)如果是当前线程已经持有了该锁了,代表这是一次锁重入了。设置Lock Record第一部分为null,起到了一个重入计数的作用

4)如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。

2、解锁过程

1)遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。

2)如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。

3)如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。

6、偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。

7、Monitor实现的锁属于重量级锁,锁的升级过程?

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有,不同线程交替持有锁、多线程竞争锁三种情况。

8、Java内存模型(JMM)

JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则规范对内存的读写操作从而保证指令的正确性。

谈谈JMM(Java内存模型)

JMM java内存模型,定义了共享内存中多线程读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。

JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)。

线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存。

9、CAS

CAS的全称是:Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。

在JUC(java.util.concurrent)包下实现的很多类都用到了CAS操作。

AbstractQueuedSynchronizer(AQS框架)

AtomicXXX类

10、CAS数据交换流程

一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功

因为没有加锁,所以线程不会陷入阻塞,效率较高

如果竞争激烈,重试频繁发生,效率会受影响

11、乐观锁和悲观锁

CAS是基于乐观锁的思想:最乐观的设想,不怕别的线程来修改共享变量,就算改了也没关系,自旋重试。

synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,上锁之后谁都不能修改。

12、volatile的理解

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具有两层含义

1)保证线程间的可见性

2)禁止进行指令重排序

用volatile修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见。

13、谈谈你对volatile的理解

问题分析:主要是因为JVM虚拟机中有一个JIT(即时编译器)给代码做了优化。

解决方案一:在程序运行的时候加入vm参数-Xint表示禁用即时编译器,不推荐,得不偿失。

解决方案二:在修饰stop变量的时候加上volatile,当前搞死JIT,不对volatile修饰的变量进行优化。

volatile禁止指令重排序

用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,组织其他读写操作越过屏障,从而达到阻止重排序的效果。

volatile使用技巧:

  • 写变量让volatile修饰的变量在代码最后位置
  • 读变量让volatile修饰的变量在代码最开始位置

14、什么是AQS

全程是AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架

AQS与Synchronized的区别

synchronized

AQS

关键字,c++语言实现

java 语言实现

悲观锁,自动释放锁

悲观锁,手动开启和关闭

锁竞争激烈都是重量级锁,性能差

锁竞争激烈的情况下,提供了多种解决方案

AQS常见的实现类

  • ReentrantLock 阻塞式锁
  • Semaphore 信号量
  • CountDownLatch 倒计时锁

AQS-基本工作机制

AQS是否是公平锁:

新的线程与队列中的线程共同来抢资源,是非公平锁。

新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁。

15、ReentrantLock的实现原理

ReentrantLock翻译过来就是可重入锁,相对于synchronized具备以下特点:

  • 可中断。
  • 可以设置超时时间。
  • 可以设置公平锁。
  • 支持多个条件变量。
  • 与synchronized一样,都支持重入。

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。

构造方法接收一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在多线程访问的情况下,公平锁表现出较低的吞吐量。

1、ReentranLock的实现原理。

ReentrantLock表示支持重新进入的锁,调用lock方法获取了锁之后,再次调用lock,是不会再阻塞。

ReentrantLock主要利用CAS+AQS队列来实现。

支持公平锁和非公平锁,在提供的构造器中无参数默认是非公平锁,可以传参数设置为公平锁。

2、synchronized 和 Lock有什么区别

语法层面

synchronized是关键字,源码在jvm中,用c++语言实现。

Lock是接口,源码由jdk提供,用java语言实现。

使用synchronized时,退出同步代码块会自动释放,而使用Lock时,需要手动调用unlock方法。

功能层面

二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能。

Lock提供了许多synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量。

Lock有适合不同场景的实现,如ReentrantLock, ReentrantReadWriteLock(读写锁)。

性能层面

在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能不错。

在竞争激烈时,Lock的实现通常会提供更好的性能。

16、死锁产生的条件是什么

死锁:一个线程需要同时获取多把锁,这时容易产生死锁。

线程t1持有A的锁等待获取B锁,线程t2持有B的锁等待获取A锁。

1、如何进行死锁诊断

当程序出现死锁现象,可以使用jdk自带的工具:jps和jstack

jps:输出JVM中运行的进程状态信息。

jstack:查看java进程内线程的堆栈信息。

1.2 其它解决工具,可视化工具

jconsole

用于对jvm的内存,线程,类的监控,是一个基于jmx的GUI性能检测工具。

打开方式:java安装目录bin目录下 直接启动jconsole.exe就行。

VisualVM:故障处理工具

能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈。

打开方式:java安装目录bin目录下 直接启动 jvisualvm.exe就行。

17、聊一下ConcurrentHashMap

ConcurrentHashMap是一种线程安全的高效Map集合。

底层数据结构:

JDK1.7 底层采用分段的数组+链表实现。

JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。

1、JDK1.7中ConcurrentHashMap

2、JDK1.8中ConcurrentHashMap

18、导致并发程序出现问题的根本原因

Java并发编程的三大特性

  • 原子性
  • 可见性
  • 有序性

原子性:一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行。

不是原子操作,怎么保证原子操作呢?

  1. synchronized : 同步加锁
  2. JUC里面的lock:加锁

内存可见性:让一个线程对共享变量的修改对另一个线程可见。

解决方案: synchronized、volatile、LOCK

有序性:指令重排(JIT),处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

解决方案:volatile

三、线程池

1、说一下线程池的核心参数

corePoolSize:核心线程数目

maximumPoolSize:最大核心线程数目 maximumPoolSize = 救急线程 + 核心线程

keepAliveTime:生存单位时间

TimeUnit:时间单位 毫秒、秒

BlockingQueue<Runnable>:当没有空闲核心线程时,新来的任务会加入到此队列排队,队列满会创建救急线程执行任务。

ThreadFactory:线程工厂 -可以定制线程对象的创建,例如设置线程名字、是否是守护线程等。

RejectedExecutionHandler:当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略。

2、线程池的执行原理

3、线程池中哪些常见的阻塞队列

workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务。

  1. ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
  2. LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
  3. DelayedWorkQueue:是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的。
  4. SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出
     
4、ArrayBlockingQueue与LinkedBlockingQueue的区别

5、如何确定核心线程数

IO密集型任务

一般来说:文件读写、DB读写、网络请求等 核心线程数大小设置为2N+1

CPU秘籍型任务

一般来说:计算型代码、Bitmap转换、Gson转换等 核心线程数大小设置为N+1.

6、线程池的种类有哪些

在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见就有4种。

1、创建使用固定线程数的线程池

public static ExecutorService newFixedThreadPool(int nThreads){
   return new ThreadPoolExecutor(nThreads,nThreds
                                 ,0L
                                 ,timeUnit.MILLSECONDS
                                 ,new LinkedBlockingQueue<Runnable>);
}

核心线程数与最大线程数一样,没有临时线程。

阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE。

适用于任务量已知,相对耗时的任务。

2、单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行。

public static ExecutorService newSingleThreadExecutor(){
    return new FinalizableDelegateExecutorService(
        new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLSECONDS,new LinkedBlockingQueue<Runnable>()
    );
}

适用于按照顺序执行的任务。

3、可缓存线程池

public static ExecutorService newCachedThredPool(){
    return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.Seconds,
                                 new SynchronousQueue<Runnable>);
}

核心线程数为0

最大线程数是Integer.MAX_VALUE

阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

适合任务数比较密集,但每个任务执行时间较短的情况。

7、为什么不建议用Executors创建线程池

参考阿里开发手册《Java开发手册-嵩山版》

四、使用场景

1、线程池使用场景(CountDownLatch、Future)

项目使用到了多线程

CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)。

  • 其中构造参数用来初始化等待计数值。
  • await() 用来等待计数归零
  • countDown() 用来让计数减一

标签:队列,对象,面试,线程,内存,要点,多线程,public,wait
From: https://blog.csdn.net/goPlayJava/article/details/137552472

相关文章

  • 再探Java为面试赋能(二)Java基础知识(二)反射机制、Lambda表达式、多态
    文章目录前言1.4反射机制1.4.1Class对象的获取1.4.2Class类的方法1.4.3通过反射机制修改只读类的属性1.5Lambda表达式1.5.1函数式接口1.5.2Lambda表达式的使用1.6多态1.6.1多态的概念1.6.2多态的实现条件1.6.3重载(Overload)和重写(Override)前言往期精选......
  • LeetCode 面试经典150题---003
    ####55.跳跃游戏给你一个非负整数数组nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标,如果可以,返回true;否则,返回false。1<=nums.length<=1040<=nums[i]<=105本题题意比较明确,我们可以......
  • 肖sir__ 接口测试面试题(12.2)
    1.postman接口测试,它有一个功能可以设置参数化,你有用过吗1、创建数据文件,支持数据格式文件分别为csv和json、txt等,这里我们以创建txt文档为例2、请求中对应位置替换参数变量:请求参数中用{{参数名}}替换,代码中通过:data.参数名来进行替换,注意这里的变量名要和txt文档中的变量名一......
  • 心态崩了,约了半个月,就只有3个面试!
    声明:本文首发在同名公众号:王中阳Go,未经授权禁止转载。先来唠唠今儿咱们聊聊这位面试的哥们儿,最近半个月他只约到了3次面试,心里那个急啊,总怕错过了找工作的黄金期。我跟他说:“淡定点,现在找工作的机会还多着呢。不要和别人比,把握好自己的节奏。花也不是一下子全开的,找工作也得......
  • 大数据面试临阵磨枪不知看什么?看这份心理就有底了-大数据常用技术栈常见面试100道题
    目录1描述Hadoop的架构和它的主要组件。2MapReduce的工作原理是什么?......
  • TCP 三次握手与四次挥手面试题(计算机网络)
    TCP基本认识TCP头格式有哪些?  序列号:在建立连接时由计算机生成的随机数作为其初始值,通过SYN包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应......
  • 揭秘程序员面试技巧,助你轻松拿offer!
    上文程序员面试是求职者展现自身技术实力、沟通能力和职业素养的关键环节。为了在面试中脱颖而出,求职者需要掌握一些实用的面试技巧。以下将详细阐述程序员面试技巧,助您在面试中取得更好的成绩。一、面试前准备了解公司及职位在面试前,务必对目标公司及职位进行深入了解......
  • 面试必问!鸿蒙开发中的FA模型和Stage模型是什么?他们分别有什么区别?
    鸿蒙OS(HarmonyOS)是面向全场景的分布式操作系统,它通过创新的应用模型,为开发者提供了强大的应用开发框架。在HarmonyOS的发展过程中,FA模型(FeatureAbility)和Stage模型是两种重要的应用模型。今天来跟大家聊一聊,鸿蒙开发中的FA模型和Stage模型。这个问题是鸿蒙应用开发面试......
  • 面试经历
    Tags:#面试经历面经公司:城市轨道交通面试方式:电话面试问答首先问了一个我的项目,我开发的最完整的项目就是那个io的接口。问了常用vector,vecotr的数据保存在堆上还是栈上。堆上clear是否可以释放vector持有的内存。不能如果vector在生命周期内,如何使其释放内存。cle......
  • C语言面试题之化栈为队
    化栈为队实例要求C语言实现实现一个MyQueue类,该类用两个栈来实现一个队列;示例:MyQueuequeue=newMyQueue();queue.push(1);queue.push(2);queue.peek();//返回1queue.pop();//返回1queue.empty();//返回false说明:1、只能使用标准的栈操作,即只有p......