《面试专题-----经典高频面试题收集三》解锁 Java 面试的关键:深度解析并发编程基础篇高频经典面试题(第三篇)

目录

并发编程面试题

1.什么是进程、线程、协程,他们之间的关系是怎样的

matlab 复制代码
进程: 本质上是⼀个独⽴执⾏的程序,进程是操作系统进⾏资源分配和调度的基本概念,操作系统进⾏资源分配和调度的⼀个独⽴单位

线程:是操作系统能够进⾏运算调度的最⼩单位。它被包含在进程之中,是进程中的实际运作单位。⼀个进程中可以并发多个线程,每条线程执⾏不同的任务,切换受系统控制
 
协程: ⼜称为微线程,是⼀种⽤户态的轻量级线程,协程不像线程和进程需要进⾏系统内核上的上下⽂切换,协程的上下⽂切换是由⽤户⾃⼰决定的,有⾃⼰的上下⽂,所以说是轻量级的线程,也称之为⽤户级别的线程,⼀个线程可以多个协程,线程进程都是同步机制,⽽协程则是异步 

Java的原⽣语法中并没有实现协程,⽬前python、Lua和GO等语⾔⽀持

关系:⼀个进程可以有多个线程,它允许计算机同时运⾏两个或多个程序。线程是进程的最⼩执⾏单位,CPU的调度切换的是进程和线程,进程和线程多了之后调度会消耗⼤量的CPU,CPU上真正运⾏的是线程,线程可以对应多个协程

2.协程对于多线程有什么优缺点吗

matlab 复制代码
优点: 
    ⾮常快速的上下⽂切换,不⽤系统内核的上下⽂切换,减⼩开销 
    单线程即可实现⾼并发,单核CPU可以⽀持上万的协程 
    由于只有⼀个线程,也不存在同时写变量的冲突,在协程中控制共享资源不需要加锁 缺点: 
    协程⽆法利⽤多核资源,本质也是个单线程 
    协程需要和进程配合才能运⾏在多CPU上 
    ⽬前java没成熟的第三⽅库,存在⻛险 
    调试debug存在难度,不利于发现问题

3.说下并发和并行的区别,举些例子说下

matlab 复制代码
并发 concurrency: 
    ⼀台处理器上同时处理任务, 这个同时实际上是交替处理多个任务,程序中可以同时拥有两个或者多个线程,当有多个线程在操作时,如果系统只有⼀个CPU,则它根本不可能真正同时进⾏⼀个以上的线程,它只能把CPU运⾏时间划分成若⼲个时间段,再将时间段分配给各个线程执⾏
    
并⾏ parallellism: 
    多个CPU上同时处理多个任务,⼀个CPU执⾏⼀个进程时,另⼀个CPU可以执⾏另⼀个进程,两个进程互不抢占CPU资源,可以同时进⾏   

并发指在⼀段时间内宏观上去处理多个任务。 并⾏指同⼀个时刻,多个任务确实真的同时运⾏  
例⼦: 
    并发是⼀⼼多⽤,听课和看电影,但是CPU⼤脑只有⼀个,所以轮着来 
    并⾏:⽕影忍者中的影分身,有多个你出现,可以分别做不同的事情 
    
⼀个项⽬经理A和3个程序B C D的故事 
单线程 
    并发:A给B讲完需求,B⾃⼰去实现,期间A继续给C和D讲,不⽤等待某个程序员去完成,期间项⽬经理没空闲下来 
    并⾏:直接找3个项⽬经理分别分配给3个程序员

4.Java里面实现多线程有哪几种方式,有什么不同,比较常用哪种

matlab 复制代码
●继承Thread
//继承Thread,重写⾥⾯run⽅法,创建实例,执⾏start 
//优点:代码编写最简单直接操作 
//缺点:没返回值,继承⼀个类后,没法继承其他的类,拓展性差

