2021金九银十的面试月,大家不可错过的这些高并发的面试题,掌握就能拿20万+年薪

本文详细介绍了Java并发编程面试中常见的知识点,包括锁的分类(乐观锁、悲观锁、共享锁、独享锁、公平锁、非公平锁、互斥锁、读写锁、可重入锁、自旋锁、分段锁),线程的状态(新建、就绪/运行、阻塞、等待、超时等待、终止),synchronized与Lock的区别,以及ReentrantLock、ThreadLocal、并发集合(Vector、Hashtable、ConcurrentHashMap)等。此外,还讨论了线程池的使用,如Executors提供的四种线程池,以及自定义线程池,还有并发问题(脏读、不可重复读、幻读)和事务的ACID特性。文章最后提到了并发控制工具,如Callable、Future、CompletionService,以及并发测试中的多线程创建方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试题和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~,需要更全面的Word文档的请点主页私信【学习】领取免费的哦

本文目录多线程与并发面试问题概览:

2021经典面试—多线程与并发

本文目录

java中你知道哪些锁?

问题回答

  1. 乐观锁/悲观锁
  2. 共享锁/独享锁
  3. 公平锁/非公平锁
  4. 互斥锁/读写锁
  5. 可重入锁
  6. 自旋锁
  7. 分段锁
  8. 偏向锁/轻量级锁/重量级锁

2021经典面试—多线程与并发

锁的分类

Java线程的状态或者生命周期?

问题回答

  1. Java的线程状态被定义在公共枚举类java.lang.Thread.state中。一种有六种状态
  • 新建(NEW):表示线程新建出来还没有被启动的状态,比如:Thread t = new MyThread();
  • 就绪/运行(RUNNABLE):该状态包含了经典线程模型的两种状态:就绪(Ready)、运行(Running):
  • 阻塞(BLOCKED):通常与锁有关系,表示线程正在获取有锁控制的资源,比如进入synchronized代码块,获取ReentryLock等;发起阻塞式IO也会阻塞,比如字符流字节流操作。
  • 等待(WAITING):线程在等待某种资源就绪。
  • 超时等待(TIMED_WAIT):线程进入条件和等待类似,但是它调用的是带有超时时间的方法。
  • 终止(TERMINATED):线程正常退出或异常退出后,就处于终结状态。也可以叫线程的死亡。

看下源码

在Java.lang.Thread里,有个内部枚举: State,一个线程在某一时刻可以是State里的一种状态

2021经典面试—多线程与并发

State状态

看图理解

2021经典面试—多线程与并发

线程的状态

哪些情况或者方法可以进入等待状态?

  1. 当一个线程执行了Object.wait()的时候,它一定在等待另一个线程执行Object.notify()或者Object.notifyAll()。
  2. 一个线程thread,其在主线程中被执行了thread.join()的时候,主线程即会等待该线程执行完成。
  3. 当一个线程执行了LockSupport.park()的时候,其在等待执行LockSupport.unpark(thread)。

哪些情况或者方法可以进入超时等待状态?

问题回答

该状态不同于WAITING,它可以在指定的时间后自行返回

  1. Object.wait(long)
  2. Thread.join(long)
  3. LockSupport.parkNanos()
  4. LockSupport.parkUntil()
  5. Thread.sleep(long)

synchronized 与lock区别?

问题回答

  1. lock是一个接口,而synchronized是java的一个关键字
  2. synchronized异常会释放锁,lock异常不会释放,所以一般try catch包起来,finally中写入unlock,避免死锁。
  3. Lock可以提高多个线程进行读操作的效率
  4. synchronized关键字,可以放代码块,实例方法,静态方法,类上
  5. lock一般使用ReentrantLock类作为锁,配合lock()和unlock()方法。在finally块中写unlock()以防死锁。
  6. jdk1.6之前synchronized低效。jdk1.6之后synchronized高效。

synchronized 与ReentrantLock区别?

问题回答

  1. synchronized依赖JVM实现,ReentrantLock是JDK实现的。synchronized是内置锁,只要在代码开始的地方加synchronized,代码结束会自动释放。Lock必须手动加锁,手动释放锁。
  2. ReenTrantLock比synchronized增加了一些高级功能。synchronized代码量少,自动化,但扩展性低,不够灵活;ReentrantLock扩展性好,灵活,但代码量相对多。
  3. 两者都是可重入锁。都是互斥锁。
  4. synchronized是非公平锁,ReentrantLock可以指定是公平锁还是非公平锁。

