50道多线程并发面试题

1、为什么要使用多线程

选择多线程的原因,就是因为快。举个例子:

如果要把1000块砖搬到楼顶,假设到楼顶有几个电梯,你觉得用一个电梯搬运快,还是同时用几个电梯同时搬运快呢?这个电梯就可以理解为线程。

所以,我们使用多线程就是因为: 在正确的场景下,设置恰当数目的线程,可以用来程提高序的运行速率。更专业点讲,就是充分地利用CPU和I/O的利用率,提升程序运行速率。

当然,有利就有弊,多线程场景下,我们要保证线程安全,就需要考虑加锁。加锁如果不恰当,就很很耗性能。

2. 创建线程有几种方式?

Java中创建线程主要有以下这几种方式:

  • 定义Thread类的子类,并重写该类的run方法

  • 定义Runnable接口的实现类,并重写该接口的run()方法

  • 定义Callable接口的实现类,并重写该接口的call()方法,一般配合Future使用

  • 线程池的方式

2.1 定义Thread类的子类,并重写该类的run方法

public class ThreadTest {     public static void main(String[] args) {         Thread thread = new MyThread();         thread.start();     } } classMyThread extends Thread {     @Override     public void run() {        System.out.println

2.2 定义Runnable接口的实现类,并重写该接口的run()方法

public class ThreadTest {     public static void main(String[] args) {         MyRunnable myRunnable = new MyRunnable();         Thread thread = newThread(myRunnable);         thread.start();     } } class MyRunnableimplements Runnable {     @Override     public void run() {        System.out.println

2.3 定义Callable接口的实现类,并重写该接口的call()方法

如果想要执行的线程有返回,可以使用Callable

public class ThreadTest {     public static void main(String[] args) throws ExecutionException, InterruptedException {         MyThreadCallable mc = new MyThreadCallable();         FutureTask<Integer> ft = newFutureTask<>(mc);         Thread thread = new Thread(ft);         thread.start();         System.out.println(ft.get());     } } classMyThreadCallable implements Callable {     @Override     public Stringcall()throws Exception 

2.4 线程池的方式

日常开发中,我们一般都是用线程池的方式执行异步任务。

public class ThreadTest {     public static void main(String[] args) throws Exception {         ThreadPoolExecutor executorOne = newThreadPoolExecutor(5, 5, 1,                 TimeUnit.MINUTES, newArrayBlockingQueue<Runnable>(20), new CustomizableThreadFactory("Tianluo-Thread-pool"));         executorOne.execute(() -> {             System.out.println;         });         //关闭线程池         executorOne.shutdown();     } } 复制代码

3. start()方法和run()方法的区别

其实startrun的主要区别如下:

  • start方法可以启动一个新线程,run方法只是类的一个普通方法而已,如果直接调用run方法,程序中依然只有主线程这一个线程。

  • start方法实现了多线程,而run方法没有实现多线程。

  • start不能被重复调用,而run方法可以。

  • start方法中的run代码可以不执行完,就继续执行下面的代码,也就是说进行了线程切换。然而,如果直接调用run方法,就必须等待其代码全部执行完才能继续执行下面的代码。

大家可以结合代码例子来看看哈~

public class ThreadTest {     public static void main(String[] args){         Thread t=new Thread(){             public void run(){                 pong();             }         };         t.start();         t.run();         t.run();         System.out.println"+ Thread.currentThread().getName());     }     static void pong(){         System.out.println+ Thread.currentThread().getName());     } } 

4. 线程和进程的区别

  • 进程是运行中的应用程序,线程是进程的内部的一个执行序列

  • 进程是资源分配的最小单位,线程是CPU调度的最小单位。

  • 一个进程可以有多个线程。线程又叫做轻量级进程,多个线程共享进程的资源

  • 进程间切换代价大,线程间切换代价小

  • 进程拥有资源多,线程拥有资源少地址

  • 进程是存在地址空间的,而线程本身无地址空间,线程的地址空间是包含在进程中的

举个例子:

你打开QQ,开了一个进程;打开了迅雷,也开了一个进程。

在QQ的这个进程里,传输文字开一个线程、传输语音开了一个线程、弹出对话框又开了一个线程。

所以运行某个软件,相当于开了一个进程。在这个软件运行的过程里(在这个进程里),多个工作支撑的完成QQ的运行,那么这“多个工作”分别有一个线程。

所以一个进程管着多个线程。

通俗的讲:“进程是爹妈,管着众多的线程儿子”...

5. 说一下 Runnable和 Callable有什么区别?

