【Java校招面试】基础知识(三)——多线程与并发


前言

本篇主要介绍Java多线程与并发相关内容。

“基础知识”是本专栏的第一个部分,本篇博文是第三篇博文,如有需要,可:

  1. 点击这里,返回本专栏的索引文章
  2. 点击这里,返回上一篇《【Java校招面试】基础知识(二)——Spring Framework & AOP》
  3. 点击这里,前往下一篇《【Java校招面试】基础知识(四)——JVM》

一、基础概念

01. 进程和线程的区别
1)进程和线程的由来
在这里插入图片描述
进程让并发成为可能,线程让进程的内部并发成为可能。

2)区别
 ① 进程是资源分配的最小单位,线程是CPU调度的最小单位;
 ② 所有进程相关的资源都被记录在PCB中;
在这里插入图片描述
 ③ 进程是抢占处理机的调度单位,线程属于某个进程,共享其资源;
 ④ 线程只由堆栈寄存器、程序计数器和TCB组成。
在这里插入图片描述

3)总结
 ① 线程不能看作独立的应用,而进程可以看作独立的应用;
 ② 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径;
 ③ 线程没有独立的地址空间,一个线程崩溃,相应的进程也会崩溃,因此多进程的程序比多线程的程序健壮;
 ④ 进程的切换开销比线程大。

02. 进程和线程的关系
1) Java对操作系统提供的功能进行封装,包括进程和线程;
2) 运行一个程序会产生一个进程,进程包括至少一个线程;
3) 每个进程对应一个JVM实例,多个线程共享JVM里的堆;
4) Java采用单线程编程模型,程序会自动创建主线程;
5) 主线程可以创建子线程,原则上要后于子线程完成执行。

03. Thread中start方法和run方法的区别
在这里插入图片描述

  • 调用start方法会创建一个新的子线程,并在新的子线程中调用run
  • 直接调用run方法只会在当前线程中调用Thread中的run方法块。

04. Thread和Runnable的关系
1) Thread是实现了Runnable接口的类,使得run支持多线程;
2) 因类的单一继承原则,推荐多使用Runnable接口。

05. 如何获取线程中执行的方法的返回值?
1) 主线程等待法
2) 使用Thread类的join方法阻塞当前线程以等待子线程处理完毕;
3) 通过Callable接口实现:
通过FutureTask获取;
通过线程池获取。

06. 通过Callable接口实现获取线程返回值的例子
1) 公共的MyCallable类

	public class CallableInterface implements Callable<String>{
    
    
	    @Override
	    public String call() throws Exception {
    
    
	        String value = "ReturnValue";
	        System.out.println("Ready to Work!");
	        Thread.currentThread().sleep(5000);
	        System.out.println("Task Done.");
	        return value;
	    }
	}

2) FutureTask方式获取线程返回值

	public class FutureTaskTest {
    
    
	    public static void main(String[] args) throws InterruptedException, ExecutionException {
    
    
	        FutureTask<String> futureTask = new FutureTask<>(new CallableInterface());
	        new Thread(futureTask).start();
	        if (!futureTask.isDone()) {
    
    
	            System.out.println("Task has not been done!");
	        }
	        System.out.println("Task had been done and the return value is: " + futureTask.get());
	    }
	}

3) 线程池方式获取线程返回值

public class ThreadPoolTest {
    
    
    public static void main(String[] args) {
    
    
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        Future<String> future = newCachedThreadPool.submit(new CallableInterface());
        if (!future.isDone()) {
    
    
            System.out.println("Task has not been done!");
        }
        try {
    
    
            System.out.println("Task had been done and the return value is: " + future.get());
        } catch (InterruptedException | ExecutionException ex) {
    
    
            Logger.getLogger(ThreadPoolTest.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
    
    
            newCachedThreadPool.shutdown();
        }
    }
}

4) 输出
两种方式输出相同,只列一份

Task has not been done!
Ready to work!
Task Done.
Task had been done and the return value is: ReturnValue

07. 线程的6种状态
在这里插入图片描述
1) 新建(New): 创建后尚未启动
2) 运行(Runnable): 包含Running和Ready
3) 无限期等待(Waiting): 不会被分配CPU执行时间,需要被显式唤醒
 以下3种方法可以使线程进入Waiting状态:
   没有设置TimeOut参数的Object.wait()方法;
   没有设置TimeOut参数的Thread.join()方法;
   LockSupport.park()方法。