public class ThreadDemo1 extends Thread {  
    @Override  
    public void run() {  
        System.out.println("继承Thread实现多线程,名 称:"+Thread.currentThread().getName());  
    } 
}

public static void main(String[] args) {  
    ThreadDemo1 threadDemo1 = new ThreadDemo1();  
    threadDemo1.setName("demo1");  
    threadDemo1.start();  
    System.out.println("主线程名称:"+Thread.currentThread().getName()); 
}



●实现Runnable
//⾃定义类实现Runnable,实现⾥⾯run⽅法,创建Thread类,使⽤Runnable接⼝的实现对象作为参数传递给Thread对象,调⽤Strat⽅法 
//优点:线程类可以实现多个⼏接⼝,可以再继承⼀个类 
//缺点:没返回值,不能直接启动,需要通过构造⼀个Thread实例传递进去启动

//JDK8之后采⽤lambda表达式 
public static void main(String[] args) {  
    Thread thread = new Thread(()->{  
    System.out.println("通过Runnable实现多线程,名 称:"+Thread.currentThread().getName());  }
    );  
    thread.setName("demo2");  
    thread.start();  
    System.out.println("主线程名称:"+Thread.currentThread().getName()); }



●通过Callable和FutrueTask方式
//创建callable接⼝的实现类,并实现call⽅法,结合FutureTask类包装Callable对象,实现多线程 
//优点:有返回值,拓展性也⾼ 
//缺点:jdk5以后才⽀持,需要重写call⽅法,结合多个类⽐如FutureTask和Thread类

public class MyTask implements Callable {  
    @Override
    public Object call() throws Exception {      System.out.println("通过Callable实现多线程,名 称:"+Thread.currentThread().getName());  return "这是返回值";  
    } 
}

public static void main(String[] args) {  
    FutureTask futureTask = new FutureTask<>(()->{  System.out.println("通过Callable实现多线程,名 称:"+Thread.currentThread().getName());  
    return "这是返回值";  
    });
    
    Thread thread = new Thread(futureTask); 
    thread.setName("demo3");  
    thread.start();  
    System.out.println("主线程名 称:"+Thread.currentThread().getName());
    
    try {  
        System.out.println(futureTask.get());  
    } catch (InterruptedException e) {  
        //阻塞等待中被中断,则抛出  
        e.printStackTrace();  
    } catch (ExecutionException e) {  
        //执⾏过程发送异常被抛出  
        e.printStackTrace();  
    }  
}


●通过线程池创建线程
//⾃定义Runnable接⼝,实现run⽅法,创建线程池,调⽤执⾏⽅法并传⼊对象 
//优点:安全⾼性能,复⽤线程 
//缺点: jdk5后才⽀持,需要结合Runnable进⾏使⽤

public class ThreadDemo4 implements Runnable {  
    @Override  
    public void run() {  
    System.out.println("通过线程池+runnable实现多线程,名 称:"+Thread.currentThread().getName());  
    } 
}

public static void main(String[] args) {  
    ExecutorService executorService = Executors.newFixedThreadPool(3);  
    for(int i=0;i<10;i++){  
        executorService.execute(new ThreadDemo4());  
    }  
    System.out.println("主线程名 称:"+Thread.currentThread().getName());  
    //关闭线程池  
    executorService.shutdown();
}



●一般常用的Runnable和线程池

5.java线程常见的基本状态有哪些,这些状态分别是做什么的
JDK的线程状态分6种,JVM⾥⾯9种,我们⼀般说JDK的线程状态 。