synchronized 与ThreadLocal区别?

问题回答

  1. 都是为了解决多线程中相同变量的访问冲突问题。
  2. Synchronized同步机制,提供一份变量,让不同的线程排队访问。
  3. ThreadLocal关键字,为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
  4. ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

看代码

2021经典面试—多线程与并发

ThreadLocal

2021经典面试—多线程与并发

synchronized

synchronized 与volatile区别?

问题回答

  1. volatile是一个类型修饰符(type specifier)。
  2. volatile,它能够使变量在值发生改变时能尽快地让其他线程知道。
  3. 关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且只能修改变量,而synchronized可以修饰方法,以及代码块。
  4. 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞
  5. volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步
  6. 关键字volatile解决的下变量在多线程之间的可见性;而synchronized解决的是多线程之间资源同步问题

Thread类中的start()和run()方法有什么区别?

  1. 通过调用线程类的start()方法来启动一个线程,使线程处于就绪状态,即可以被JVM来调度执行,在调度过程中,JVM通过调用线程类的run()方法来完成实际的业务逻辑,当run()方法结束后,此线程就会终止。
  2. 如果直接调用线程类的run()方法,会被当作一个普通的函数调用,程序中仍然只有主线程这一个线程。即start()方法能够异步地调用run()方法,但是直接调用run()方法却是同步的,无法达到多线程的目的。
  3. 因此,只有通过调用线程类的start()方法才能达到多线程的目的。

事务的隔离级别及引发的问题?

问题回答

  1. 4个隔离级别:读未提交、读已提交、可重复读、串行化
  2. 分别怎么理解呢?
  • 读未提交(READ UNCOMMITTED),事务中的修改,即使没有提交,对其它事务也是可见的。
  • 读已提交(READ COMMITTED),一个事务能读取已经提交的事务所做的修改,不能读取未提交的事务所做的修改。也就是事务未提交之前,对其他事务不可见。
  • 可重复读(REPEATABLE READ),保证在同一个事务中多次读取同样数据的结果是一样的。
  • 串行化(SERIALIZABLE),强制事务串行执行。

3.读已提交是sql server的默认隔离级别。

可重复读是mysql的默认隔离级别。

简要回答

  1. 4个隔离级别,读未提交、读已提交、可重复读、可串行化。
  2. 读未提交(READ UNCOMMITTED),事务提交与否都可见,引发脏读、不可重复读、幻读。
  3. 读已提交(READ COMMITTED),已提交的事务可见,引发不可重复读、幻读。
  4. 可重复读(REPEATABLE READ),多次读取,数据一致,引发幻读。
  5. 串行化(SERIALIZABLE),串行执行。

2021经典面试—多线程与并发

事务隔离级别和引发的问题

大多数数据库的默认隔离级别为: Read Commited,如Sql Server , Oracle。

少数数据库默认的隔离级别为Repeatable Read, 如MySQL InnoDB存储引擎。

理解脏读、不可重复读、幻读

脏读:读到未提交的数据。

2021经典面试—多线程与并发

脏读

不可重复读:重点是修改,同样的条件, 你读取过的数据, 再次读取出来发现值不一样了。

2021经典面试—多线程与并发

不可重复读

幻读:重点在于新增或者删除,同样的条件, 第1次和第2次读出来的记录数不一样。

2021经典面试—多线程与并发

幻读

简单理解4个隔离级别

  • 读未提交,比如事务A和事务B同时进行,事务A在整个执行阶段,会将某数据的值从1开始一直加到10,然后进行事务提交。此时,事务B能够读取事务A操作过程中的未提交的数据(1、2、3、4、5、6...10)。
  • 读已提交,事务A在整个执行阶段,会将某数据的值从1开始一直加到10,然后进行事务提交。此时,事务B只能读取到最终的10。
  • 可重复读,事务B开始读取到的是某个值是0,事务A对值进行修改提交多次,事务B读取到的依然是0。多次读取,结果一致。
  • 串行化,是最严格的事务隔离级别,它要求所有事务被串行执行,一个事务没有结束,另外的事务没法继续。

案列演示

读未提交

2021经典面试—多线程与并发

读未提交

读已提交

2021经典面试—多线程与并发

读已提交

