转载

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

一. 如何保证集合是线程安全的? ConcurrentHashMap 如何实现高效的线程安全?

典型回答:

Java提供了不同层面的线程安全支持。在传统集合框架内部,除了Hashtable Vector等 同步容器,还提供了同步包装器,我们可以调用Collections工具类提供的包装方法,来获取一个同步的包装容器(例如Collections. synchronizedMap) ,但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较底下,另外,更加普遍的选择是利用并发包提供的线程安全容器类。具体保证线程安全的方式,包括有从简单的synchronize方式,到基于更加精细化的,比如基于分离式锁实现的ConcurrentHashMap等并发实现等。具体选择要看开发的场景需求,总体来说,并发包内提供的容器通用场景,远优于早期的简单同步实现。

知识扩展

1. 为什么需要ConcurrentHashMap?

Hashtable本身比较低效,因为它的实现基本就是put get size等各种方法加上" synchronized"。简单来说,这就导致了所有并发操作都要竞争同一把锁,一个线程在进行同步操作时,其他线程只能等待,大大降低了并发操作的效率。前面说过HashMap不是线程安全的,那么能不能利用Collections提供的同步包装器来解决问题?实际上同步器只是利用输入Map构造了另一个同步版本,所有操作不再声明为synchronized方法,但是还是利用了"this"作为互斥的mutex,没有真正意义上的改进。

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

所以,Hashtable或者同步包装版本,都只是适合在非高度并发的场景下。

2. ConcurrentHashMap工 作机制

在早期的ConcurrentHashMap,其实现是基于分离锁,也就是将内部进行分段,里面则是HashEntry的数据。和HashMap类似,哈希相同的条目也是以链表形式存放。(简单点来说,就是在ConcurrentHashMap中,把Map分成了N个Segment, put和get的时候,都是现根据key. hashCode()算出放到哪个Segment中)

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

在构造的时候, Segment的数量由所谓的concurrentcyLevel来决定,默认是16,也可以在相应构造函数直接指定。注意,Java需要它是2的幂数值,如果输入是类似15这种非幂值,会自动调整到16之类2的幂数值。

看如下的测试代码:

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

我们创建了3个线程分别向ConcurrentHashMap中添加数据,根据ConcurrentHashMap . segmentFor的算法,当我们向ConcurrentHashMap中put key为3或者4的数据时,此时他们对应的Segment都是segments[1],而put key为7对应的数据时,此时对应的Segment是segments[12]。那么当线程1在put(3,33)时,线程2也想put(4,44)时,因为线程1和线程2操作的都是同一个Segment,线程1会首先获取到锁,可以进入,线程2则会阻塞在锁上。线程3在put数据时,他对应的segments是12,他不会上锁。以上就是ConcurrentHashMap的工作机制,通过把整个Map分为N个Segment (类似HashTable),可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。

二. Synchronized和ReentrantLock有什么区别?

典型回答:

synchronized是Java内建的同步机制,所以也有人称其为Intrinsic Locking (固有锁) ,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。在Java 5以前, synchronized是仅有的同步手段,在代码中, synchronized可以用来修饰方法,也可以使用在特定的代码块上,本质上synchronized方法等同于把方法全部语句用synchronized块包起来。

ReentrantLock,通常翻译为再入锁,是Java 5提供的锁实现,它的语义和synchronized基本相同。再入锁通过代码直接调用lock()方法获取,代码书写更加灵活。与此同时,ReentrantLock提供了很多实用的方法,能够实现很多synchronized无法做到的细节控制,但是在编码中也需要注意,必须要明确调用unlock()方法释放,不然就会一直持有该锁。synchronzi ed和ReentrantLock的性能不能一概而论,早期版本synchronized在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于ReentrantLock.

知识扩展:

什么是线程安全?线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境下共享的可修改的状态的正确性,这里的状态反映在程序中其实可以看做是数据。

换个角度来看,如果状态不是共享的,或者不是可修改的,也就不存在线程安全问题,进而可以推理出保证线程安全的两个办法:第一个是封装,我们可以将对象内部状态隐藏保护起来。第二个是不可变。

线程安全需要保证几个基本特性:第一个是原子性,简单来说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。第二是可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存中,volatile关键字就是负责保证可见性的。第三个是有序性,是保证线程内串行语义,避免指令重排等。

可能有点晦涩,那么我们看看下面的代码段,分析一下原子性需求体现在哪里。这个例子通过取两个次数值然后进行对比,来模拟两次对共享状态的操作。你可以编译并执行,可以看到,仅仅是两个线程的低度并发,就非常容易碰到former和latter不相等的情况。这是因为,在两次取值的过程中,其他线程已经修改了sharedState的值。

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

运行结果

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

因为线程安全的问题,导致以上的运行结果。我们可以将两次赋值过程用synchronized保护起来,使用this作为互斥单元,就可以避免别的线程并发的去修改sharedState.

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

