程序员必看的14个并发编程高级面试题!这些你都会吗?

1、跟Synchronized相比,可重入锁ReentrantLock其实现原理有什么不同?

其实,锁的实现原理基本是为了达到一个目的:让所有的线程都能看到某种标记。

Synchronized通过在对象头中设置标记实现了这一目的,是一种JVM原生的锁实现方式,而Reentrantl ock以及所有的基于Lock接口的实现类,都是通过用一个vlitile修饰的int型变量,并保证每个线程都能拥有对该int的可见性和原子修改,其本质是基于所谓的AQS框架。

2、那么请谈谈AQS框架是怎么回事儿?

AQS(AbstractQueuedSynchronizer类)是一个用来构建锁和同步器的框架,各种Lock包中的锁(常用的有ReentrantL ock、ReadWriteL ock),以及其他如Semaphore、CountDownL atch,甚至 是早期的FutureTask等,都是基于AQS来构建。

  1. AQS在内部定义了一个volatile int state变量,表示同步状态:当线程调用lock方法时,如果state=0, 说明没有任何线程占有共享资源的锁,可以获得锁并将state=1 ;如果state=1,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。
  2. AQS通过Node内部类构成的一个双向链表结构的同步队列,来完成线程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。
    ●Node类是对要访问同步代码的线程的封装,包含了线程本身及其状态叫waitStatus(有五种不同取值,分别表示是否被阻塞,是否等待唤醒,是已经被取消等),每个Node结点关联其prev结点和next结点,方便线程释放锁后快速唤醒下一个在等待的线程,是一 个FIFO的过程。
    ●Node类有两个常量,SHARED 和EXCLUSIVE,分别代表共享模式和独占模式。所谓共享模式是一个锁允许多条线程同时操作(信号量Semaphore就是基于AQS的共享模式实现的),独占模式是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待(如ReentranLock)。
  3. AQS通过内部类ConditionObject构建等待队列(可有多个),当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。
  4. AQS和Condition各自维护了不同的队列,在使用Lock和Condition的时候,其实就是两个队列的互相移动。

3、请尽可能详尽地对比下Synchronized和ReentrantLock的异同。

ReentrantLock是Lock的实现类,是一个互斥的同步锁.从功能角度,ReentrantL ock比Synchronized 的同步操作更精细(因为可以像普通对象一样使用),甚至实现Synchronized没有的高级功能,如:
●等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。
●带超时的获取锁尝试:在指定的时间范围内获取锁,如果时间到了仍然无法获取则返回。
●可以判断是否有线程在排队等待获取锁。
●可以响应中断请求:与Synchronized不同,当获取到锁的线程被中断时,能够响应中断,中断异常将会被抛出,同时锁会被释放。
●可以实现公平锁。

4、ReentrantL ock是如何实现可重入性的?

ReentrantL ock内部自定义了同步器Sync(Sync既实现了AQS,而AOS提供了一种互斥锁持有的方式),其实就是加锁的时候通过CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下当前维护的那个线程ID和当前请求的线程ID 是否一样,一样就可重入了。

5、除了ReetrantLock,你还接触过JUC中的哪些并发工具?

通常所说的并发包(JUC)也就是java.util.concurrent及其子包,集中了Java并发的各种基础工具类,具体主要包括几个方面:
●提供了CountDownL atch、CyliBarrier. Semaphore等, 比
Synchronized更加高级,可以实现更加丰富多线程操作的同步结构。
●提供了ConcurrentHashMap、有序的ConcunrrentSkipListMap,或者通过类似快照机制实现线程安全的动态数组CopyOnWriteArrayList 等各种线程安全的容器。
●提供了ArrayBlockingQueue、SynchorousQueue 或针对特定场景的PriorityBlockingQueue等,各种并发队列实现。
●强大的Executor 框架,可以创建各种不同类型的线程池,调度任务运行等。

6、请谈谈ReadWriteLock和StampedLock.

虽然Reentrantl ock和Synchronized简单实用,但是行为上有一定局限性,要么不占,要么独占。实际应用场景中,有时候不需要大量竞争的写操作,而是以并发读取为主,为了进一步优化并发操作的粒度, Java 提供了读写锁。读写锁基于的原理是多个读操作不需要互斥,如果读锁试图锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作结束,这样就可以自动保证不会读取到有争议的数据。
ReadWriteLock代表了一对锁,下面是一个基于读写锁实现的数据结构,当数据量较大,并发读多、并发写少的时候,能够比纯同步版本凸显出优势:
在这里插入图片描述
读写锁看起来比Synchronized的粒度似乎细一些,但在实际应用中,其表现也并不尽如入意,主要还是因为相对比较大的开销。所以,JDK 在后期引入了StampedL ock,在提供类似读写锁的同时,还支持优化读模式。
优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。
在这里插入图片描述