可重复读

2021经典面试—多线程与并发

可重复读

串行化

2021经典面试—多线程与并发

串行化1

什么是线程安全,java如何保证线程安全?

问题回答

  1. 在多线程环境中,能永远保证程序的正确性。执行结果不存在二义性。说白了,运行多少次结果都是一致的。
  2. 换种说法,当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
  3. 使用synchronized关键字和使用锁。

介绍一下线程池?结果

问题回答

  1. 线程池就是预先创建一些线程,它们的集合称为线程池。
  2. 线程池可以很好地提高性能,在系统启动时即创建大量空闲的线程,程序将一个task给到线程池,线程池就会启动一条线程来执行这个任务,执行结束后,该线程不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
  3. 线程的创建和销毁比较消耗时间,线程池可以避免这个问题。
  4. Executors是jdk1.5之后的一个新类,提供了一些静态方法,帮助我们方便的生成一些常见的线程池
  • newSingleThreadExecutor:创建一个单线程化的Executor。
  • newFixedThreadPool:创建一个固定大小的线程池。
  • newCachedThreadPool:创建一个可缓存的线程池
  • newScheduleThreadPool:创建一个定长的线程池,可以周期性执行任务。

我们还可以使用ThreadPoolExecutor自己定义线程池,弄懂它的构造参数即可

  • int corePoolSize,//核心池的大小
  • int maximumPoolSize,//线程池最大线程数
  • long keepAliveTime,//保持时间/额外线程的存活时间
  • TimeUnit unit,//时间单位
  • BlockingQueue workQueue,//任务队列
  • ThreadFactory threadFactory,//线程工厂
  • RejectedExecutionHandler handler //异常的捕捉器

简要回答

  1. 线程池就是预先创建一些线程
  2. 线程池可以很好地提高性能
  3. 线程池可以避免线程的频繁创建和销毁
  4. Executors可以创建常见的4种线程(单线程池、固定大小的、可缓存的、可周期性执行任务的)。
  5. 可以通过ThreadPoolExecutor自己定义线程池。

看看Excutors中的方法

2021经典面试—多线程与并发

Excutors中的方法

看看ThreadPoolExecutor的构造函数

2021经典面试—多线程与并发

ThreadPoolExecutor的构造函数

常见的线程池有哪些?

问题回答

  1. Executors是jdk1.5之后的一个新类,提供了一些静态方法,帮助我们方便的生成一些常见的线程池
  2. 单线程线程池,通过newSingleThreadExecutor()创建
  3. 固定大小的线程池,通过newFixedThreadPool()创建
  4. 可缓存的线程池,通过newCachedThreadPool()创建
  5. 可周期性执行任务的线程池,通过newScheduleThreadPool()创建

看下Excutors的使用

2021经典面试—多线程与并发

Excutors的使用

几个线程的区别?

newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

这种类型的线程池特点是:

  • 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
  • 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
  • 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。

newFixedThreadPool

创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

newSingleThreadExecutor

创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

newScheduleThreadPool

创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

同步和异步有何异同?

问题回答

  1. 同步发了指令,会等待返回,然后再发送下一个。
  2. 异步发了指令,不会等待返回,随时可以再发送下一个请求
  3. 同步可以避免出现死锁,读脏数据的发生
  4. 异步则是可以提高效率
  5. 实现同步的机制主要有临界区、互斥、信号量和事件

哪些集合是线程安全?

问题回答

  1. Vector:就比Arraylist多了个同步化机制(线程安全)。
  2. Hashtable:就比Hashmap多了个线程安全。
  3. ConcurrentHashMap:是一种高效但是线程安全的集合。

如何异步获取多线程返回的数据?

问题包含

说一下Callable这个接口的理解?

说一下Future接口的理解?

说一下FutureTask类的理解?

说一下CompletionService接口的理解?

问题回答

  1. 通过Callable+Future,Callable负责执行返回,Future负责接收。Callable接口对象可以交给ExecutorService的submit方法去执行。
  2. 通过Callable+FutureTask,Callable负责执行返回,FutureTask负责接收。FutureTask同时实现了Runnable和Callable接口,可以给到ExecutorService的submit方法和Thread去执行。
  3. 通过CompletionService,jdk1.8之后提供了完成服务CompletionService,可以实现这样的需求。
  4. 注意,实现Runnable接口任务执行结束后无法获取执行结果。

