Java小白入门 —— 并发编程热点面试题(2020)

Java小白入门 —— 并发编程热点面试题(2020)

一. synchronized 相关问题:

1. synchronized 锁使用的方法?

	 方法锁:修饰实例方法,修饰静态方法,修饰代码块;
	 对象锁:synchronized(this);
	 类锁:synchronied(类.class)。

2. 说一下 synchronized 原理?

在被 synchronized 修饰的程序块中,编译器在编译时会生成 monitorenter 和 monitorexit 两个字节码指令。当虚拟机执行到 monitorenter 时,首先会尝试获取对象的锁,若该对象没有锁定,或该当前线程已获取到该对象的锁,就把锁的计数器+1;当执行到 monitorexit 指令时,把锁的计数器 -1;当计数器为 0 时,锁就会被释放。如果获取对象失败,那么当前线程就一直处于阻塞状态,直到对象锁被另一个线程释放。Java中 synchronized 关键字通过在对象头处设置标记,达到获取锁和释放锁。

3. 对于刚提到的锁,你对锁的概念是怎么样的?如何确定对象的锁?

“ 锁 ” 的 本 质 其 实 是 monitorenter 和 monitorexit 字 节 码 指 令 的 一个 Reference 类 型 的 参 数 , 即 要 锁 定 和 解 锁 的 对 象 。可以分为两种情况确定:

  1. 如 果 Synchronized 明 确 指 定 了 锁 对 象 , 比 如 Synchronized( 变 量名 ) 、Synchronized(this) 等 , 说 明 加 解 锁 对 象 为 该 对 象 。
  2. 如 果 没 有 明 确 指 定 :
    若 Synchronized 修 饰 的 方 法 为 非 静 态 方 法 , 表 示 此 方 法 对 应 的 对 象 为锁 对 象 ;
    若 Synchronized 修 饰 的 方 法 为 静 态 方 法 , 则 表 示 此 方 法 对 应 的 类 对 象为 锁 对 象 。

当一个对象被锁住时,对象中所有被 synchronized 修饰的方法都将进入阻塞状态,而未被 synchronized 修饰的方法都不会受到锁的影响。

3. 什么是可重入性?为什么说 synchronized 锁是可重入锁?

可重入性的意思是:当一个线程在持有一个锁时,它的内部是否能再次申请锁。如果当前线程已经持有锁,且其内部还能多次申请到该锁,那么我们就认为是可重入性锁。而 synchronized 锁在上面已经说过了,每当编译执行到 monitorenter 指令时,synchronized 做的操作是将计数器 +1,该操作并没有限定是第几次获取该锁,所以,synchronized 是具有可重入性的。

4. JVM 对 Java 的原生锁做了哪些优化?

在 Java 1.6版本之前,monitor 的实现是依靠底层操作系统的互斥锁实现的,原理就和获取锁/释放锁的逻辑一致。 而由于Java 层面和操作系统之间有映射关系,每当需要将一个线程进行唤醒/阻塞时,都需要操作系统从用户态切换到内核态来执行,消耗大量的处理时间。
优化如下:

  1. 自旋锁,即在线程进入阻塞之前,先让线程自旋等待一段时间,在这段时间里,若锁释放了,则获取到锁,这样就避免了操作系统从用户态到内核态的重复切换。
  2. 偏向锁:它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
    如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
  3. 轻量级锁:由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
    使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来升级为重量级锁。

当没有竞争出现时,默认使用偏向锁,JVM 会利用 CAS 操作,在对象头上的 Mark Word 部分设置线程ID,表示这个对象偏向于此线程,从而降低无竞争开销。
当有另一个线程试图锁定某个被偏斜的对象时,JVM 就撤销偏斜锁,从而升级为轻量级锁。
轻量级锁会依赖 CAS 操作 Mark Word 来获取锁,如果获取成功,就使用普通的轻量级锁;否则,再次升级为重型锁。

5. 为什么说 Synchronized 锁是非公平锁?

synchronized 并不是按照申请锁的前后给等待线程分配锁的,每当锁被释放时,每个等待线程都有机会竞争到锁,这样做的目的是为了提高执行效率,缺点是可能会产生线程饥饿情况。

6. 什么是锁消除和锁粗化?

  1. 锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。锁削除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
  2. 锁粗化:原则上,同步块的作用范围要尽可能小,但是如果一系列操作使得同一对象被反复加锁解锁,导致频繁进行互斥同步降低没必要的性能消耗,锁粗化的目的是增大锁的作用域,防止频繁的互斥同步。

7. 什么是乐观锁和悲观锁?

  1. 乐观锁:假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。
  2. 悲观锁:对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。

8. 为什么说 synchronized 是一个悲观锁?

synchronized 是并发策略是:不管是否会产生竞争,任何的数据操作都会施加锁,用户态内核态转换,维护锁计数器和检查是否有线程处于阻塞状态被唤醒等操作。

9. 乐观锁一定就是好的吗?

优点:避免独占对象的现象,提高并发性能;
缺点:
1. 只能保证一个共享变量的原子操作。当多个变量时,乐观锁将力不从心,单互斥锁能轻易解决;
2. 长时间自旋可能导致开销过大;
3. ABA 问题:CAS 的核心思想是通过对比内存值和预期值是否一样从而判断内存值是否被改过,但假设内存值原来是A,后来改成B后再次改成A,则CAS会认为内存值并没有发生变化。解决方法是引入版本号,每次变量修改后版本号加1;

二. Java 线程池相关问题:

1. Java 中的线程池是如何实现的?