7、CyclicBarrier 和CountDownLatch看起来很相似,请对比下呢?

它们的行为有一定相似度,区别主要在于:
●CountDownLatch是不可以重置的,所以无法重用,CyclicBarrier 没有这种限制,可以重用。

●CountDownLatch的基本操作组合是countDown/await,调用await的线程阻塞等待countDown足够的次数,不管你是在一个线程还是多个线程里countDown,只要次数足够即可。CyclicBarrier 的基本操作组合就是await,当所有的伙伴都调用了await,才会继续进行任务,并自动进行重置。

CountDownL atch目的是让一个线程等待其他N个线程达到某个条件后,自己再去做某个事(通过CydlicBarrier的第二个构造方法publicCylicBarrier(int parties, Runnable barrierAction),在新线程里做事可以达到同样的效果)。而CyllicBarrier的目的是让N多线程互相等待直到所有的都达到某个状态,然后这N个线程再继续执行各自后续(通过CountDownL atch在某些场合也能完成类似的效果)。

8、Java中的线程池是如何实现的?

●在Java中,所谓的线程池中的“线程”,其实是被抽象为了一个静态内部类Worker,它基于AQS实现,存放在线程池的HashSet workers成员变量中;
●而需要执行的任务则存放在成员变量workQueue(BlockingQueueworkQueue)中。
这样,整个线程池实现的基本思想就是:从workQueue中不断取出需要执行的任务,放在Workers中进行处理。

9、创建线程池的几个核心构造参数?

Java中的线程池的创建其实非常灵活,我们可以通过配置不同的参数,创建出行为不同的线程池,这几个参数包括:
●corePoolSize:线程池的核心线程数。
●maximumPoolSize:线程池允许的最大线程数。
●keepAliveTime:超过核心线程数时闲置线程的存活时间。
●workQueue:任务执行前保存任务的队列,保存由execute方法提交的Runnable任务。

10、什么是Java的内存模型,Java中各个线程是怎么彼此看到对方的变量的?

Java的内存模型定义了程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为这些是线程私有的,不会被共享,所以不存在竞争问题。

Java中各个线程是怎么彼此看到对方的变量的呢?Java中定义了主内存与工作内存的概念:
所有的变量都存储在主内存,每条线程还有自己的工作内存,保存了被该线程使用到的变量的主内存副本拷贝。
线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存的变量。不同的线程之间也无法直接访问对方工作内存的变量,线程间变量值的传递需要通过主内存。

11、既然volatile能够保证线程间的变量可见性,是不是就意味着基于volatile变量的运算就是并发安全的?

显然不是的。基于volatile变量的运算在并发下不一定是安全的。volatile ;变量在各个线程的工作内存,不存在-致性问题(各个线程的工作内存中volatile变量,每次使用前都要刷新到主内存)。但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。

12、请对比下volatile对比Synchronized的异同。

Synchronized既能保证可见性,又能保证原子性,而volaile只能保证可见性,无法保证原子性
ThreadL ocal和Synchonized都用于解决多线程并发访问,防止任务在共享资源上产生冲突。但是ThreadL ocal与Synchronized有本质的区别。

Synchronized用于实现同步机制,是利用锁的机制使变量或代码块在某一时该只能被一个线程访问,是一种“以时间换空间”的方式。而ThreadLocal为每一个线程都提供 了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,根除了对变量的共享,是一种“以空间换时间”的方式。

13、请谈谈ThreadLocal是怎么解决并发安全的?

ThreadL ocal这是Java提供的-种保存线程私有信息的机制,因为其在整个线程生命周期内有效,所以可以方便地在一个线程关联的不同业务模块之间传递信息,比如事务ID、Cookie 等上下文相关信息。
ThreadL ocal为每一个线程维护变量的副本,把共享数据的可见范围限制在同一个线程之内,其实现原理是,在ThreadL ocal类中有一个Map,用于存储每一个线程的变量的副本。

14、很多人都说要慎用ThreadLocal,谈谈你的理解,使用 ThreadL ocal需要注意些什么?

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

-end

创作不易, 非常欢迎大家的点赞、评论和关注
你的点赞、评论以及关注
是对我最大的支持和鼓励!

猜你喜欢

转载自blog.csdn.net/weixin_49527334/article/details/108470976