Callable有返回值,Runnable没有返回值

2021经典面试—多线程与并发

Callable有返回值,Runnable没有返回值

看看Future接口

看看所有方法

2021经典面试—多线程与并发

Future接口方法

重点看看get方法

2021经典面试—多线程与并发

get方法

总结下:

V get() 获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。

V get(Long timeout , TimeUnit unit) :获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。

boolean isDone() :判断任务是否完成如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。future.isDone()

boolean isCanceller() 如果任务完成前被取消,则返回true。 future.isCanceller()

boolean cancel(boolean mayInterruptRunning) 如果任务还没开始,执行cancel(...)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经完成,执行cancel(...)方法将返回false。mayInterruptRunning参数表示是否中断执行中的线程。

通过方法分析我们也知道实际上Future提供了3种功能:

(1)能够中断执行中的任务

(2)判断任务是否执行完成

(3)获取任务执行完成后的结果。

看看ExecutorService的submit方法

2021经典面试—多线程与并发

submit方法

注意:

Callable接口的call方法有返回值,Runnable接口的run方法没有返回值

submit(Runnable):Future<?> ,返回的Future无法获取返回值。submit(Runnable, T):Future,返回的Future无法获取返回值。

看看FutureTask

看FutureTask定义

2021经典面试—多线程与并发

FutureTask定义

看RunnableFuture定义,同时实现了Runnable、Future

2021经典面试—多线程与并发

RunnableFuture定义

所以,FutureTask

ExecutorService的submit方法接收Runnable和Callable,所以接收FutureTask。

Thread只支持Runnable,所以也支持FutureTask。FutureTask让Thread也可以有返回值的效果。

看代码

Callable接口配合ExecutorService的submit方法

2021经典面试—多线程与并发

AsynTask1.java

结果

2021经典面试—多线程与并发

结果

Callable接口配合FutureTask,ExecutorService的submit方法去执行

2021经典面试—多线程与并发

AsynTask2.java

结果

2021经典面试—多线程与并发

结果

Callable接口配合FutureTask,给Thread类去执行

2021经典面试—多线程与并发

AsynTask3.java

2021经典面试—多线程与并发

结果

CompletionService配合Callable

2021经典面试—多线程与并发

AsynTask4

结果

2021经典面试—多线程与并发

结果

CompletionService和Future的区别呢?

  • Future获取结果,一个一个地取,一个取完了,再取另外一个,就会等待
  • CompletionService,任意一个线程有返回,就立马取出
 public class AsynTask5 {
     public static void main(String[] args) {
         asynCompletionService();
         System.out.println("----------------分割线-------------------");
         asynFuture();
         System.out.println("主线程执行完毕");
    }
 
     private static void asynCompletionService() {
         try {
             //使用ExecutorService
             ExecutorService executorService = Executors.newCachedThreadPool();
 
             // 构建完成服务
             CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(executorService);
             for (int i = 1; i <= 5; i++) {
                 //提交任务
                 completionService.submit(new HandleFuture<>(i));
            }
             //获取结果,一个一个阻塞的取出。这中间肯定会浪费一定的时间在等待上
             for (int i = 1; i <= 5; i++) {
                 Integer result = completionService.take().get();
                 System.out.println("结果:" + result);
            }
        } catch (InterruptedException e) {
             e.printStackTrace();
        } catch (ExecutionException e) {
             e.printStackTrace();
        }
    }
 
     private static void asynFuture() {
         try {
             //使用ExecutorService
             ExecutorService executorService = Executors.newCachedThreadPool();
 
             //Future列表
             List<Future<Integer>> result = new ArrayList<Future<Integer>>();
             for (int i = 1; i <= 5; i++) {
                 //提交任务
                 Future<Integer> submit = executorService.submit(new HandleFuture<>(i));
                 result.add(submit);
            }
             //获取结果,输出和线程的放入顺序无关系。每一个线程执行成功后,立刻就输出
             for (Future<Integer> integerFuture : result) {
                 Integer integer = integerFuture.get();
                 System.out.println("结果:" + integer);
            }
        } catch (InterruptedException e) {
             e.printStackTrace();
        } catch (ExecutionException e) {
             e.printStackTrace();
        }
    }
 }
 
 class HandleFuture<Integer> implements Callable<Integer> {
     private Integer num;
 
     public HandleFuture(Integer num) {
         this.num = num;
    }
 
     @Override
     public Integer call() throws Exception {
         Thread.sleep(2 * 1000l);
         System.out.println(Thread.currentThread().getName());
         return num;
    }
 
 }