4) 限期等待(Timed Waiting): 在一定时间后由系统自动唤醒
 以下5种方法可以使线程进入Timed Waiting状态:
   Thread.sleep()方法;
   设置了TimeOut参数的Object.wait()方法;
   设置了TimeOut参数的Thread.join()方法;
   LockSupport.parkNanos()方法;
   LockSupport.parkUntil()方法。
5) 阻塞(Blocked): 等待获取排它锁
6) 结束(Terminated): 已终止,线程已经结束执行

08. sleep和wait的区别
1) sleep是Thread类的方法,wait是Object类中定义的方法;
2) sleep方法可以在任何地方使用,wait方法只能在被冠以synchronized的方法或synchronized块中使用;
3) Thread.sleep只会让出CPU,不会导致锁行为的改变,Object.wait不仅让出CPU,还会释放已经占有的同步资源锁。

09. notify和notifyAll的区别
在这里插入图片描述
1) 锁池
  假设线程A已经拥有了某个对象(不是类)的锁,而其他线程B、C想要调用这个对象的某个synchronized方法(或者块),由于B、C线程在进入对象的synchronized方法(或者块)之前必须先获得该对象的锁,而此时锁正在被线程A持有,此时B、C线程就会被阻塞,进入锁池去等待锁的释放。
2) 等待池
  假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。
3) notify和notifyAll的区别

  • notify只会随机选取一个处于等待池中的线程进入锁池去竞争锁;
  • notifyAll会让所有处于等待池中的线程全部进入锁池去竞争锁。

10. yield
当调用Thread.yield()方法时,会给线程调度器一个当前线程愿意让出CPU的暗示,但是线程调度器可能会忽略这个暗示。yield不会改变锁的状态。

11. 如何中断线程?

  • 已被抛弃的方法(这些方法可能引发数据不同步的问题):

    • 通过调用stop()方法停止线程;
    • 通过调用suspend()和resume()方法;
  • 目前的方法:

    • 调用interrupt(), 通知线程应该中断了。
      • 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个 InterruptedException异常;
      • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置 中断标志的线程将继续正常运行,不受影响;
      • 需要被调用的线程配合中断。在正常运行任务时,经常检查本线程的中断标志 位,如果被设置了中断标志就自行停止线程。

二、互斥锁

01. 互斥锁的特性
1) 互斥性: 即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。
2) 可见性: 必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得共享变量最新的值),否则另一个线程可能是在本地缓存的某个副本上执行操作,从而导致数据的不一致。

注意: synchronized锁的是对象,而不是代码

02. 对象锁和类锁
1) 获取对象锁的方法
  ①同步代码块synchronized(this)或synchronized(类对象实例),锁的是小括号 中的对象实例;
  ②同步非静态方法synchronized method,锁的也是当前的对象实例。
2) 获取类锁的方法
  ①同步代码块synchronized(类.class),锁的是小括号中的类对象;
  ②同步静态方法synchronized static method,锁的是当前对象的类对象。

注意:类锁和对象锁互不干扰

03. Monitor
在这里插入图片描述
每个对象自带的锁,也称为内部锁、管程。重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的。

04. 重入
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入。

05. 早期版本中synchronized的缺点
1) 早期版本中,synchronized属于重量级锁,依赖于Mutex Lock实现;
2) 线程之间的切换需要从用户态转换到内核态,开销比较大。

06. 自旋锁和自适应自旋锁
1) 自旋锁
  许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得。自旋锁通过让线程执行忙循环等待锁的释放,不让出CPU。 它的缺点是:若锁被其他线程长时间占用,会带来很多性能上的开销。可以使用PreBlockSpin来设置自旋上限,当自选次数超过这个值,就是用传统方式挂起线程。
2) 自适应自旋锁
  由于自旋上限很难设定,因此在自适应自旋锁中自旋的的次数不再固定。由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
  如果在同一个锁上,刚刚通过自旋获取到过锁,且持有锁的线程正在运行中,则JVM会判定在该锁上通过自旋获得锁的概率很大,JVM会自动增加等待时间。
  反之,如果在一个锁上通过自旋很少获得过锁,那么在以后和获取该锁时,JVM可能会省略掉自旋过程,以避免浪费处理器资源。

