转载

Java并发编程面试基础问题和答案

点击关注上方“ 知了小巷 ”,

设为“置顶或星标”,第一时间送达干货。

Java并发编程

1.Synchronized用过吗,其原理是什么?

这是一道Java面试中几乎百分百会问到的问题,因为没有任何写过并发程序的开发者会没听说或者没接触过Synchronized。Synchronized是由JVM实现的一种实现互斥同步的一种方式,如果你查看被Synchronized修饰过的程序块编译后的字节码,会发现,被Synchronized修饰过的程序块,在编译前后被编译器生成了 monitorenter和monitorexit 两个字节码指令。这两个指令是什么意思呢?在虚拟机执行到monitorenter指令时,首先要尝试获取对象的锁:如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+1;当执行monitorexit指令时将锁计数器-1;当计数器为0时,锁就被释放了。如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。 Java中Synchronize通过在对象头设置标记,达到了获取锁和释放锁的目的。

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

非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生 线程饥饿 现象。

3.为什么说Synchronized是一个悲观锁?乐观锁的实现原理又是什么?什么是CAS,它有什么特性?

Synchronized显然是一个悲观锁,因为它的 并发策略是悲观 的:不管是否会产生竞争,任何的数据操作都必须要加锁、用户态内核态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。随着 硬件指令集的发展 ,我们可以使用 基于冲突检测的乐观并发策略 。先进行操作,如果没有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。 这种乐观的并发策略的许多实现不需要线程挂起,所以被称为非阻塞同步。乐观锁的核心算法是CAS(CompareandSwap,比较并交换), 它涉及到三个操作数: 内存值、预期值、新值。 当且仅当预期值和内存值相等时才将内存值修改为新值。这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过, 舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。 CAS具有原子性,它的原子性由CPU硬件指令实现保证,即使用JNI调用Native方法调用由C++编写的硬件级别指令,JDK中提供了 Unsafe 类执行这些操作。

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

ReentrantLock是Lock的实现类,是一个互斥的同步锁。从功能角度, ReentrantLock比Synchronized的同步操作更精细(因为可以像 普通对象 一样使用), 甚至实现Synchronized没有的高级功能,如:

等待可中断: 当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。

带超时的获取锁尝试: 在指定的时间范围内获取锁,如果时间到了仍然无法获取则返回。

可以判断是否有线程在排队等待获取锁。

可以响应中断请求: 与Synchronized不同,当获取到锁的线程被中断时,能够响应中断,中断异常将会被抛出,同时锁会被释放。

可以实现公平锁。 从锁释放角度,Synchronized在JVM层面上实现的,不但可以通过一些监控工具监控Synchronized的锁定,而且在代码执行出现异常 时,JVM会自动释放锁定;但是使用Lock则不行,L ock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中。 从性能角度,Synchronized早期实现比较低效,对比ReentrantLock,大多数场景性能都相差较大。但是在Java6中对其进行了非常多的改进,在竞争不激烈时,Synchronized的性能要优于ReetrantLock;在高竞争情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。

5.谈谈ReadWriteLock和StampedLock。

虽然ReentrantLock和Synchronized简单实用,但是行为上有一定局限性, 要么不占,要么独占。 实际应用场景中,有时候不需要大量竞争的写操作,而是以并发读取为主,为了 进一步优化并发操作的粒度 ,Java提供了读写锁。读写锁基于的原理是多个读操作不需要互斥,如果读锁试图锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作结束,这样就可以自动保证不会读取到有争议的数据。 ReadWriteLock代表了一对锁 ,下面是一个基于读写锁实现的数据结构,当数据量较大,并发读多、并发写少的时候,能够比纯同步版本凸显出优势:

Java并发编程面试基础问题和答案

读写锁看起来比Synchronized的粒度似乎细一些,但在实际应用中,其表现也并不尽如人意,主要还是 因为相对比较大的开销。 所以, JDK在后期引入了StampedLock ,在提供类似读写锁的同时,还支持优化读模式。优化读基于假设:大多数情况下读操作并不会和写操作冲突,其逻辑是先试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。

Java并发编程面试基础问题和答案

6.如何让Java的线程彼此同步? 你了 解过哪些同步器? 请分 别介绍下。

JUC中的同步器三个主要的成员: CountDownLatch CyclicBarrier Semaphore ,通过它们可以方便地实现很多线程之间协作的功能。

CountDownLatch叫倒计数,允许一个或多个线程等待某些操作完成。

看几个场景:

跑步比赛,裁判需要等到所有的运动员(“其他线程”)都跑到终点(达到目标),才能去算排名和颁奖。

模拟并发,我需要启动100个线程去同时访问某一个地址,我希望它们能同时并发,而不是一个一个的去执行。用法:CountDownLatch构造方法指明计数数量,被等待线程调用

countDown将计数器减1,等待线程使用await进行线程等待。

一个简单的例子:

Java并发编程面试基础问题和答案

CyclicBarrier叫循环栅栏,它实现让一组线程等待至某个状态之后再全部同时执行,而且当所有等待线程被释放后,CyclicBarrier可以被重复使用。 CyclicBarrier的典型应用场景是用来等待并发线程结束。CyclicBarrier的主要方法是await(),await()每被调用一次,计数便会减少1,并阻塞住当前线程。当计数减至0时,阻塞解除,所有在此CyclicBarrier上面阻塞的线程开始运行。在这之后,如果再次调用await(),计数就又会变成N-1,新一轮重新开始,这便是Cyclic的含义所在。CyclicBarrier.await()带有返回值,用来表示当前线程是第几个到达这个Barrier的线程。