我们在来看看ReentrantLock.

你可能好奇什么是再入?它是表示当一个线程试图获取一个它已经获取的琐时,这个获取动作就自动成功,这是对锁获取粒度的一个概念,也就是一个锁的持有是以线程为单位而不是基于调用次数。

首先看下面的例子:

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

从上可以看出,使用重入锁进行加锁是一种显式操作,通过何时加锁与释放锁使重入锁对逻辑控制的灵活性远远大于synchronized关键字。同时,需要注意,有加锁就必须有释放锁,而且加锁与释放锁的份数要相同,这里就引出了“重”字的概念,如上边代码演示,放开①、②处的注释,与原来效果一致。

ReentrantLock实现中断操作,先了解几个方法的作用

lockInterruptib1y():当两个线程同时通过lock. lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB. interrupt()方法能够中断线程B的等待过程。.

i sHeldByCurrentThread():查询当前线程是否保持锁。

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

t1、t2线程开始运行时,会分别持有lock1和1ock2而请求1ock2和1ock1, 这样就发生了死锁。但是,在③处给t2线程状态标记为中断后,持有重入锁lock2的线程t2会响应中断,并不再继续等待lock1,同时释放了其原本持有的1ock2, 这样t1获取到了lock2,正常执行完成。t2也会退出,但只是释放了资源并没有完成工作。

ReentrantLock实现锁申请等待限时

可以使用tryLock()或者tryLock(long timeout, TimeUtil unit) 方法进行一次限时的锁等待。前者不带参数,这时线程尝试获取锁,如果获取到锁则继续执行,如果锁被其他线程持有,则立即返回false ,也就是不会使当前线程等待,所以不会产生死锁。后者带有参数,表示在指定时长内获取到锁则继续执行,如果等待指定时长后还没有获取到锁则返回false。

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

公平锁

所谓公平锁,就是按照时间先后顺序,使先等待的线程先得到锁,而且,公平锁不会产生饥饿锁,也就是只要排队等待,最终能等待到获取锁的机会。使用重入锁(默认是非公平锁)创建公平锁:

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

ReentrantLock与Condition一起使用

配合关键字synchronized使用的方法如: await()、 notify()、 notifyAll(), 同样配合ReentrantLock使用的Conditon提供了以下方法:

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

ReentrantLock实现了Lock接口,可以通过该接口提供的newCondition()方法创建Condition对象

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

三. 谈谈MySQL支持的事务隔离级别?

典型回答:

MySQL数据库事务隔离级别分为四个不同层次:

1. 读未提交:一个事务能够读取其他事务未提交的修改的数据,这是最低的隔离水平,允许脏读出现。

脏读的场景:

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

2. 读已提交:一个事务能够读取其他事务已经提交的修改的数据,脏读不会出现。但是隔离级别比较低,允许出现不可重复读和幻象读。

不可重读的场景:

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

3. 可重复读:一个事务读取到另一个事务已经提交的数据,隔离级别比较高,允许出现幻读。这也是MySQL InnoDB引 擎的默认隔离级别,但是和一些其他的数据库不同,可以简单的认为MySQL在可重复读级别不会出现幻读。

幻读的场景:

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

4. 串行化:并发事务之间是串行化的,通常意味着读取需要共享读锁,更新需要获取排他写锁。这是最高的隔离级别。

不可重复读和幻读到底有什么区别呢?

  • 不可重复读是读取了其他事务更改的数据,针对update操作解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
  • 幻读是读取了其他事务新增的数据,针对insert操作解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。

四. Java并发类库提供的线程池有哪几种?分 别有什么特点?

经典回答:

通常开发者都是利用Executors提供的通用线程池创建方法,去创建不同配置的线程池,主要区别在于不同的ExecutorService类型或者不同的初识参数。

Executors目前提供了5种不同的线程池创建配置:

newCachedThreadPool(),它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过60S,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗资源。

newFixedThreadPool (int nThreads),重用指定数目的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目nThreads。

newSingleThreadExecutor(),它的特点在于工作线程数目被限制为1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改造线程实例,因此可以避免其改变线程数目。

newSingleThreadSchedul edExecutor ()和newSecheduledThreadPool (int corePoolSize) ,创建的是个Schedul edExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。

文末福利

限于篇幅,今日份大厂面试真题到此为止;需要的更多的Java后端面试题、视频学习资料、架构师成长路线图的朋友点击下方传送门, 即可免费领取面试资料和视频学习资料

传送门

笔者整理的面试题包含但不限于Kafka、Mysql、Tomcat、Docker、Spring、MyBatis、Nginx、Netty、Dubbo、Redis、Netty、Spring cloud、分布式、高并发、性能调优、微服务等架构技术

以下是笔者整理的部分面试题截图

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

最新蚂蚁金服Java后端面试题,双十一也要加油呀,附面试学习资料

原文  https://segmentfault.com/a/1190000020956404
正文到此结束
Loading...