07. 锁消除
它是更彻底的优化。在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。

08. 锁粗化
如果在一个代码块中,对一个对象反复加锁解锁,或者锁出现在循环体中,此时通过扩大加锁的范围来避免反复加锁解锁。

09. synchronized的四种状态
无锁偏向锁轻量级锁重量级锁
锁膨胀的方向: 无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁
1) 偏向锁: 减少同一线程获取锁的代价
  大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得。
  如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需检查Mark Word的锁标记位为偏向锁,以及当前线程Id等于Mark Word的ThreadID即可,这样就省去大量有关锁申请的操作。

注意: 不适用于锁竞争激烈的多线程场景。

2) 轻量级锁: 由偏向锁升级而来,当第二个线程加入锁竞争的时候,偏向锁就会升级为轻量级锁。适用于线程交替执行同步块的场景,如果存在同一时间访问同一个锁的情况,就会导致轻量级锁膨胀为重量级锁。

  • i) 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为Displaced Mark Word。
  • ii) 拷贝对象头中的Mark Word复制到锁记录中。这时候线程堆栈与对象头的状态如图所示:
    在这里插入图片描述
  • iii) 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(iv),否则执行步骤(v)。
  • iv) 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示:
    在这里插入图片描述
  • v) 如果这个更新操作失败了,说明多个线程竞争锁,轻量级锁就要膨胀为重量级 锁。

3) 解锁的过程
i) 通过CAS操作尝试用线程中复制的Displaced Mark Word对象替换当前的 Mark Word;
ii) 如果替换成功,整个同步过程就完成了;
iii) 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那么在 释放锁的同时,还会唤醒被挂起的线程。

10. 偏向锁、轻量级锁、重量级锁的对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差异 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 只有一个线程访问同步块或同步方法的场景
轻量级锁 竞争的线程不会阻塞,提高了响应速度 若线程长时间抢不到锁,自旋会消耗CPU性能 线程交替执行同步块或同步方法的场景
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗 追求吞吐量,同步块或者同步方法执行时间较长的场景

11. synchronized和ReentrantLock的区别
1) ReentrantLock
   位于java.concurrent.locks包;
   和CountDownLatch、FutureTask、Semaphore一样基于AQS(Abstract Queue Synchronizer)实现;
   能够实现比synchronized更细粒度的控制,如控制fairness;
   调用lock()之后必须调用unlock()释放锁;
   性能未必比synchronized高,并且也是可重入的。
2) ReentrantLock的公平性设置
   ReentrantLock fairLock = new ReentrantLock(true);
   参数为true时,倾向于将锁赋予等待时间最久的线程;
   公平锁:获取锁的顺序按先后调用lock()的顺序(慎用);
   非公平锁:抢占的顺序不一定;
   synchronized是非公平锁。
3) ReentrantLock的特性
   ReentrantLock将锁对象化;
   可以判断是否有线程,或者某个特定的线程在排队等待获取锁;
   带超时的获取锁的尝试;
   可以感知有没有获取到锁;
4) 区别
   synchronized是关键字,ReentrantLock是类;
   ReentrantLock可以对获取锁的等待时间进行设置,避免死锁;
   ReentrantLock可以获取各种锁的信息;
   ReentrantLock可以灵活地实现多路通知;
   机制:synchronized操作Mark Word,ReentrantLock调用Unsafe类的 park()方法。


三、Java内存模型(JMM)

01. JMM是什么?
JMM本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。
在这里插入图片描述
1) JMM中的主内存
   存储Java实例对象包括成员变量、类信息、常量、静态变量等;
   属于数据共享区域,多线程并发操作时会引发线程安全问题。
2) JMM中的工作内存
   存储当前方法的所有本地变量信息,本地变量对其他线程不可见;
   包括字节码行号指示器、Native方法信息;
   属于线程私有数据区域,不存在线程安全问题。

02. 主内存和工作内存的数据存储类型以及操作方式
1) 方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中;
2) 引用类型的本地变量:引用存储在工作内存中,实力存储在主内存中;
3) 成员变量、static变量、类信息均会被存储在主内存中;
4) 主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存。