结果

2021经典面试—多线程与并发

如何自定义线程池?

corePoolSize:核心池的大小

  • 默认情况下,在创建了线程池之后,线程池中的线程数为0
  • 当有任务到来后,如果线程池中存活的线程数小于corePoolSize,则创建一个线程。

maximumPoolSize:线程池最大线程数

  • 线程池中允许的最大线程数,这个参数表示了线程池中最多能创建的线程数量。
  • 当任务数量比corePoolSize大时,任务添加到workQueue
  • 当workQueue满了,将继续创建线程以处理任务。
  • maximumPoolSize表示当wordQueue满了,线程池中最多可以创建的线程数量。

keepAliveTime:

  • 当线程池处于空闲状态时,超过keepAliveTime时间之后,空闲的线程会被终止。
  • 只有当线程池中的线程数大于corePoolSize时,这个参数才会起作用,但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
  • 当线程数大于corePoolSize时,如果一个线程的空闲时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。

unit:参数keepAliveTime的时间单位,有7种取值

  • TimeUnit.DAYS; //天
  • TimeUnit.HOURS; //小时
  • TimeUnit.MINUTES; //分钟
  • TimeUnit.SECONDS; //秒
  • TimeUnit.MILLISECONDS; //毫秒
  • TimeUnit.MICROSECONDS; //微妙
  • TimeUnit.NANOSECONDS; //纳秒

workQueue : 任务队列,阻塞队列,存储提交的等待任务。常见子类有:

  • ArrayBlockingQueue;
  • LinkedBlockingQueue;
  • SynchronousQueue;

threadFactory : 线程工厂,指定创建线程的工厂

handler : 任务队列添加异常的捕捉器,当任务超出线程池范围和队列容量时,采取何种拒绝策略。参考 RejectedExecutionHandler,常见实现类。

  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

看一个自定义线程的例子

2021经典面试—多线程与并发

简单演示

执行结果

2021经典面试—多线程与并发

执行结果

工作中哪些地方使用了多线程?

问题回答

  1. 一般业务,web层--> service层 -->dao --> sql基本用不到多线程
  2. 数据量很大(1000w级别、TB级别)的I/O操作,可以考虑多线程

举一些例子

  1. 自己做并发测试的时候,假如想写想模拟3000个并发请求。
  2. 多线程下单抢单,假如支持5000人的并发下单。
  3. 多线程写入mysql,假如有1000w条数据要入库。
  4. 多线程写入redis,假如有1000w的数据要存入redis。
  5. 多线程导入ES索引,假如有1000w的数据要添加到ES索引。
  6. poi多线程导出,假如xls里面有10w的数据需要导出。
  7. poi多线程导入,假如有10w条数据需要导入到xls。
  8. 多线程发送邮件,假如有10w用户需要发送邮件。
  9. 多线程发送短信,假如有10w用户需要发送邮件。

10.多线程备份日志,假如10tb日志文件要备份。

11.多线程验证数据,比如验证url是否存在,假如有100w个url

数据并发操作可能的问题?

问题回答

  1. 丢失的修改
  2. 不可重复读,读第二次,数据就不对了
  3. 读脏数据
  4. 幻影读

脏读:指事务读到了其它事务做了修改而未提交的数据

不可重复读:不能重复读两次,读两次就不同了

幻读:同一个事务T1在两个不同的时间段t执行同一条查询语句得到的记录数量不同

2021经典面试—多线程与并发

消息等待通知wait/notify具体的应用

问题回答

  1. 一个线程修改了一个对象的值,另外一个线程需要感知到这个变化
  2. Java中我们使用的对象锁以及wait/notify方法进行线程通信
  3. 等待方遵循的原则:
    获取对象的锁不满足条件 就调用wait()方法条件满足继续执行
  4. 通知方原则:
    获取对象的锁改变条件, 然后notify

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

问题回答

  1. execute() 参数 Runnable ;
  2. submit() 参数 (Runnable) 或 (Runnable 和 结果 T) 或 (Callable);
  3. execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。
  4. submit(Callable x)有返回值,返回一个Future类的对象。
  5. Future对象
  • 通过get方法,获取线程返回结果
  • 通过get方法,接收任务执行时候抛出的异常
  • 通过isDone方法,可以判断线程是否执行完成。