常⻅的5种状态:
    创建(NEW): ⽣成线程对象,但是并没有调⽤该对象start(), new Thread()  
    就绪(Runnable):当调⽤线程对象的start()⽅法,线程就进⼊就绪状态,但是此刻线程调度还没把该线程设置为当前线程,就是没获得CPU使⽤权。如果线程运⾏后,从等待或者睡眠中回来之后,也会进⼊就绪状态。注意:有些⽂档把就绪和运⾏两种状态统⼀称为 "运⾏中" 
    运⾏(Running) 程序将处于就绪状态的线程设置为当前线程,即获得CPU使⽤权,这个时候线程进⼊运⾏状态,开始运⾏run⾥⾯的逻辑 
    阻塞(Blocked) 等待阻塞:进⼊该状态的线程需要等待其他线程作出⼀定动作(通知或中断),这种状态的话CPU不会分配过来,他们需要被唤醒,可能也会⽆限等待下去。⽐如调⽤wait(状态就会变成 WAITING状态),也可能通过调⽤sleep(状态就会变成TIMED_WAITING), join或者发出IO请求,阻塞结束后线程重新进⼊就绪状态 
    同步阻塞:线程在获取synchronized同步锁失败,即锁被其他线程占⽤,它就会进⼊同步阻塞状态 
    备注:相关资料会⽤细分下⾯的状态  
        等待(WAITING):进⼊该状态的线程需要等待其他线程做出⼀些特定动作(通知或中断)
        超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后⾃⾏返回 
    死亡(TERMINATED):⼀个线程run⽅法执⾏结束,该线程就死亡了,不能进⼊就绪状态

6.是否了解多线程开发里面的常用方法,sleep/yield/join wait/notify/notifyAll

matlab 复制代码
sleep 属于线程Thread的⽅法,让线程暂缓执⾏,等待预计时间之后再恢复,交出CPU使⽤权,不会释放锁,进⼊阻塞状态TIME_WAITGING,睡眠结束变为就绪Runnable 

yield 属于线程Thread的⽅法  t1/t2/t3 暂停当前线程的对象,去执⾏其他线程 交出CPU使⽤权,不会释放锁,和sleep类似 
作⽤:让相同优先级的线程轮流执⾏,但是不保证⼀定轮流 
注意:不会让线程进⼊阻塞状态,直接变为就绪Runnable,只需要重新获得CPU使⽤权 

join 属于线程Thread的⽅法 在主线程上运⾏调⽤该⽅法,会让主线程休眠,不会释放已经持有的对象锁 让调⽤join⽅法的线程先执⾏完毕,在执⾏其他线程 类似让救护⻋警⻋优先通过 

wait 属于Object的⽅法 当前线程调⽤对象的wait⽅法,会释放锁,进⼊线程的等待队列 需要依靠notify或者notifyAll唤醒,或者wait(timeout)时间⾃动唤醒 

notify 属于Object的⽅法 唤醒在对象监视器上等待的单个线程,选择是任意的 

notifyAll 属于Object的⽅法 唤醒在对象监视器上等待的全部线程

7.画一下线程状态转换图和这些转换过程中常见的api也标记一下

8.举例几个平时遇到的多线程场景

matlab 复制代码
异步任务:⽤户注册、记录⽇志、数据解析
定时任务:定期备份⽇志、备份数据库
分布式计算:Hadoop处理任务mapreduce,master-wark(单机单进程) 
服务器编程:Socket⽹络编程,⼀个连接⼀个线程

9.举几个不是线程安全的结构

HashMap ArrayList LinkedList

10.Java中可以有哪些方法保证线程安全

matlab 复制代码
加锁,⽐如synchronized/ReentrantLock 
使⽤volatile声明变量,轻量级同步,不能保证原⼦性(需要解释) 
使⽤线程安全类(原⼦类AtomicXXX,并发容器,同步容器)
CopyOnWriteArrayList/ConcurrentHashMap等 
ThreadLocal本地私有变量/信号量Semaphore等

11.了解volatile吗?能否解释下,然后这和synchronized有什么大的区别

matlab 复制代码
volatile是轻量级的synchronized,保证了共享变量的可⻅性,被volatile关键字修饰的变量,如果值发⽣了变化,其他线程⽴刻可⻅,避免出现脏读现象
volatile:保证可⻅性,但是不能保证原⼦性 
synchronized:保证可⻅性,也保证原⼦性 