  • Runnable接口中的run()方法没有返回值,是void类型,它做的事情只是纯粹地去执行run()方法中的代码而已;

  • Callable接口中的call()方法是有返回值的,是一个泛型。它一般配合Future、FutureTask一起使用,用来获取异步执行的结果。

  • Callable接口call()方法允许抛出异常;而Runnable接口run()方法不能继续上抛异常;

大家可以看下它俩的API:

@FunctionalInterface public interface Callable<V> {     /**      * 支持泛型V,有返回值,允许抛出异常      */     V call() throws Exception; } @FunctionalInterface public interface Runnable {     /**      *  没有返回值,不能继续上抛异常      */     public abstract void run(); } 复制代码

为了方便大家理解,写了一个demo,小伙伴们可以看看哈:

*  @date 2022-07-11  */ public class CallableRunnableTest {     public static void main(String[] args) {         ExecutorService executorService =Executors.newFixedThreadPool(5);         Callable<String> callable =newCallable<String>() {             @Override             public String call() throws Exception {                 return          }         };         //支持泛型         Future<String> futureCallable = executorService.submit(callable);         try {            System.out.println("获取callable的返回结果:"+futureCallable.get());         } catch (InterruptedException e) {             e.printStackTrace();         } catch (ExecutionException e) {             e.printStackTrace();         }         Runnable runnable = new Runnable() {             @Override             public void run() {                 System.out.println,runnable,;             }         };         Future<?> futureRunnable = executorService.submit(runnable);         try {            System.out.println("获取runnable的返回结果:"+futureRunnable.get());         } catch (InterruptedException e) {             e.printStackTrace();         } catch (ExecutionException e) {             e.printStackTrace();         }         executorService.shutdown();     } } 

6. 聊聊volatile作用,原理

volatile关键字是Java虚拟机提供的的最轻量级的同步机制。它作为一个修饰符,用来修饰变量。它保证变量对所有线程可见性,禁止指令重排,但是不保证原子性

我们先来一起回忆下java内存模型(jmm):

  • Java虚拟机规范试图定义一种Java内存模型,来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果。

  • Java内存模型规定所有的变量都是存在主内存当中,每个线程都有自己的工作内存。这里的变量包括实例变量和静态变量,但是不包括局部变量,因为局部变量是线程私有的。

  • 线程的工作内存保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接操作主内存。并且每个线程不能访问其他线程的工作内存。

volatile变量,保证新值能立即同步回主内存,以及每次使用前立即从主内存刷新,所以我们说volatile保证了多线程操作变量的可见性。

volatile保证可见性和禁止指令重排,都跟内存屏障有关。我们来看一段volatile使用的demo代码:

 public class Singleton {      private volatile static Singleton instance;      private Singleton (){}      public static Singleton getInstance() {      if (instance == null) {          synchronized (Singleton.class) {          if (instance == null) {              instance = new Singleton();          }          }      }      return instance;      }   }   复制代码

编译后,对比有volatile关键字和没有volatile关键字时所生成的汇编代码,发现有volatile关键字修饰时,会多出一个lock addl $0x0,(%esp),即多出一个lock前缀指令,lock指令相当于一个内存屏障

lock指令相当于一个内存屏障,它保证以下这几点:

  1. 重排序时不能把后面的指令重排序到内存屏障之前的位置

  2. 将本处理器的缓存写入内存

  3. 如果是写入动作,会导致其他处理器中对应的缓存无效。

第2点和第3点就是保证volatile保证可见性的体现嘛,第1点就是禁止指令重排的体现

内存屏障四大分类:(Load 代表读取指令,Store代表写入指令)

  • 在每个volatile写操作的前面插入一个StoreStore屏障。

  • 在每个volatile写操作的后面插入一个StoreLoad屏障。

  • 在每个volatile读操作的后面插入一个LoadLoad屏障。

  • 在每个volatile读操作的后面插入一个LoadStore屏障。

有些小伙伴,可能对这个还是有点疑惑,内存屏障这玩意太抽象了。我们照着代码看下吧:

内存屏障保证前面的指令先执行,所以这就保证了禁止了指令重排啦,同时内存屏障保证缓存写入内存和其他处理器缓存失效,这也就保证了可见性,哈哈有关于volatile的底层实现,我们就讨论到这哈

7. 说说并发与并行的区别?

并发和并行最开始都是操作系统中的概念,表示的是CPU执行多个任务的方式。

  • 顺序:上一个开始执行的任务完成后,当前任务才能开始执行

  • 并发:无论上一个开始执行的任务是否完成,当前任务都可以开始执行

(即 A B 顺序执行的话,A 一定会比 B 先完成,而并发执行则不一定。)

  • 串行:有一个任务执行单元,从物理上就只能一个任务、一个任务地执行

  • 并行:有多个任务执行单元,从物理上就可以多个任务一起执行

(即在任意时间点上,串行执行时必然只有一个任务在执行,而并行则不一定。)

知乎有个很有意思的回答,大家可以看下:

  • 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。

  • 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。

  • 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是同时

来源:知乎

8.synchronized 的实现原理以及锁优化?

synchronized是Java中的关键字,是一种同步锁。synchronized关键字可以作用于方法或者代码块。

一般面试时。可以这么回答:

8.1 monitorenter、monitorexit、ACC_SYNCHRONIZED

如果synchronized作用于代码块,反编译可以看到两个指令:monitorenter、monitorexit,JVM使用monitorenter和monitorexit两个指令实现同步;如果作用synchronized作用于方法,反编译可以看到ACCSYNCHRONIZED标记,JVM通过在方法访问标识符(flags)中加入ACCSYNCHRONIZED来实现同步功能。

  • 同步代码块是通过monitorenter和monitorexit来实现,当线程执行到monitorenter的时候要先获得monitor锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。

  • 同步方法是通过中设置ACCSYNCHRONIZED标志来实现,当线程执行有ACCSYNCHRONI标志的方法,需要获得monitor锁。每个对象都与一个monitor相关联,线程可以占有或者释放monitor。

8.2 monitor监视器

monitor是什么呢?操作系统的管程(monitors)是概念原理,ObjectMonitor是它的原理实现。

在Java虚拟机(HotSpot)中,Monitor(管程)是由ObjectMonitor实现的,其主要数据结构如下:

ObjectMonitor() {     _header       = NULL;     _count        = 0; // 记录个数     _waiters      = 0,     _recursions   = 0;     _object       = NULL;     _owner        = NULL;     _WaitSet      = NULL;  // 处于wait状态的线程,会被加入到_WaitSet     _WaitSetLock  = 0 ;     _Responsible  = NULL ;     _succ         = NULL ;     _cxq          = NULL ;     FreeNext      = NULL ;     _EntryList    = NULL ;  // 处于等待锁block状态的线程,会被加入到该列表     _SpinFreq     = 0 ;     _SpinClock    = 0 ;     OwnerIsThread = 0 ;   } 复制代码

ObjectMonitor中几个关键字段的含义如图所示:

8.3 Java Monitor 的工作机理

  • 想要获取monitor的线程,首先会进入_EntryList队列。

  • 当某个线程获取到对象的monitor后,进入Owner区域,设置为当前线程,同时计数器count加1。

  • 如果线程调用了wait()方法,则会进入WaitSet队列。它会释放monitor锁,即将owner赋值为null,count自减1,进入WaitSet队列阻塞等待。

  • 如果其他线程调用 notify() / notifyAll() ,会唤醒WaitSet中的某个线程,该线程再次尝试获取monitor锁,成功即进入Owner区域。

  • 同步方法执行完毕了,线程退出临界区,会将monitor的owner设为null,并释放监视锁。

8.4 对象与monitor关联

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

  • 对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)

Mark Word 是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。

重量级锁,指向互斥量的指针。其实synchronized是重量级锁,也就是说Synchronized的对象锁,Mark Word锁标识位为10,其中指针指向的是Monitor对象的起始地址。

9. 线程有哪些状态?

线程有6个状态,分别是:New, Runnable, Blocked, Waiting, Timed_Waiting, Terminated

转换关系图如下:

  • New:线程对象创建之后、但还没有调用start()方法,就是这个状态。

 public class ThreadTest {     public static void main(String[] args) {         Thread thread = new Thread();         System.out.println(thread.getState());     } } //运行结果: NEW 复制代码

  • Runnable:它包括就绪(ready)和运行中(running)两种状态。如果调用start方法,线程就会进入Runnable状态。它表示我这个线程可以被执行啦(此时相当于ready状态),如果这个线程被调度器分配了CPU时间,那么就可以被执行(此时处于running状态)。

public class ThreadTest {     public static void main(String[] args) {         Thread thread = new Thread();         thread.start();         System.out.println(thread.getState());     } } //运行结果: RUNNABLE 复制代码

  • Blocked: 阻塞的(被同步锁或者IO锁阻塞)。表示线程阻塞于锁,线程阻塞在进入synchronized关键字修饰的方法或代码块(等待获取锁)时的状态。比如前面有一个临界区的代码需要执行,那么线程就需要等待,它就会进入这个状态。它一般是从RUNNABLE状态转化过来的。如果线程获取到锁,它将变成RUNNABLE状态

Thread t = new Thread(new Runnable {     void run() {         synchronized (lock) { // 阻塞于这里,变为Blocked状态             // dothings         }      } }); t.getState(); //新建之前,还没开始调用start方法,处于New状态 t.start(); //调用start方法,就会进入Runnable状态 复制代码

  • WAITING : 永久等待状态,进入该状态的线程需要等待其他线程做出一些特定动作(比如通知)。处于该状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。一般Object.wait

Thread t = new Thread(new Runnable {     void run() {         synchronized (lock) { // Blocked             // dothings             while (!condition) {                 lock.wait(); // into Waiting             }         }      } }); t.getState(); // New t.start(); // Runnable 复制代码

  • TIMED_WATING: 等待指定的时间重新被唤醒的状态。有一个计时器在里面计算的,最常见就是使用Thread.sleep方法触发,触发后,线程就进入了Timed_waiting状态,随后会由计时器触发,再进入Runnable状态。

Thread t = new Thread(new Runnable {     void run() {         Thread.sleep(1000); // Timed_waiting     } }); t.getState(); // New t.start(); // Runnable 复制代码

  • 终止(TERMINATED):表示该线程已经执行完成。

再来看个代码demo吧:

*/ public class ThreadTest {     private static Object object = new Object();     public static void main(String[] args) throws Exception {         Thread thread = new Thread(new Runnable() {             @Override             public void run() {                 try {                     for(int i = 0; i< 1000; i++){                         System.out.print("");                     }                     Thread.sleep(500);                     synchronized (object){                         object.wait();                     }                 } catch (InterruptedException e) {                     e.printStackTrace();                 }             }         });         Thread thread1 = new Thread(new Runnable() {             @Override             public void run() {                 try {                     synchronized (object){                         Thread.sleep(1000);                     }                     Thread.sleep(1000);                     synchronized (object){                         object.notify();                     }                 } catch (InterruptedException e) {                     e.printStackTrace();                 }             }         });                  System.out.println("1"+thread.getState());         thread.start();         thread1.start();         System.out.println("2"+thread.getState());         while (thread.isAlive()){             System.out.println("---"+thread.getState());             Thread.sleep(100);         }         System.out.println("3"+thread.getState());     } } 运行结果: 1NEW 2RUNNABLE ---RUNNABLE ---TIMED_WAITING ---TIMED_WAITING ---TIMED_WAITING ---TIMED_WAITING ---BLOCKED ---BLOCKED ---BLOCKED ---BLOCKED ---BLOCKED ---WAITING ---WAITING ---WAITING ---WAITING ---WAITING ---WAITING ---WAITING ---WAITING ---WAITING 复制代码

10. synchronized和ReentrantLock的区别?

  • Synchronized是依赖于JVM实现的,而ReenTrantLockAPI实现的。

  • Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者性能就差不多了。

  • Synchronized的使用比较方便简洁,它由编译器去保证锁的加锁和释放。而ReenTrantLock需要手工声明来加锁和释放锁,最好在finally中声明释放锁。

  • ReentrantLock可以指定是公平锁还是⾮公平锁。⽽synchronized只能是⾮公平锁。

  • ReentrantLock可响应中断、可轮回,而Synchronized是不可以响应中断的

11. wait(),notify()和suspend(),resume()之间的区别

  • wait()方法使得线程进入阻塞等待状态,并且释放锁

  • notify()唤醒一个处于等待状态的线程,它一般跟wait()方法配套使用。

  • suspend()使得线程进入阻塞状态,并且不会自动恢复,必须对应的resume()被调用,才能使得线程重新进入可执行状态。suspend()方法很容易引起死锁问题。

  • resume()方法跟suspend()方法配套使用。

suspend()不建议使用,因为suspend()方法在调用后,线程不会释放已经占有的资 源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。

12. CAS?CAS 有什么缺陷,如何解决?

CAS,全称是Compare and Swap,翻译过来就是比较并交换;

CAS涉及3个操作数,内存地址值V,预期原值A,新值B;如果内存位置的值V与预期原A值相匹配,就更新为新值B,否则不更新

CAS有什么缺陷?

  • ABA 问题

并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。

可以通过AtomicStampedReference 解决ABA问题,它,一个带有标记的原子引用类,通过控制变量值的版本来保证CAS的正确性。

  • 循环时间长开销

自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~

  • 只能保证一个变量的原子操作。

CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。可以通过这两个方式解决这个问题:1. 使用互斥锁来保证原子性; 2.将多个变量封装成对象,通过AtomicReference来保证原子性。

有兴趣的朋友可以看看我之前的这篇实战文章哈~CAS乐观锁解决并发问题的一次实践

13. 说说CountDownLatch与CyclicBarrier 区别

CountDownLatch和CyclicBarrier都用于让线程等待,达到一定条件时再运行。主要区别是:

  • CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;

  • CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。

举个例子吧:

  • CountDownLatch:假设老师跟同学约定周末在公园门口集合,等人齐了再发门票。那么,发门票(这个主线程),需要等各位同学都到齐(多个其他线程都完成),才能执行。

  • CyclicBarrier:多名短跑运动员要开始田径比赛,只有等所有运动员准备好,裁判才会鸣枪开始,这时候所有的运动员才会疾步如飞。

14. 什么是多线程环境下的伪共享

14.1 什么是伪共享?

CPU的缓存是以缓存行(cache line)为单位进行缓存的,当多个线程修改相互独立的变量,而这些变量又处于同一个缓存行时就会影响彼此的性能。这就是伪共享

现代计算机计算模型:

  • CPU执行速度比内存速度快好几个数量级,为了提高执行效率,现代计算机模型演变出CPU、缓存(L1,L2,L3),内存的模型。

  • CPU执行运算时,如先从L1缓存查询数据,找不到再去L2缓存找,依次类推,直到在内存获取到数据。

  • 为了避免频繁从内存获取数据,聪明的科学家设计出缓存行,缓存行大小为64字节。

也正是因为缓存行的存在,就导致了伪共享问题,如图所示:

假设数据a、b被加载到同一个缓存行。

  • 当线程1修改了a的值,这时候CPU1就会通知其他CPU核,当前缓存行(Cache line)已经失效。

  • 这时候,如果线程2发起修改b,因为缓存行已经失效了,所以「core2 这时会重新从主内存中读取该 Cache line 数据」。读完后,因为它要修改b的值,那么CPU2就通知其他CPU核,当前缓存行(Cache line)又已经失效。

  • 酱紫,如果同一个Cache line的内容被多个线程读写,就很容易产生相互竞争,频繁回写主内存,会大大降低性能。

14.2 如何解决伪共享问题

既然伪共享是因为相互独立的变量存储到相同的Cache line导致的,一个缓存行大小是64字节。那么,我们就可以使用空间换时间的方法,即数据填充的方式,把独立的变量分散到不同的Cache line~

来看个例子:

 */ public class FalseShareTest  {     public static void main(String[] args) throws InterruptedException {         Rectangle rectangle = new Rectangle();         long beginTime = System.currentTimeMillis();         Thread thread1 = new Thread(() -> {             for (int i = 0; i < 100000000; i++) {                 rectangle.a = rectangle.a + 1;             }         });         Thread thread2 = new Thread(() -> {             for (int i = 0; i < 100000000; i++) {                 rectangle.b = rectangle.b + 1;             }         });         thread1.start();         thread2.start();         thread1.join();         thread2.join();         System.out.println("执行时间" + (System.currentTimeMillis() - beginTime));     } } class Rectangle {     volatile long a;     volatile long b; } //运行结果: 执行时间2815 复制代码

一个long类型是8字节,我们在变量a和b之间不上7个long类型变量呢,输出结果是啥呢?如下:

class Rectangle {     volatile long a;     long a1,a2,a3,a4,a5,a6,a7;     volatile long b; } //运行结果 执行时间1113 复制代码

可以发现利用填充数据的方式,让读写的变量分割到不同缓存行,可以很好挺高性能~

15. Fork/Join框架的理解

Fork/Join框架是Java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

Fork/Join框架需要理解两个点,「分而治之」和「工作窃取算法」。

分而治之

以上Fork/Join框架的定义,就是分而治之思想的体现啦

工作窃取算法

把大任务拆分成小任务,放到不同队列执行,交由不同的线程分别执行时。有的线程优先把自己负责的任务执行完了,其他线程还在慢慢悠悠处理自己的任务,这时候为了充分提高效率,就需要工作盗窃算法啦~

工作盗窃算法就是,「某个线程从其他队列中窃取任务进行执行的过程」。一般就是指做得快的线程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程各在一端。

16. 聊聊ThreadLocal原理?

17. TreadLocal为什么会导致内存泄漏呢?

17.1 弱引用导致的内存泄漏呢?

17.2 key是弱引用,GC回收会影响ThreadLocal的正常工作嘛?

17.3 ThreadLocal内存泄漏的demo

18 为什么ThreadLocalMap 的 key是弱引用,设计理念是?

19. 如何保证父子线程间的共享ThreadLocal数据

20. 如何保证多线程下 i++ 结果正确?

21. 如何检测死锁?怎么预防死锁?死锁四个必要条件

22. 如果线程过多,会怎样?

23. 聊聊happens-before原则

24. 如何实现两个线程间共享数据

25. LockSupport作用是?

26  线程池如何调优,如何确认最佳线程数?

27. 为什么要用线程池?

28. Java的线程池执行原理

29. 聊聊线程池的核心参数

30.当提交新任务时,异常如何处理?

31. AQS组件,实现原理

31.1 state 状态的维护

31.2 CLH队列

31.3 ConditionObject通知

31.4 模板方法设计模式列方法可以实现等待/通知模式。而Lock呢?

31.5 独占与共享模式。

31.6 自定义同步器

32 Semaphore原理

32.1 Semaphore使用demo

32.2 Semaphore原理

33 synchronized做了哪些优化?什么是偏向锁?什么是自旋锁?锁租化?

34 什么是上下文切换?

35.为什么wait(),notify(),notifyAll()在对象中,而不在Thread类中

36. 线程池中 submit()和 execute()方法有什么区别?

37 AtomicInteger 的原理?

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

39. shutdown() 和 shutdownNow()的区别

40 说说几种常见的线程池及使用场景?

40.1 newFixedThreadPool

40.2 newCachedThreadPool

40.3 newSingleThreadExecutor 单线程的线程池

40.4 newScheduledThreadPool

41 什么是FutureTask

42 java中interrupt(),interrupted()和isInterrupted()的区别

43  有三个线程T1,T2,T3,怎么确保它们按顺序执行

44 有哪些阻塞队列

45 Java中ConcurrentHashMap的并发度是什么?

46 Java线程有哪些常用的调度方法?

46.1 线程休眠

46.2 线程中断

46.3 线程等待

46.4 线程让步

46.5 线程通知

47. ReentrantLock的加锁原理

47.1 ReentrantLock使用的模板

47.2 什么是非公平锁,什么是公平锁?

47.3 lock()加锁流程

48. 线程间的通讯方式

48.1 volatile和synchronized关键字

48.2 等待/通知机制

48.3 管道输入/输出流

48.4 join()方法

48.5 ThreadLocal

49  写出3条你遵循的多线程最佳实践

50. 为什么阿里发布的 Java开发手册中强制线程池不允许使用 Executors 去创建?

猜你喜欢

转载自blog.csdn.net/l688899886/article/details/127079247