看execute和submit方法的定义

2021经典面试—多线程与并发

看submit方法的定义

2021经典面试—多线程与并发

看看Future的get方法

可以接收结果,可以接收任务执行时候抛出的异常

2021经典面试—多线程与并发

看看Future的方法

2021经典面试—多线程与并发

线程的创建方式有哪些?

问题回答

  1. 继承Thread类实现
  2. 实现Runnable接口方式
  3. 实现Callable接口方式
  4. 其中前两种比较常用。但是,需要有返回值需要实现Callable接口。

继承Thread类实现

/**
  * 继承Thread类,并重写run方法
  */
 public class MyThread extends Thread {
     @Override
     public void run() {
         super.run();
         System.out.println("MyThread...");
    }
 }

实现Runnable接口方式

 /**
  * 实现Runnable接口,并重写run方法
  */
 public class MyRunnable implements Runnable{
 
     @Override
     public void run() {
         System.out.println("MyRunnable...");
    }
 }

   MyRunnable runnable=new MyRunnable();
   Thread thread=new Thread(runnable);
   thread.start();

调用

   MyRunnable runnable=new MyRunnable();
   Thread thread=new Thread(runnable);
   thread.start();

实现Callable接口方式

/**
  * 实现Callable接口,并重写call方法
  */
 public class MyCallable implements Callable<String>{
 
     @Override
     public String call() throws Exception {
         return "MyCallable...";
    }
 }

调用

 //创建和调用
 MyCallable callable=new MyCallable();
 ExecutorService eService=Executors.newSingleThreadExecutor();
 Future<String> future=eService.submit(callable);
     
 //获取返回结果
 try {
    String result=future.get();
    System.out.println(result);
 } catch (Exception e) {
    e.printStackTrace();
 }

注意

  • callable需要配合线程池使用
  • callable比runnable功能复杂一些
    Callable的call方法有返回值并且可以抛异常,而Runnable的run方法就没有返回值也没有抛异常,也就是可以知道执行线程的时候除了什么错误。
  • Callable运行后可以拿到一个Future对象,这个对象表示异步计算结果,可以从通过Future的get方法获取到call方法返回的结果。但要注意调用Future的get方法时,当前线程会阻塞,直到call方法返回结果。

说一下CAS锁机制?

问题回答

  1. CAS(Compare and Swap 比较并交换),是一种无锁算法,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
  2. CAS算法涉及到三个操作数
  • 需要读写的内存位置(V)
  • 进行比较的预期原值(A)
  • 拟写入的新值(B)

3.如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。

说一下ConcurrentHashMap?

问题回答

  1. ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现
  2. JDK1.7 ConcurrentHashMap 由Segment 数组+HashEntry 组成,也就是数组+链表。
  3. JDK1.7 ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。支持 N 个 Segment 这么多次数的并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
  4. JDK1.8抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性
  5. JDK1.8将HashEntry改为了Node,和 1.8 HashMap 结构类似,当链表节点数超过指定阈值的话,会转换成红黑树的。

看看图片

jdk 1.7 ConcurrentHashMap

2021经典面试—多线程与并发

jdk 1.7 ConcurrentHashMap

jdk1.8 ConcurrentHashMap

2021经典面试—多线程与并发

jdk1.8 ConcurrentHashMap

说一下Threadlocal关键字?

问题回答

  1. 线程本地变量,可以为变量在每个线程中都创建一个副本,使每个线程都可以访问自己内部的副本变量

说一下乐观锁和悲观锁的区别?

问题回答

  1. 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。
    synchronized、Lock属于悲观锁。Lock有三种实现类:ReentrantLock、ReadLock(读锁)和WriteLock(写锁)。
  2. 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁。
  3. CAS属于乐观锁。
  4. 悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
  5. 悲观锁对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
  6. 乐观锁不会上锁,在更新时会判断数据有没有被修改,一般会使用“数据版本机制”或“CAS操作”来实现。

数据版本机制

实现数据版本一般有两种,第一种是使用版本号,第二种是使用时间戳。以版本号方式为例。

版本号方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。

当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。核心SQL代码:

 update table set xxx=#{xxx}, version=version+1 where id=#{id} and version=#{version};