使⽤场景 
1、不能修饰写⼊操作依赖当前值的变量,⽐如num++、num=num+1,不是原⼦操作,⾁眼看起来是,但是JVM字节码层⾯不⽌⼀步 
2、由于禁⽌了指令重排,所以JVM相关的优化没了,效率会偏弱

12.为什么会出现脏读

matlab 复制代码
JAVA内存模型简称:JMM 
JVM规定所有的变量存在在主内存,每个线程有⾃⼰的⼯作内存,线程对变量的操作都在⼯作内存中进⾏,不能直接对主内存就⾏操作
使⽤volatile修饰变量 每次读取前必须从主内存获取最新的值 每次写⼊需要⽴刻写到主内存中
volatile关键字修修饰的变量随时看到的⾃⼰的最新值,假如线程1对变量v进⾏修改,那么线程2 是可以⻢上看⻅

13.volatile可以避免指令重排,解释一下为什么

matlab 复制代码
指令重排序分两类:编译器重排序和运⾏时重排序 
JVM在编译java代码或者CPU执⾏JVM字节码时,对现有的指令进⾏重新排序,主要⽬的是优化运⾏效率(不改变程序结果的前提) 

int a = 3 //1 
int b = 4 //2 
int c = 5 //3  
int h = a*b*c //4 

定义顺序 1,2,3,4 计算顺序 1,3,2,4 和 2,1,3,4 结果都是⼀样 
虽然指令重排序可以提⾼执⾏效率,但是多线程上可能会影响结果,有什么解决办法? 
解决办法:内存屏障 
解释:内存屏障是屏障指令,使CPU对屏障指令之前和之后的内存操作执⾏结果的⼀种约束

14.知道happens-before吗,能否简单解释一下

matlab 复制代码
先⾏发⽣原则,volatile的内存可⻅性就体现了该原则之⼀

例⼦: 
//线程A操作 
int k = 1; 
//线程B操作 
int j = k; 
//线程C操作 
int k = 2;

分析: 
假设线程A中的操作"k=1"先⾏发⽣于线程B的操作"j=k",那确定在线程B的操作执⾏后,变量j的值 ⼀定等于1,依据有两个:⼀是先⾏发⽣原则,"k=1"的结果可以被观察到;⼆是第三者线程C还没出现,线程A操作结束之后没有其他线程会修改变量k的值。 
但是考虑线程C出现了,保持线程A和线程B之间的先⾏发⽣关系,线程C出现在线程A和线程B的操作之间,但是线程C与线程B没有先⾏发⽣关系,那j的值会是多少?答案是1和2都有可能,因为线程C对变量k的影响可能会被线程B观察到,也可能不会,所以线程B就存在读取到不符合预期数据的⻛险,不具备多线程安全性

⼋⼤原则(对这个不理解,⼀定要去补充相关博⽂知识) 
1、程序次序规则 
2、管程锁定规则 
3、volatile变量规则 
4、线程启动规则 
5、线程中断规则 
6、线程终⽌规则 
7、对象终结规则 
8、传递性

结语

这部分是第三篇高频经典面试题学习分享,欢迎大家讨论学习分享,你的三连是我最大的动力,第四期会马上出哦,敬请期待

相关推荐
Lee川37 分钟前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i3 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
孟陬3 小时前
国外技术周刊 #1:Paul Graham 重新分享最受欢迎的文章《创作者的品味》、本周被划线最多 YouTube《如何在 19 分钟内学会 AI》、为何我不
java·前端·后端
想用offer打牌3 小时前
一站式了解四种限流算法
java·后端·go
绝无仅有3 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有3 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
华仔啊3 小时前
Java 开发千万别给布尔变量加 is 前缀!很容易背锅
java
AAA梅狸猫4 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫4 小时前
Handler基本概念
面试