03. happens-before

  • 如果两个操作不满足以下8大规则中的任意一个,那么两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序;
  • 如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。

happens-before的八大规则:
1) 程序次序规则: 一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2) 锁定规则: 一个unLock操作先发生于后面对同一个锁的Lock操作;
3) volatile变量规则: 对一个变量的写操作先行发生于后面对这个变量的读操作;
4) 传递规则: 如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
5) 线程启动规则: Thread对象的start()方法先行发生于此线程的每一个动作;
6) 线程中断规则: 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
7) 线程终结原则: 线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
8) 对象终结规则: 一个对象的初始化完成先行发生于他的finalize()方法的开始。

04. volatile
Java提供的轻量级同步机制
1) 保证被volatile修饰的共享变量对所有线程总是可见的;
2) 禁止指令重排序优化。

05. volatile变量的修改为何立即可见?
1) 当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中;
2) 当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,该线程只能从主内存中重新读取共享变量。

06. volatile如何禁止重排优化?
通过插入内存屏障指令,禁止在内存屏障前后的指令执行重排序优化。

内存屏障(Memory Barrier): 是一个CPU指令,可以保证特定操作的执行顺序。

07. volatile的使用举例——设计模式之单例模式的双重检测
1) 首先看一个单例模式的基础实现:

	public class Singleton {
    
    
    	private static Singleton instance;
    	private Singleton(){
    
    };
    	public static Singleton getInstance(){
    
    
        	// 第一次检测
        	if (null == instance){
    
    
            	// 同步
            	synchronized(Singleton.class){
    
    
                	// 第二次检测,多线程下可能会出现问题
                	if (null == instance){
    
    
                    	instance = new Singleton();
                	}
            	}
        	}
        	return instance;
    	}
	}

2) 问题分析:
首先将instance = new Singleton()转换成伪代码:

	① memory = allocate();
	② Singleton(memory);
	③ instance = memory;

因为②和③不存在happens-before关系,指令可能会重排为:

	① memory = allocate();
	③ instance = memory;
	② Singleton(memory);

这在单线程中不存在问题。但是在多线程中,当线程a执行了③之后instance != null,但Singleton对象未必完成了初始化,此时若线程b来取instance,会取到未完成实例化的instance,发生线程安全问题。

3) 改进方案:
instance加上volatile限定符,禁止指令重排

4) 改进后的代码:

		public class Singleton {
    
    
			// 这里加上了volatile关键字
	    	private volatile static Singleton instance;
	    	private Singleton(){
    
    };
	    	public static Singleton getInstance(){
    
    
	        	// 第一次检测
	        	if (null == instance){
    
    
	            	// 同步
	            	synchronized(Singleton.class){
    
    
	                	// 第二次检测
	                	if (null == instance){
    
    
	                    	instance = new Singleton();
	                	}
	            	}
	        	}
	        	return instance;
	    	}
		}

08. volatile和synchronized的区别
1) volatile本质是告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到当前线程完成了变量操作;
2) volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别;
3) volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性;
4) volatile不会造成现成的阻塞;synchronized可能会造成现成的阻塞;
5) volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。


四、线程池

01. 利用Executors创建不同的线程池满足不同场景的需求
1) newFixedThreadPool(int nThreads): 指定工作线程数量的线程池
2) newCachedThreadPool(): 处理大量短时间工作任务的线程
   试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
   如果线程闲置的时间超过阈值,则会被终止并移除缓存;
   系统长时间闲置的时候,不会消耗什么资源。
3) newSingleThreadExecutor(): 创建唯一的工作线程来执行任务,如果线程异常结束,会有另一个线程取代它
4) newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize): 定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程
5) newWorkStealingPool(): 内部会构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序。

02. Folk / Join
把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果得到大任务结果的框架
working-stealing算法: 某个线程从其他队列里窃取任务来执行
在这里插入图片描述
03. 为什么要使用线程池?
1) 降低资源消耗
2) 提高线程的可管理性

04. java.util.concurrent包中的三个Executor接口
1) Executor: 运行新任务的简单接口,将任务提交和任务执行的细节解耦
2) ExecutorService: 具备管理执行器和任务生命周期的方法,提交任务机制更完善
3) ScheduledExecutorService: 支持Future和定期执行任务