举例说明如下:

Java并发编程面试基础问题和答案

Semaphore,Java版本的信号量实现,用于控制同时访问的线程个数,来达到限制通用资源访问的目的, 其原理是通过acquire()获取一个许可,如果没有就等待,而release()释放一个许可。

Java并发编程面试基础问题和答案

如果Semaphore的数值被初始化为1,那么一个线程就可以通过acquire进入互斥状态,本质上和互斥锁是非常相似的。但是区别也非常明显,比如互斥锁是有持有者的,而对于Semaphore这种计数器结构,虽然有类似功能,但其实不存在真正意义的持有者,除非我们进行扩展包装。

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

显然不是的。 线程池默认初始化后不启动Worker, 等待有请求时才启动。每当我们调用execute()方法添加一个任务时,线程池会做如下判断:

如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常RejectExecutionException。

当一个线程完成任务时,它会从队列中取下一个任务来执行。当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断。 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。 所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

8.提到可以通过配置不同参数创建出不同的线程池,那么Java中默认实现好的线程池又有哪些呢?请比较它们的异同。

8.1.SingleThreadExecutor线程池

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

corePoolSize:1,只有一个核心线程在工作。

maximumPoolSize:1。

keepAliveTime:0L。

workQueue:newLinkedBlockingQueue<Runnable>(),其缓冲队列是无界的。

8.2.FixedThreadPool线程池

FixedThreadPool是 固定大小的线程池,只有核心线程。 每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器。

corePoolSize:nThreads

maximumPoolSize:nThreads

keepAliveTime:0L

workQueue:newLinkedBlockingQueue<Runnable>(),其缓冲队列是无界的。

8.3.CachedThreadPool线程池

CachedThreadPool是无界线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。 线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。 SynchronousQueue是一个是缓冲区为1的阻塞队列。缓存型池子通常用于执行一些生存期很短的异步型任务,因此在一些面向 连接的daemon型SERVER中用得不多。 但对于生存期短的异步任务,它是Executor的首选。

corePoolSize: 0

maximumPoolSize: Integer.MAX_VALUE

keepAliveTime: 60L

workQueue:newSynchronousQueue<Runnable>(),一个是缓冲区为1的阻塞队列。 8.4.ScheduledThreadPool线程池

ScheduledThreadPool:核心线程池固定,大小无限的线程池。此线程池支持 定时以及周期性执行任务的需求。 创建一个周期性执行任务的线程池。如果闲置,非核心线程池会在DEFAULT_KEEPALIVEMILLIS时间内回收。

corePoolSize:corePoolSize

maximumPoolSize:Integer.MAX_VALUE

keepAliveTime:DEFAULT_KEEPALIVE_MILLIS

workQueue:newDelayedWorkQueue()

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

线程池最常用的提交任务的方法有两种:

1.execute(): ExecutorService.execute方法接收一个Runable实例,它用来执行一个任务:

Java并发编程面试基础问题和答案

2.submit(): ExecutorService.submit()方法返回的是Future对象。可以用isDone()来查询Future是否已经完成,当任务完成时,它具有一个结果,可以调用get()来获取结果。也可以不用isDone()进行检查就直接调用get(),在这种情况下,get()将阻塞,直至结果准备就绪。

Java并发编程面试基础问题和答案

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

关键字volatile是Java虚拟机提供的最轻量级的同步机制【不保证原子性】。当一个变量被定义成volatile之后,具备两种特性:

1.保证此变量对所有线程的可见性。 当一条线程修改了这个变量的值,新值对于其他线程是可以立即得知的。而普通变量做不到这一点。

2.禁止指令重排序优化【保证有序性】。 普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。

Java的内存模型定义了8种内存间操作:

lock和unlock

lock-把一个变量标识为一条线程独占的状态。

unlock-把一个处于锁定状态的变量释放出来,释放之后的变量才能被其他线程锁定。

read和write

read-把一个变量值从主内存传输到线程的工作内存,以便load。

write-把store操作从工作内存得到的变量的值,放入主内存的变量中。

load和store

load-把read操作从主内存得到的变量值放入工作内存的变量副本中。

store-把工作内存的变量值传送到主内存,以便write。

use和assgin

use-把工作内存变量值传递给执行引擎。

assign-将执行引擎值传递给工作内存变量值。

volatile的实现基于这8种内存间操作,保证了一个线程对某个volatile变量的修改,一定会被另一个线程看见,即保证了可见性。

11.请对比下volatile对比Synchronized的异同。

Synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。

12.ThreadLocal和Synchonized

ThreadLocal和Synchonized 都用于解决多线程并发访问,防止任务在共享资源上产生冲突【值得注意的是, ThreadLocal 本质并没有访问共享资源,只是 ThreadLocal 对象只有一个而已】。但是ThreadLocal与Synchronized有本质的区别。Synchronized用于实现同步机制,是利用锁的机制使变量或代码块在某一时该只能被一个线程访问,是一种“以时间换空间”的方式。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,根除了对变量的共享,是一种“以空间换时间”的方式。

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

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

ThreadLocal.ThreadLocalMap threadLocals。

threadLocals 由ThreadLocal#createMap时创建。

【END】

往期推荐:

JVM面试基础问题和答案

Java并发编程面试基础问题和答案  

Java并发编程面试基础问题和答案

原文  http://mp.weixin.qq.com/s?__biz=MzA4NzA5NzE5Ng==&mid=2650228866&idx=2&sn=6ebaa8b86e5a4dac366e9d9315b7ed49
正文到此结束
Loading...