100道Java并发和多线程基础⾯试题⼤集合(含解答),这波⾯
试稳了~
100个问题汇总
1、多线程有什么⽤?
⼀个可能在很多⼈看来很扯淡的⼀个问题:我会⽤多线程就好了,还管它有什么⽤?在我看来,这个回答更扯淡。所谓"知其然知其所以然","会⽤"只是"知其然","为什么⽤"才是"知其所以然",只有达到"知其然知其所以然"的程度才可以说是把⼀个知识点运⽤⾃如。OK,下⾯说说我对这个问题的看法:
(1)发挥多核CPU的优势
随着⼯业的进步,现在的笔记本、台式机乃⾄商⽤的应⽤服务器⾄少也都是双核的,4核、8核甚⾄16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的"多线程"那是假的多线程,同⼀时间处理器只会处理⼀段逻辑,只不过线程之间切换得⽐较快,看着像多个线程"同时"运⾏罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时⼯作,多线程,可以真正发挥出多核CPU的优势来,达到充分利⽤CPU的⽬的。
(2)防⽌阻塞
从程序运⾏效率的⾓度来看,单核CPU不但不会发挥出多线程的优势,反⽽会因为在单核CPU上运⾏多线程导致线程上下⽂的切换,⽽降低程序整体的效率。但是单核CPU我们还是要应⽤多线程,就是为了防⽌阻塞。试想,如果单核CPU使⽤单线程,那么只要这个线程阻塞了,⽐⽅说远程读取某个数据吧,对端迟迟未返回⼜没有设置超时时间,那么你的整个程序在数据返回回来之前就停⽌运⾏了。多线程可以防⽌这个问题,多条线程同时运⾏,哪怕⼀条线程的代码执⾏读取数据阻塞,也不会影响其它任务的执⾏。
(3)便于建模
这是另外⼀个没有这么明显的优点了。假设有⼀个⼤的任务A,单线程编程,那么就要考虑很多,建⽴整个程序模型⽐较⿇烦。但是如果把这个⼤的任务A分解成⼏个⼩任务,任务B、任务C、任务D,分别建⽴程序模型,并通过多线程分别运⾏这⼏个任务,那就简单很多了。
2、创建线程的⽅式
biubiu游戏动画论坛⽐较常见的⼀个问题了,⼀般就是两种:
(1)继承Thread类
(2)实现Runnable接⼝
⾄于哪个好,不⽤说肯定是后者好,因为实现接⼝的⽅式⽐继承类的⽅式更灵活,也能减少程序之间的耦合度,⾯向接⼝编程也是设计模式6⼤原则的核⼼。
3、start()⽅法和run()⽅法的区别
只有调⽤了start()⽅法,才会表现出多线程的特性,不同线程的run()⽅法⾥⾯的代码交替执⾏。如果只是调⽤run()⽅法,那么代码还是同步执⾏的,必须等待⼀个线程的run()⽅法⾥⾯的代码全部执⾏完毕之后,另外⼀个线程才可以执⾏其run()⽅法⾥⾯的代码。
4、Runnable接⼝和Callable接⼝的区别
有点深的问题了,也看出⼀个Java程序员学习知识的⼴度。
Runnable接⼝中的run()⽅法的返回值是void,它做的事情只是纯粹地去执⾏run()⽅法中的代码⽽已;Callable接⼝中的call()⽅法是有返回值的,是⼀个泛型,和Future、FutureTask配合可以⽤来获取异步执⾏的结果。
这其实是很有⽤的⼀个特性,因为多线程相⽐单线程更难、更复杂的⼀个重要原因就是因为多线程充满着未知性,某条线程是否执⾏了?某条线程执⾏了多久?某条线程执⾏的时候我们期望的数据是否已经赋值完毕?⽆法得知,我们能做的只是等待这条多线程的任务执⾏完毕⽽已。⽽Callable+Future/
FutureTask却可以获取多线程运⾏的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是⾮常有⽤。
5、CyclicBarrier和CountDownLatch的区别
两个看上去有点像的类,都在urrent下,都可以⽤来表⽰代码运⾏到某个点上,⼆者的区别在于:
(1)CyclicBarrier的某个线程运⾏到某个点上之后,该线程即停⽌运⾏,直到所有的线程都到达了这个点,所有线程才重新运⾏;CountDownLatch则不是,某线程运⾏到某个点上之后,只是给某个数值-1⽽已,该线程继续运⾏
(2)CyclicBarrier只能唤起⼀个任务,CountDownLatch可以唤起多个任务
(3)CyclicBarrier可重⽤,CountDownLatch不可重⽤,计数值为0该CountDownLatch就不可再⽤了
6、volatile关键字的作⽤
⼀个⾮常重要的问题,是每个学习、应⽤多线程的Java程序员都必须掌握的。理解volatile关键字的作⽤的前提是要理解Java内存模型,这⾥就不讲Java内存模型了,可以参见第31点,volatile关键字的作⽤主要有两个:
telnet 端口命令(1)多线程主要围绕可见性和原⼦性两个特性⽽展开,使⽤volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,⼀定是最新的数据
(2)代码底层执⾏不像我们看到的⾼级语⾔----Java程序这么简单,它的执⾏是Java代码-->字节码-->根据字节码执⾏对应的C/C++代码-->C/C++代码被编译成汇编语⾔-->和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进⾏重排序,多线程下可能会出现⼀些意想不到的问题。使⽤volatile则会对禁⽌语义重排序,当然这也⼀定程度上降低了代码执⾏效率
从实践⾓度⽽⾔,volatile的⼀个重要作⽤就是和CAS结合,保证了原⼦性,详细的可以参见urrent.atomic包下的类,⽐如AtomicInteger。
7、什么是线程安全
⼜是⼀个理论的问题,各式各样的答案有很多,我给出⼀个个⼈认为解释地最好的:如果你的代码在多线程下执⾏和在单线程下执⾏永远都能获得⼀样的结果,那么你的代码就是线程安全的。
这个问题有值得⼀提的地⽅,就是线程安全也是有⼏个级别的:
java常见笔试题(1)不可变
像String、Integer、Long这些,都是final类型的类,任何⼀个线程都改变不了它们的值,要改变除⾮新创建⼀个,因此这些不可变对象不需要任何同步⼿段就可以直接在多线程环境下使⽤
(2)绝对线程安全easyuijquery整合
不管运⾏时环境如何,调⽤者都不需要额外的同步措施。要做到这⼀点通常需要付出许多额外的代价,Java中标注⾃⼰是线程安全的类,实际上绝⼤多数都不是线程安全的,不过绝对线程安全的类,Java中也有,⽐⽅说CopyOnWriteArrayList、CopyOnWriteArraySet
(3)相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove⽅法都是原⼦操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
(4)线程⾮安全
点猫编程平台网址这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程⾮安全的类
8、Java中如何获取到线程dump⽂件
死循环、死锁、阻塞、页⾯打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:
(1)获取到线程的pid,可以通过使⽤jps命令,在Linux环境下还可以使⽤ps -ef | grep java
(2)打印线程堆栈,可以通过使⽤jstack pid命令,在Linux环境下还可以使⽤kill -3 pid
另外提⼀点,Thread类提供了⼀个getStackTrace()⽅法也可以⽤于获取线程堆栈。这是⼀个实例⽅法,因此此⽅法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运⾏的堆栈,
9、⼀个线程如果出现了运⾏时异常会怎么样
如果这个异常没有被捕获的话,这个线程就停⽌执⾏了。另外重要的⼀点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被⽴即释放
10、如何在两个线程之间共享数据
通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进⾏唤起和等待,⽐⽅说阻塞队列BlockingQueue 就是为线程之间共享数据⽽设计的
11、sleep⽅法和wait⽅法有什么区别
这个问题常问,sleep⽅法和wait⽅法都可以⽤来放弃CPU⼀定的时间,不同点在于如果线程持有某个对象的监视器,sleep⽅法不会放弃这个对象的监视器,wait⽅法会放弃这个对象的监视器
12、⽣产者消费者模型的作⽤是什么
这个问题很理论,但是很重要:
(1)通过平衡⽣产者的⽣产能⼒和消费者的消费能⼒来提升整个系统的运⾏效率,这是⽣产者消费者模型最重要的作⽤
(2)解耦,这是⽣产者消费者模型附带的作⽤,解耦意味着⽣产者和消费者之间的联系少,联系越少越可以独⾃发展⽽不需要收到相互的制约
13、ThreadLocal有什么⽤
简单说ThreadLocal就是⼀种以空间换时间的做法,在每个Thread⾥⾯维护了⼀个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进⾏隔离,数据不共享,⾃然就没有线程安全⽅⾯的问题了
14、为什么wait()⽅法和notify()/notifyAll()⽅法要在同步块中被调⽤
这是JDK强制的,wait()⽅法和notify()/notifyAll()⽅法在调⽤前都必须先获得对象的锁
15、wait()⽅法和notify()/notifyAll()⽅法在放弃对象监视器时有什么区别
wait()⽅法和notify()/notifyAll()⽅法在放弃对象监视器的时候的区别在于:
wait()⽅法⽴即释放对象监视器,notify()/notifyAll()⽅法则会等待线程剩余代码执⾏完毕才会放弃对象监视器。
16、为什么要使⽤线程池
避免频繁地创建和销毁线程,达到线程对象的重⽤。另外,使⽤线程池还可以根据项⽬灵活地控制并发的数⽬。
17、怎么检测⼀个线程是否持有对象监视器
我也是在⽹上看到⼀道多线程⾯试题才知道有⽅法可以判断某个线程是否持有对象监视器:Thread类提供了⼀个holdsLock(Object obj)⽅法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是⼀个static⽅法,这意味着"某条线程"指的是当前线程。
18、synchronized和ReentrantLock的区别
synchronized是和if、else、for、while⼀样的关键字,ReentrantLock是类,这是⼆者的本质区别。既然ReentrantLock是类,那么它就提供了⽐
synchronized更多更灵活的特性,可以被继承、可以有⽅法、可以有各种各样的类变量,ReentrantLock⽐synchronized的扩展性体现在⼏点上:
(1)ReentrantLock可以对获取锁的等待时间进⾏设置,这样就避免了死锁
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活地实现多路通知
另外,⼆者的锁机制其实也是不⼀样的。ReentrantLock底层调⽤的是Unsafe的park⽅法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。
19、ConcurrentHashMap的并发度是什么
织梦模板怎么入侵ConcurrentHashMap的并发度就是segment的⼤⼩,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最⼤优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?
20、ReadWriteLock是什么
⾸先明确⼀下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使⽤ReentrantLock,可能本⾝是为了防⽌线程A在写数据、线程B在读数据造成的数据不⼀致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
因为这个,才诞⽣了读写锁ReadWriteLock。ReadWriteLock是⼀个读写锁接⼝,ReentrantReadWriteLock是ReadWriteLock接⼝的⼀个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
21、FutureTask是什么
这个其实前⾯有提到过,FutureTask表⽰⼀个异步运算的任务。FutureTask⾥⾯可以传⼊⼀个Callable的具体实现类,可以对这个异步运算的任务的结果进⾏等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接⼝的实现类,所以FutureTask 也可以放⼊线程池中。
22、Linux环境下如何查哪个线程使⽤CPU最长
这是⼀个⽐较偏实践的问题,这种问题我觉得挺有意义的。可以这么做:
(1)获取项⽬的pid,jps或者ps -ef | grep java,这个前⾯有讲过
(2)top -H -p pid,顺序不能改变
这样就可以打印出当前的项⽬,每条线程占⽤CPU时间的百分⽐。注意这⾥打出的是LWP,也就是操作系统原⽣线程的线程号,我笔记本⼭没有部署Linux环境下的Java⼯程,因此没有办法截图演⽰,⽹友朋友们如果公司是使⽤Linux环境部署项⽬的话,可以尝试⼀下。
使⽤"top -H -p pid"+"jps pid"可以很容易地到某条占⽤CPU⾼的线程的线程堆栈,从⽽定位占⽤CPU⾼的原因,⼀般是因为不当的代码操作导致了死循环。
最后提⼀点,"top -H -p pid"打出来的LWP是⼗进制的,"jps pid"打出来的本地线程号是⼗六进制的,转换⼀下,就能定位到占⽤CPU⾼的线程的当前线程堆栈了。
23、Java编程写⼀个会导致死锁的程序
第⼀次看到这个题⽬,觉得这是⼀个⾮常好的问题。很多⼈都知道死锁是怎么⼀回事⼉:线程A和线程B相互等待对⽅持有的锁导致程序⽆限死循环下去。当然也仅限于此了,问⼀下怎么写⼀个死锁的程序
就不知道了,这种情况说⽩了就是不懂什么是死锁,懂⼀个理论就完事⼉了,实践中碰到死锁的问题基本上是看不出来的。
真正理解什么是死锁,这个问题其实不难,⼏个步骤:
(1)两个线程⾥⾯分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;
(2)线程1的run()⽅法中同步代码块先获取lock1的对象锁,Thread.sleep(xxx),时间不需要太多,50毫秒差不多了,然后接着获取lock2的对象锁。这么做主要是为了防⽌线程1启动⼀下⼦就连续获得了lock1和lock2两个对象的对象锁
(3)线程2的run)(⽅法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁,当然这时lock1的对象锁已经被线程1锁持有,线程2肯定是要等待线程1释放lock1的对象锁的
这样,线程1"睡觉"睡完,线程2已经获取了lock2的对象锁了,线程1此时尝试获取lock2的对象锁,便被阻塞,此时⼀个死锁就形成了。代码就不写了,占的篇幅有点多,Java多线程7:死锁这篇⽂章⾥⾯有,就是上⾯步骤的代码实现。
24、怎么唤醒⼀个阻塞的线程
如果线程是因为调⽤了wait()、sleep()或者join()⽅法⽽导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,⽆能为⼒,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
25、不可变对象对多线程有什么帮助
前⾯有提到过的⼀个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进⾏额外的同步⼿段,提升了代码执⾏效率。
26、什么是多线程的上下⽂切换
多线程的上下⽂切换是指CPU控制权由⼀个已经正在运⾏的线程切换到另外⼀个就绪并等待获取CPU执⾏权的线程的过程。
27、如果你提交任务时,线程池队列已满,这时会发⽣什么
这⾥区分⼀下:
1. 如果使⽤的是⽆界队列LinkedBlockingQueue,也就是⽆界队列的话,没关系,继续添加任务到阻塞队列中等待执⾏,因为
LinkedBlockingQueue可以近乎认为是⼀个⽆穷⼤的队列,可以⽆限存放任务
2. 如果使⽤的是有界队列⽐如ArrayBlockingQueue,任务⾸先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据
maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使⽤拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy
28、Java中⽤到的线程调度算法是什么
抢占式。⼀个线程⽤完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出⼀个总的优先级并分配下⼀个时间⽚给某个线程执⾏。
29、Thread.sleep(0)的作⽤是什么
这个问题和上⾯那个问题是相关的,我就连在⼀起了。由于Java采⽤抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级⽐较低的线程也能获取到CPU控制权,可以使⽤Thread.sleep(0)⼿动触发⼀次操作系统分配时间⽚的操作,这也是平衡CPU控制权的⼀种操作。
30、什么是⾃旋
很多synchronized⾥⾯的代码只是⼀些很简单的代码,执⾏时间⾮常快,此时等待的线程都加锁可能是⼀种不太值得的操作,因为线程阻塞涉及到⽤户态和内核态切换的问题。既然synchronized⾥⾯的代码执⾏得⾮常快,不妨让等待锁的线程不要被阻塞,⽽是在synchronized的
边界做忙循环,这就是⾃旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是⼀种更好的策略。
31、什么是Java内存模型
Java内存模型定义了⼀种多线程访问Java内存的规范。Java内存模型要完整讲不是这⾥⼏句话能说清楚的,我简单总结⼀下Java内存模型的⼏部分内容:
(1)Java内存模型将内存分为了主内存和⼯作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程⽤到这些主内存中的变量的时候,会读⼀次主内存中的变量,并让这些内存在⾃⼰的⼯作内存中有⼀份拷贝,运⾏⾃⼰线程代码的时候,⽤到这些变量,操作的都是⾃⼰⼯作内存中的那⼀份。在线程代码执⾏完毕之后,会将最新的值更新到主内存中去
(2)定义了⼏个原⼦操作,⽤于操作主内存和⼯作内存中的变量
(3)定义了volatile变量的使⽤规则
(4)happens-before,即先⾏发⽣原则,定义了操作A必然先⾏发⽣于操作B的⼀些规则,⽐如在同⼀个线程内控制流前⾯的代码⼀定先⾏发⽣于控制流后⾯的代码、⼀个释放锁unlock的动作⼀定先⾏发⽣于后⾯对于同⼀个锁进⾏锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码⼀定是线程⾮安全的
32、什么是CAS
CAS,全称为Compare and Swap,即⽐较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS⼀定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是⼀个不会变的值A,只要某次CAS操作失败,永远都不可能成功。
33、什么是乐观锁和悲观锁
(1)乐观锁:就像它的名字⼀样,对于并发间操作产⽣的线程安全问题持乐观状态,乐观锁认为竞争不总是会发⽣,因此它不需要持有锁,将⽐较-替换这两个动作作为⼀个原⼦操作尝试去修改内存中的变量,如果失败则表⽰发⽣冲突,那么就应该有相应的重试逻辑。
(2)悲观锁:还是像它的名字⼀样,对于并发间操作产⽣的线程安全问题持悲观状态,悲观锁认为竞争总是会发⽣,因此每次对某资源进⾏操作时,都会持有⼀个独占的锁,就像synchronized,不管三七⼆⼗⼀,直接上了锁就操作资源了。
34、什么是AQS
简单说⼀下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
如果说urrent的基础是CAS的话,那么AQS就是整个Java并发包的核⼼了,ReentrantLock、CountDownLatch、Semaphore等等都⽤到了它。AQS实际上以双向队列的形式连接所有的Entry,⽐⽅说ReentrantLock,所有等待的线程都被放在⼀个Entry中并连成双向队列,前⾯⼀个线程使⽤ReentrantLock好了,则双向队列实际上的第⼀个Entry开始运⾏。
AQS定义了对双向队列所有的操作,⽽只开放了tryLock和tryRelease⽅法给开发者使⽤,开发者可以根据⾃⼰的实现重写tryLock和tryRelease⽅法,以实现⾃⼰的并发功能。
35、单例模式的线程安全性
⽼⽣常谈的问题了,⾸先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建⼀次出来。单例模式有很多种的写法,我总结⼀下:
(1)饿汉式单例模式的写法:线程安全
(2)懒汉式单例模式的写法:⾮线程安全
(3)双检锁单例模式的写法:线程安全
36、Semaphore有什么作⽤
Semaphore就是⼀个信号量,它的作⽤是限制某段代码块的并发数。
Semaphore有⼀个构造函数,可以传⼊⼀个int型整数n,表⽰某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执⾏完毕这段代码块,下⼀个线程再进⼊。由此可以看出如果Semaphore构造函数中传⼊的int型整数n=1,相当于变成了⼀个synchronized了。
37、Hashtable的size()⽅法中明明只有⼀条语句"return count",为什么还要做同步?
这是我之前的⼀个困惑,不知道⼤家有没有想过这个问题。某个⽅法中如果有多条语句,并且都在操作同⼀个类变量,那么在多线程环境下不加锁,势必会引发线程安全问题,这很好理解,但是size()⽅法明明只有⼀条语句,为什么还要加锁?
关于这个问题,在慢慢地⼯作、学习中,有了理解,主要原因有两点:
(1)同⼀时间只能有⼀条线程执⾏固定类的同步⽅法,但是对于类的⾮同步⽅法,可以多条线程同时访问。所以,这样就有问题了,可能
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论