最全⾯的阿⾥多线程⾯试题,你能回答⼏个?
1、什么是进程,什么是线程,为什么需要多线程编程?
进程是具有⼀定独⽴功能的程序关于某个数据集合上的⼀次运⾏活动,是操作系统进⾏资源分配和调度的⼀个独⽴单位;
线程是进程的⼀个实体,是CPU调度和分派的基本单位,是⽐进程更⼩的能独⽴运⾏的基本单位。线程的划分尺度⼩于进程,这使得多线程程序的并发性⾼;进程在执⾏时通常拥有独⽴的内存单元,⽽线程之间可以共享内存。
使⽤多线程的编程通常能够带来更好的性能和⽤户体验,但是多线程的程序对于其他程序是不友好的,因为它可能占⽤了更多的CPU资源。当然,也不是线程越多,程序的性能就越好,因为线程之间的调度和切换也会浪费CPU时间。时下很时髦的Node.js就采⽤了单线程异步
I/O的⼯作模式。
2、什么是线程安全
如果你的代码在多线程下执⾏和在单线程下执⾏永远都能获得⼀样的结果,那么你的代码就是线程安全的。
这个问题有值得⼀提的地⽅,就是线程安全也是有⼏个级别的:
不可变。像String、Integer、Long这些,都是final类型的类,任何⼀个线程都改变不了它们的值,要改变除⾮新创建⼀个,因此这些不可变对象不需要任何同步⼿段就可以直接在多线程环境下使⽤
绝对线程安全。不管运⾏时环境如何,调⽤者都不需要额外的同步措施。要做到这⼀点通常需要付出许多额外的代价,Java中标注⾃⼰是线程安全的类,实际上绝⼤多数都不是线程安全的,不过绝对线程安全的类,Java中也有,⽐⽅说CopyOnWriteArrayList、CopyOnWriteArraySet
相对线程安全。相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove⽅法都是原⼦操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现
ConcurrentModificationException,也就是fail-fast机制。
线程⾮安全。这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程⾮安全的类
3、编写多线程程序有⼏种实现⽅式?
Java 5以前实现多线程有两种实现⽅法:⼀种是继承Thread类;另⼀种是实现Runnable接⼝。 两种⽅
式都要通过重写run()⽅法来定义线程的⾏为,推荐使⽤后者,因为Java中的继承是单继承,⼀个类有⼀个⽗类,如果继承了Thread类就⽆法再继承其他类了,显然使⽤Runnable接⼝更为灵活。
Java 5以后创建线程还有第三种⽅式:实现Callable接⼝,该接⼝中的call⽅法可以在线程执⾏结束时产⽣⼀个返回值。
4、synchronized关键字的⽤法?
synchronized关键字可以将对象或者⽅法标记为同步,以实现对对象和⽅法的互斥访问,可以⽤synchronized(对象) { … }定义同步代码块,或者在声明⽅法时将synchronized作为⽅法的修饰符。
5、简述synchronized 和urrent.locks.Lock的异同?
Lock是Java 5以后引⼊的新的API,和关键字synchronized相⽐主要相同点:Lock 能完成synchronized所实现的所有功能;主要不同点:Lock有⽐synchronized更精确的线程语义和更好的性能,⽽且不强制性的要求⼀定要获得锁。synchronized会⾃动释放锁,⽽Lock ⼀定要求程序员⼿⼯释放,并且最好在finally 块中释放(这是释放外部资源的最好的地⽅)。
6、当⼀个线程进⼊⼀个对象的synchronized⽅法A之后,其它线程是否可进⼊此对象的synchronized⽅法B?
不能。其它线程只能访问该对象的⾮同步⽅法,同步⽅法则不能进⼊。因为⾮静态⽅法上的synchronized修饰符要求执⾏⽅法时要获得对象的锁,如果已经进⼊A⽅法说明对象锁已经被取⾛,那么试图进⼊B⽅法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。
7、synchronized和ReentrantLock的区别
synchronized是和if、else、for、while⼀样的关键字,ReentrantLock是类,这是⼆者的本质区别。既然ReentrantLock是类,那么它就提供了⽐synchronized更多更灵活的特性,可以被继承、可以有⽅法、可以有各种各样的类变量,ReentrantLock⽐synchronized的扩展性体现在⼏点上:
1. ReentrantLock可以对获取锁的等待时间进⾏设置,这样就避免了死锁
2. ReentrantLock可以获取各种锁的信息
3. ReentrantLock可以灵活地实现多路通知
另外,⼆者的锁机制其实也是不⼀样的:ReentrantLock底层调⽤的是Unsafe的park⽅法加锁,synchronized操作的应该是对象头中mark word.
8、举例说明同步和异步。
如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另⼀个线程读到,或者正在读的数据可能已经被另⼀个线程写过了,那么这些数据就必须进⾏同步存取(数据库操作中的排他锁就是最好的例⼦)。当应⽤程序在对象上调⽤了⼀个需要花费很长时间来执⾏的⽅法,并且不希望让程序等待⽅法的返回时,就应该使⽤异步编程,在很多情况下采⽤异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,⽽异步就是⾮阻塞式操作。
9、启动⼀个线程是调⽤run()还是start()⽅法?
启动⼀个线程是调⽤start()⽅法,使线程所代表的虚拟处理机处于可运⾏状态,这意味着它可以由JVM 调度并执⾏,这并不意味着线程就会⽴即运⾏。run()⽅法是线程启动后要进⾏回调(callback)的⽅法。
10、为什么需要run()和start()⽅法,我们可以只⽤run()⽅法来完成任务吗?
我们需要run()&start()这两个⽅法是因为JVM创建⼀个单独的线程不同于普通⽅法的调⽤,所以这项⼯作由线程的start⽅法来完成,start 由本地⽅法实现,需要显⽰地被调⽤,使⽤这俩个⽅法的另外⼀个好处是任何⼀个对象都可以作为线程运⾏,只要实现了Runnable接⼝,这就避免因继承了Thread类⽽造成的Java的多继承问题。
11、什么是线程池(thread pool)?
在⾯向对象编程中,创建和销毁对象是很费时间的,因为创建⼀个对象要获取内存资源或者其它更多资源。
在Java中更是如此,虚拟机将试图跟踪每⼀个对象,以便能够在对象销毁后进⾏垃圾回收。所以提⾼服务程序效率的⼀个⼿段就是尽可能减少创建和销毁对象的次数,特别是⼀些很耗资源的对象创建和销毁,这就是“池化资源”技术产⽣的原因。线程池顾名思义就是事先创建若⼲个可执⾏的线程放⼊⼀个池(容器)中,需要的时候从池中获取线程不⽤⾃⾏创建,使⽤完毕不需要销毁线程⽽是放回池中,从⽽减少创建和销毁线程对象的开销。
Java 5+中的Executor接⼝定义⼀个执⾏线程的⼯具。它的⼦类型即线程池接⼝是ExecutorService。要配置⼀个线程池是⽐较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在⼯具类Executors⾯提供了⼀些静态⼯⼚⽅法,⽣成⼀些常⽤的线程池,如下所⽰:
newSingleThreadExecutor:创建⼀个单线程的线程池。这个线程池只有⼀个线程在⼯作,也就是相当于单线程串⾏执⾏所有任务。
如果这个唯⼀的线程因为异常结束,那么会有⼀个新的线程来替代它。此线程池保证所有任务的执⾏顺序按照任务的提交顺序执⾏。
newFixedThreadPool:创建固定⼤⼩的线程池。每次提交⼀个任务就创建⼀个线程,直到线程达到线程池的最⼤⼤⼩。线程池的⼤⼩⼀旦达到最⼤值就会保持不变,如果某个线程因为执⾏异常⽽结束,那么线程池会补充⼀个新线程。
newCachedThreadPool:创建⼀个可缓存的线程池。如果线程池的⼤⼩超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执⾏任务)的线程,当任务数增加时,此线程池⼜可以智能的添加新线程来处理任务。此线程池不会对线程池⼤⼩做限制,线程池⼤⼩完全依赖于操作系统(或者说JVM)能够创建的最⼤线程⼤⼩。
newScheduledThreadPool:创建⼀个⼤⼩⽆限的线程池。此线程池⽀持定时以及周期性执⾏任务的需求。
newSingleThreadExecutor:创建⼀个单线程的线程池。此线程池⽀持定时以及周期性执⾏任务的需求。
12、线程的基本状态以及状态之间的关系?
其中Running表⽰运⾏状态;Runnable表⽰就绪状态(万事俱备,只⽋CPU);Blocked表⽰阻塞状态;阻塞状态⼜有多种情况,可能是因为调⽤wait()⽅法进⼊等待池,也可能是执⾏同步⽅法或同步代码块进⼊等锁池,或者是调⽤了sleep()⽅法或join()⽅法等待休眠或其他线程结束,或是因为发⽣了I/O中断。
13、Java中如何实现序列化,有什么意义?
序列化就是⼀种⽤来处理对象流的机制,所谓对象流也就是将对象的内容进⾏流化。可以对流化后的
对象进⾏读写操作,也可将流化后的对象传输于⽹络之间。序列化是为了解决对象流读写操作时可能引发的问题(如果不进⾏序列化可能会存在数据乱序的问题)。
要实现序列化,需要让⼀个类实现Serializable接⼝,该接⼝是⼀个标识性接⼝,标注该类对象是可被序列化的,然后使⽤⼀个输出流来构造⼀个对象输出流并通过writeObject(Object)⽅法就可以将实现对象写出(即保存其状态);如果需要反序列化则可以⽤⼀个输⼊流建⽴对象输⼊流,然后通过readObject⽅法从流中读取对象。序列化除了能够实现对象的持久化之外,还能够⽤于对象的深度克隆。
14、产⽣死锁的条件
1. 互斥条件:⼀个资源每次只能被⼀个进程使⽤。
2. 请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放
3. 不剥夺条件:进程已获得的资源,在末使⽤完之前,不能强⾏剥夺。
4. 循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。
15、什么是线程饿死,什么是活锁?
线程饿死和活锁虽然不想是死锁⼀样的常见问题,但是对于并发编程的设计者来说就像⼀次邂逅⼀样。
当所有线程阻塞,或者由于需要的资源⽆效⽽不能处理,不存在⾮阻塞线程使资源可⽤。JavaAPI中线程活锁可能发⽣在以下情形:
当所有线程在程序中执⾏Object.wait(0),参数为0的wait⽅法。程序将发⽣活锁直到在相应的对象上有线程调⽤ify()或者ifyAll()。
当所有线程卡在⽆限循环中。
16、什么导致线程阻塞
阻塞指的是暂停⼀个线程的执⾏以等待某个条件发⽣(如某资源就绪),学过操作系统的同学对它⼀定已经很熟悉了。Java 提供了⼤量⽅法来⽀持阻塞,下⾯让我们逐⼀分析。
⽅法说明sleep()sleep() 允许 指定以毫秒为单位的⼀段时间作为参数,它使得线程在指定的时间内进⼊阻塞状态,不能得到CPU 时间,指定的时间⼀过,线程重新进⼊可执⾏状态。 典型地,sleep() 被⽤在等待某个资源就绪的情形:测试发现条件不满⾜后,让线程阻塞⼀段时间后重新测试,直到条件满⾜为⽌suspend() 和 resume()两个⽅法配套使⽤,suspend()使得线程进⼊阻塞状态,并且不会⾃动恢
复,必须其对应的resume() 被调⽤,才能使得线程重新进⼊可执⾏状态。典型地,suspend() 和 resume() 被⽤在等待另⼀个线程产⽣的结果的情形:测试发现结果还没有产⽣后,让线程阻塞,另⼀个线程产⽣了结果后,调⽤ resume() 使其恢复。yield()yield() 使当前线程放弃当前已经分得的CPU 时间,但不使当前线程阻塞,即线程仍处于可执⾏状态,随时可能再次分得 CPU 时间。调⽤ yield() 的效果等价于调度程序认为该线程已执⾏了⾜够的时间从⽽转到另⼀个线程。wait() 和 notify()两个⽅法配套使⽤,wait() 使得线程进⼊阻塞状态,它有两种形式,⼀种允许 指定以毫秒为单位的⼀段时间作为参数,另⼀种没有参数,前者当对应的 notify() 被调⽤或者超出指定时间时线程重新进⼊可执⾏状态,后者则必须对应的 notify() 被调⽤.
17、怎么检测⼀个线程是否持有对象监视器
Thread类提供了⼀个holdsLock(Object obj)⽅法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是⼀个static ⽅法,这意味着”某条线程”指的是当前线程。
18、请说出与线程同步以及线程调度相关的⽅法。
wait():使⼀个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():使⼀个正在运⾏的线程处于睡眠状态,是⼀个静态⽅法,调⽤此⽅法要处理InterruptedException异常;
notify():唤醒⼀个处于等待状态的线程,当然在调⽤此⽅法的时候,并不能确切的唤醒某⼀个等待状态的线程,⽽是由JVM确定唤醒哪个线程,⽽且与优先级⽆关;
notityAll():唤醒所有处于等待状态的线程,该⽅法并不是将对象的锁给所有线程,⽽是让它们竞争,只有获得锁的线程才能进⼊就绪状态;
19、sleep() 、join()、yield()有什么区别
sleep()⽅法给其他线程运⾏机会时不考虑线程的优先级,因此会给低优先级的线程以运⾏的机会;yield()⽅法只会给相同优先级或更⾼优先级的线程以运⾏的机会;
线程执⾏sleep()⽅法后转⼊阻塞(blocked)状态,⽽执⾏yield()⽅法后转⼊就绪(ready)状态;
sleep()⽅法声明抛出InterruptedException,⽽yield()⽅法没有声明任何异常;
sleep()⽅法⽐yield()⽅法(跟操作系统CPU调度相关)具有更好的可移植性。
20、wait(),notify()和suspend(),resume()之间的区别
初看起来它们与 suspend() 和 resume() ⽅法对没有什么分别,但是事实上它们是截然不同的。区别的
核⼼在于,前⾯叙述的所有⽅法,阻塞时都不会释放占⽤的锁(如果占⽤了的话),⽽这⼀对⽅法则相反。上述的核⼼区别导致了⼀系列的细节上的区别。
⾸先,前⾯叙述的所有⽅法都⾪属于 Thread 类,但是这⼀对却直接⾪属于 Object 类,也就是说,所有对象都拥有这⼀对⽅法。初看起来这⼗分不可思议,但是实际上却是很⾃然的,因为这⼀对⽅法阻塞时要释放占⽤的锁,⽽锁是任何对象都具有的,调⽤任意对象的 wait()
⽅法导致线程阻塞,并且该对象上的锁被释放。⽽调⽤ 任意对象的notify()⽅法则导致从调⽤该对象的 wait() ⽅法⽽阻塞的线程中随机选择的⼀个解除阻塞(但要等到获得锁后才真正可执⾏)。
其次,前⾯叙述的所有⽅法都可在任何位置调⽤,但是这⼀对⽅法却必须在 synchronized ⽅法或块中调⽤,理由也很简单,只有在synchronized ⽅法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调⽤这⼀对⽅法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这⼀对⽅法调⽤必须放置在这样的 synchronized ⽅法或块中,该⽅法或块的上锁对象就是调⽤这⼀对⽅法的对象。若不满⾜这⼀条件,则程序虽然仍能编译,但在运⾏时会出现IllegalMonitorStateException 异常。
wait() 和 notify() ⽅法的上述特性决定了它们经常和synchronized关键字⼀起使⽤,将它们和操作系统进程间通信机制作⼀个⽐较就会发现它们的相似性:synchronized⽅法或块提供了类似于操作系统原
语的功能,它们的执⾏不会受到多线程机制的⼲扰,⽽这⼀对⽅法则相当于 block 和wakeup 原语(这⼀对⽅法均声明为 synchronized)。它们的结合使得我们可以实现操作系统上⼀系列精妙的进程间通信的算法(如信号量算法),并⽤于解决各种复杂的线程间通信问题。
关于 wait() 和 notify() ⽅法最后再说明两点:
第⼀:调⽤ notify() ⽅法导致解除阻塞的线程是从因调⽤该对象的 wait() ⽅法⽽阻塞的线程中随机选取的,我们⽆法预料哪⼀个线程将会被选择,所以编程时要特别⼩⼼,避免因这种不确定性⽽产⽣问题。
第⼆:除了 notify(),还有⼀个⽅法 notifyAll() 也可起到类似作⽤,唯⼀的区别在于,调⽤ notifyAll() ⽅法将把因调⽤该对象的 wait() ⽅法⽽阻塞的所有线程⼀次性全部解除阻塞。当然,只有获得锁的那⼀个线程才能进⼊可执⾏状态。
谈到阻塞,就不能不谈⼀谈死锁,略⼀分析就能发现,suspend() ⽅法和不指定超时期限的 wait() ⽅法的调⽤都可能产⽣死锁。遗憾的是,Java 并不在语⾔级别上⽀持死锁的避免,我们在编程中必须⼩⼼地避免死锁。
以上我们对 Java 中实现线程阻塞的各种⽅法作了⼀番分析,我们重点分析了 wait() 和 notify() ⽅法,
因为它们的功能最强⼤,使⽤也最灵活,但是这也导致了它们的效率较低,较容易出错。实际使⽤中我们应该灵活使⽤各种⽅法,以便更好地达到我们的⽬的。
21、为什么wait()⽅法和notify()/notifyAll()⽅法要在同步块中被调⽤
这是JDK强制的,wait()⽅法和notify()/notifyAll()⽅法在调⽤前都必须先获得对象的锁
22、wait()⽅法和notify()/notifyAll()⽅法在放弃对象监视器时有什么区别
wait()⽅法和notify()/notifyAll()⽅法在放弃对象监视器的时候的区别在于:wait()⽅法⽴即释放对象监视器,notify()/notifyAll()⽅法则会等待线程剩余代码执⾏完毕才会放弃对象监视器。
23、Runnable和Callable的区别
Runnable接⼝中的run()⽅法的返回值是void,它做的事情只是纯粹地去执⾏run()⽅法中的代码⽽已;Callable接⼝中的call()⽅法是有返回值的,是⼀个泛型,和Future、FutureTask配合可以⽤来获取异步执⾏的结果。
这其实是很有⽤的⼀个特性,因为多线程相⽐单线程更难、更复杂的⼀个重要原因就是因为多线程充满着未知性,某条线程是否执⾏了?某条线程执⾏了多久?某条线程执⾏的时候我们期望的数据是否
已经赋值完毕?⽆法得知,我们能做的只是等待这条多线程的任务执⾏完毕⽽已。⽽Callable+Future/FutureTask却可以⽅便获取多线程运⾏的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务。
24、Thread类的sleep()⽅法和对象的wait()⽅法都可以让线程暂停执⾏,它们有什么区别?
sleep()⽅法(休眠)是线程类(Thread)的静态⽅法,调⽤此⽅法会让当前线程暂停执⾏指定的时间,将执⾏机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会⾃动恢复。
wait()是Object类的⽅法,调⽤对象的wait()⽅法导致当前线程放弃对象的锁(线程暂停执⾏),进⼊对象的等待池(wait pool),只有调⽤对象的notify()⽅法(或notifyAll()⽅法)时才能唤醒等待池中的线程进⼊等锁池(lock pool),如果线程重新获得对象的锁就可以进⼊就绪状态。
25、线程的sleep()⽅法和yield()⽅法有什么区别?
1. sleep()⽅法给其他线程运⾏机会时不考虑线程的优先级,因此会给低优先级的线程以运⾏的机会;yield()⽅法只会给相同优先级或更
⾼优先级的线程以运⾏的机会;
2. 线程执⾏sleep()⽅法后转⼊阻塞(blocked)状态,⽽执⾏yield()⽅法后转⼊就绪(ready)状态;
java接口可以创建对象吗3. sleep()⽅法声明抛出InterruptedException,⽽yield()⽅法没有声明任何异常;
4. sleep()⽅法⽐yield()⽅法(跟操作系统CPU调度相关)具有更好的可移植性。
26、为什么wait,nofity和nofityAll这些⽅法不放在Thread类当中
⼀个很明显的原因是JAVA提供的锁是对象级的⽽不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调⽤对象中的wait()⽅法就有意义了。如果wait()⽅法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
27、怎么唤醒⼀个阻塞的线程
如果线程是因为调⽤了wait()、sleep()或者join()⽅法⽽导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,⽆能为⼒,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
28、什么是多线程的上下⽂切换
多线程的上下⽂切换是指CPU控制权由⼀个已经正在运⾏的线程切换到另外⼀个就绪并等待获取CPU执⾏权的线程的过程。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论