CAS操作

CAS(Compare and Swap 比较并交换),当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

 CAS操作中包含三个操作数——需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。

说一下事务特性?

问题回答

  1. 事务特性指的就是ACID。
  2. 分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
  3. 分别解释下:
  • 原子性:原子性是指事务包含的操作要么全部成功,要么全部失败。因此事务的操作成功就必须要完全应用到数据库。
  • 一致性:一致性强调的是数据是一致性的。假设用户A和用户B两者的钱加起来一共是5000,那么不管A还是B如何转账,转几次账,事务结束后两个用户的钱加起来应该还是5000,这就是事务的一致性。
  • 隔离性:当多个用户并发访问数据库时,多个并发事务是相互隔离的。事务之间不能相互干扰。
  • 持久性:一个事务一旦被提交了,那么对数据库中的数据改变是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作

简要理解

  1. 也就是acid。
  2. 分别是原子性、一致性、隔离性、持久性。
  3. 原子性,要么同时成功要么同时失败。
  4. 一致性,数据应该是一致的。
  5. 隔离性,多个并发事务是相互隔离的。
  6. 持久性,事务提交,对数据的改变是永久的。

关于几个特性,补充理解

  • 原子性,算是事务最基本的特性了。
  • 一致性,感觉像事务的目标,其他的三个特性都是为了保证数据一致性存在的。
  • 隔离性,为了保证并发情况下的一致性而引入,并发状态下单靠原子性不能完全解决一致性的问题,在多个事务并发进行的情况下,即使保证了每个事务的原子性,仍然可能导致数据不一致。比如,事务1需要将100元转入帐号A:先读取帐号A的值,然后在这个值上加上100。但是,在这两个操作之间,另一个事务2将100元转入帐号A,为它增加了100元。那么最后的结果应该是A增加了200元。但事实上,事务1最终完成后,帐号A只增加了100元,因为事务1覆盖了事务2的修改结果。
  • 持久性,好理解,事务一旦提交,对数据库的影响是永久的,保证所有操作都是有效。

看图

2021经典面试—多线程与并发

事务特性

说一下互斥锁/读写锁?

问题回答

  1. 上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁,就是具体的实现。
  2. 一次只能一个线程拥有互斥锁,其他线程只有等待
  3. 互斥锁在Java中的具体实现就是ReentrantLock。
  4. 读写锁在Java中的具体实现就是ReadWriteLock。

说一下偏向锁/轻量级锁/重量级锁?

问题回答

  1. 这三种锁是指锁状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
  2. 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
  3. 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
  4. 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。

说一下公平锁/非公平锁?

问题回答

  1. 公平锁是指多个线程按照申请锁顺序来获取锁。
  2. 非公平锁是指多个线程获取锁的顺序并不是按照申请锁顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
  3. 对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
  4. 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

说一下分段锁?

问题回答

  1. 分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
  2. 我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
  3. 当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对分段加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行插入。
  4. 但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
  5. 分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

说一下可重入锁?

问题回答

  1. 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
  2. 对于Java ReetrantLock而言,从名字就可以看出是一个重入锁,其名字是Re entrant Lock 重新进入锁。
  3. 对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

看代码理解可重入锁

2021经典面试—多线程与并发

可重入锁

上面的代码就是一个可重入锁的一个特点。如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。

说一下对象锁和类锁?

问题回答

  1. java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,实际区别大
  2. 对象锁是用于对象实例方法,或者一个对象实例上的
  3. 类锁是用于类的静态方法或者一个类的class对象上的。
  4. 我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的

说一下死锁?

问题回答

  1. Java发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。即线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。

说一下独享锁/共享锁?

问题回答

  1. 独享锁是指该锁一次只能被一个线程所持有。
  2. 共享锁是指该锁可被多个线程所持有。
  3. 对于Java ReentrantLock(重入锁)而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
  4. 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
  5. 独享锁与共享锁也是通过AQS(AbstractQuenedSynchronizer抽象的队列式同步器)来实现的,通过实现不同的方法,来实现独享或者共享。
  6. 对于Synchronized而言,当然是独享锁。

说一下自旋锁?

问题回答

  1. 在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁
  2. 优点是减少线程上下文切换的消耗
  3. 缺点是循环会消耗CPU。

需要更全面的Word文档的请点主页私信【学习】领取免费的哦

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

maxiao112

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值