05. ThreadPoolExecutor构造方法中的参数
1) corePoolSize: 核心线程数;
2) maximumPoolSize: 线程不够时可以创建的最大线程数;
3) workQueue: 等待队列,可根据实际情况选择的实现类:
  ① PriorityBlockingQueue: 支持优先级排序的无界队列;
  ② DelayQueue: 支持延时获取元素的无界阻塞队列;
  ③ SynchronousQueue: 不存储元素的阻塞队列。每一个put操作必须等待一个 take操作,否则不能继续添加元素;
  ④ LinkedTransferQueue: 相对于其他队列多了transfer和tryTransfer方 法。其中前者会等待消费者将元素消费之后才返回,后者无视结果直接返回;
  ⑤ LinkedBlockingDeque: 由链表结构组成的双向阻塞队列。在多线程同时入队 时,可以减少一半的竞争。可以运用在working-stealing模式中。
4) keepAliveTime: 线程池允许线程空闲的时间,大于corePoolSize的线程最大的空闲时间,超时才会被销毁;
5) threadFactory: 用于创建新线程,默认为Executors.defaultThreadFactory();
6) handler: 线程池的饱和策略
  ① AbortPolicy: 直接抛出异常,默认策略;
  ② CallerRunsPolicy: 用调用者所在的线程来执行;
  ③ DiscardOldestPolicy: 丢弃队列中最靠前的任务,并执行当前任务;
  ④ DiscardPolicy: 直接丢弃任务;
   可以通过实现RejectedExecutionHandler借口来使用自定义策略;

06. 提交新任务后,线程池的处理流程
在这里插入图片描述
1) 如果运行的线程少于corePoolSize,则创建新线程来处理任务,即使线程池中其他线程是空闲的;
2) 如果线程池中的线程数量 >= corePoolSize且小于maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务;
3) 如果设置的corePoolSizemaximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程从workQueue中去任务并处理;
4) 如果运行的线程数 >= maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务。

07. 什么时候适用多线程?
1) 多任务同时执行加快处理速度,如批量给满足条件的用户发送邮件;
2) 异步执行,如UI线程与计算、下载线程分开,避免UI假死;
总结: 短期模型相同的任务使用线程池,长期任务使用独立线程。

08. 如果突然断电了,线程池会怎么样?
为了应对这种情况,应该对正在处理的任务做事务管理,对阻塞队列中的任务持久化处理。当断电后,通过回溯日志的方式来撤销正在处理的已经执行成功的操作,重新执行整个阻塞队列。

09. 线程池的状态
在这里插入图片描述
1) RUNNING: 能接受新提交的任务,并且能处理阻塞队列中的任务;
2) SHUTDOWN: 不再接受新提交的任务,但可以处理阻塞队列中的任务;
3) STOP: 不再接受新提交的任务,也不处理阻塞队列中的任务;
4) TIDYING: 所有的任务都已终止;
5) TERMINATED: terminated()方法执行后进入该状态。

10. 工作线程的生命周期
在这里插入图片描述
11. 线程池的大小(maximumPoolSize)如何选定?
1) CPU密集型任务: 线程数 = CPU核数或CPU核数 + 1;
2) I/O密集型: 线程数 = CPU核数 * (1 + 平均等待时间 / 平均工作时间)。

注意: 这样做确实可以将CPU资源使用率压榨到极致。但是现实工作中,生产服务器的CPU占用率超过一定数值(如80%)就会告警,提示异常。甚至会发生OOM(Out of memory,内存溢出),主进程直接被系统kill掉。

措施: 科研环境或者测试环境可以这么设置,尽最大可能榨取系统资源的效率。生产环境下要视具体情况对上述数值进行 “打折”


后记

多线程与并发这部分内容也很多。为了便于阅读,我把这里分为了4个子章节。

在面试实战过程中,这里是最核心的知识,没有之一。4个子章节一共大约40个题目,知识点确实很多。不过也不需要一次性全部记下来,可以先以自己舒适的速度过一两遍,然后被问到不会的时候,事后来这里查,加强记忆即可。反复几遍,就会掌握的越来越牢固。

猜你喜欢

转载自blog.csdn.net/Mr_Megamind/article/details/130459113