线程池本质是由一个线程集合 workerSet 和 workQueue 组成,当用户往线程池中提交一个任务时,线程池会将任务首先放到队列中,workerSet 会不断从队列中获取任务,然后执行,当 workQueue 中没有任务时,workerSet就会阻塞,直到队列中有任务了才继续执行。

2. 说一下创建线程的几个核心构造参数?

corePoolSize:线程池的核心线程数;
maximumPoolSize:能容纳的最大线程数;
keepAliveTime:空闲线程存活时间;
unit: 存活的时间单位;
workQueue: 存放提交但未执行任务的队列;

3. 线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?

线程池在默认初始化后不会启动 worker ,待有请求时才启动;
每当调用 execute() 方法时,线程池会判断当前运行的线程数是否小于 corePoolSize ;
如果小于则马上创建线程执行此任务,如果大于等于则将此任务放到 workQueue 中等待;
如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果队列满了,且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException;
当一个线程完成任务时,它会从队列中取下一个任务来执行;
当一个线程无事可做,超过一定的时间( keepAliveTime)时,线程池会判断 :如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉;
所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小.

4. Java 中默认实现好了的线程池有哪些?他们有什么异同?

①. SingleThreadExecutor 线程池:

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

②. FixedThreadPool 线程池 :

创建一个定长线程池,可控制线程最大并发数,超过线程池数量的在队列中等待。

③. ScheduledThreadPool 线程池 :

创建一个定长线程池,支持定时和周期性任务执行。

④. SingleThreadExecutor 线程池 :

创建一个单线程化的线程池,它只会用唯一的线程来执行任务,保证所有任务按照指定顺序(FIFO//先进先出,LIFO//后进先出,优先级)执行。

⑤. CachedThreadPool 线程池:

创建一个无边界线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。

5. 如何在 Java 线程池中提交线程?

  1. execute(): ExecutorService.execute 方法接收一个 Runable 实例,它用来执行一个任务:
        ExecutorService.execute(Runnable runable);
  1. submit(): ExecutorService.submit() 方法返回的是 Future 对象。可以用 isDone() 来查询 Future 是否已经完成,当任务完成时,它具有一个结果,可以调用 get() 来获取结果。也可以不用 isDone()进行检查就直接调用 get(),在这种情况下,get() 将阻塞,直至结果准备就绪。
  FutureTask task = ExecutorService.submit(Runnable tunnable);
  FutureTask<T> task = ExecutorService.submit(Runnable runnable, T Result);
  FutureTask<T> task = ExecutorService.submit(Callable < T > Callable);

三. Java 内存模型相关问题:

1. 什么是JMM,Java 中各线程是如何看到彼此的变量的?

JMM是Java的并发采用的是共享内存模型,定义了程序中各个变量的访问规则,所有的变量都存储在主内存中,每个线程都有自己独立的工作内存,工作内存在保存的是主内存中的变量备份。每个线程只能操作自己工作内存中的变量,不能直接读取主内存中的变量,不同线程也不能互相访问彼此工作内存中的变量,只能通过主内存进行变量的传递。

2. 请说说 volatile 有什么特点,为什么它能保证变量对所有线程的可见性?

关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制,当一个变量被 volatile 修饰时,它将具有两种特性:
a. 保证此变量对所有线程的可见性,即当某一线程对此变量进行修改后,其他线程都可立即得知新值;
b. 禁止指令重排序优化。

volatile 的实现主要依赖于内存间的8中操作,保证一个线程对 volatile 修饰的变量操作后,其他线程都能立刻得知:

lock:把变量标记成一条线程独占状态;
unlock:把一个处于锁定状态的变量释放出来,放入主内存的变量中;

read:把一个变量从主内存中读取到工作内存中,以便 load;
write:把 store 操作从工作内存中得到的值放到主内存中;

load:把 read 操作从主内存中得到的值放入工作内存的变量副本中;
store:把工作内存中的变量值传递到主内存中,以便 write;

use:把工作内存中的值传递给执行引擎;
asgin:把执行引擎中的值传递到工作内存中。

3. 既然 volatile 对线程间具有可见性,那么基于 volatile 的变量的运算是不是就是并发安全的呢?

不是的,volatile 并不具有原子性,volatile 变量在各个线程的工作内存中并不具有一致性,因为 Java 里面的运算并非原子性操作,导致 volatile 变量的运算在并发下并不安全。

4. 说说 volatile 和 synchronized 的区别?

synchronizedvolatile
可见性,一致性可见性
修饰变量,方法修饰变量
锁定当前变量,只能一个线程访问从主内存中拷贝变量到工作内存
会造成线程阻塞不会造成线程阻塞
标记的变量可以被编译器优化编辑的变量不可被编译器优化

5. 说说 ThreadLocal 怎么解决并发安全的?

当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用他的线程都创建了一个变量副本,每个线程都独立使用自己的副本,而不会影响其他线程的变量。

6. 很多人都说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么?

== 使用 ThreadLocal 要注意 remove==
ThreadLocal 的实现是基于一个所谓的 ThreadLocalMap, 在 ThreadLocalMap 中,它的 key 是一个弱引用。通常弱引用都会和引用队列配合清理机制使用,但是 ThreadLocal 是个例外,它并没有这么做。这意味着,废弃项目的回收依赖于显式地触发,否则就要等待线程结束,进而回收相应 ThreadLocalMap!这就是很多 OO的来源,所以通常都会建议,应用一定要自己负责 remove,并且不要和线程池配合,因为 worker 线程往往是不会退出的。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值