目录
14、什么是 Callable 和 Future?
15、什么是 FutureTask?使用 ExecutorService 启动任务。
16、什么是并发容器的实现?
17、多线程同步和互斥有几种实现方法,都是什么?
18、什么是竞争条件?你怎样发现和解决竞争?
19、你将如何使用 thread dump?你将如何分析 Thread dump?
20、为什么我们调用 start()方法时会执行 run()方法,为什么我们不能直接调用 run()方法?
21、Java 中你怎样唤醒一个阻塞的线程?
22、在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?
23、什么是不可变对象,它对写并发应用有什么帮助?
24、什么是多线程中的上下文切换?
25、Java 中用到的线程调度算法是什么?
14、什么是 Callable 和 Future?
Callable
接口类似于
Runnable
,从名字就可以看出来了,但是
Runnable
不会返回结果,并且无法抛出返回结果的异常,而 Callable
功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future
拿到,也就是说,
Future
可以拿到异步执行任务的返回值。
可以认为是带有回调的
Runnable
。
Future
接口表示异步任务,是还没有完成的任务给出的未来结果。所以说
Callable用于产生结果,Future
用于获取结果。
15、什么是 FutureTask?使用 ExecutorService 启动任务。
在
Java
并发程序中
FutureTask
表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get
方法将会阻塞。一个
FutureTask
对象可以对调用了 Callable
和
Runnable
的对象进行包装,由于
FutureTask
也是调用了
Runnable接口所以它可以提交给 Executor
来执行。
16、什么是并发容器的实现?
何为同步容器:可以简单地理解为通过
synchronized
来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector
,
Hashtable
,以及 Collections.synchronizedSet
,
synchronizedList
等方法返回的容器。可以通过查看 Vector
,
Hashtable
等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized
。
并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap
中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map
,并且执行读操作的线程和写操作的线程也可以并发的访问 map
,同时允许一定数量的写操作线程并发地修改 map
,所以它可以在并发环境下实现更高的吞吐量。
17、多线程同步和互斥有几种实现方法,都是什么?
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
18、什么是竞争条件?你怎样发现和解决竞争?
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件(race condition
)。
19、你将如何使用 thread dump?你将如何分析 Thread dump?
新建状态(
New
)
用
new
语句创建的线程处于新建状态,此时它和其他
Java
对象一样,仅仅在堆区中被分配了内存。
就绪状态(
Runnable
)
当一个线程对象创建后,其他线程调用它的
start()
方法,该线程就进入就绪状态,Java 虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得 CPU
的使用权。
运行状态(
Running
)
处于这个状态的线程占用
CPU
,执行程序代码。只有处于就绪状态的线程才有机会转到运行状态。
阻塞状态(
Blocked
)
阻塞状态是指线程因为某些原因放弃
CPU
,暂时停止运行。当线程处于阻塞状态时,Java
虚拟机不会给线程分配
CPU
。直到线程重新进入就绪状态,它才有机会转到运行状态。
阻塞状态可分为以下
3
种:
位于对象等待池中的阻塞状态(
Blocked in object
’
s wait pool
):
当线程处于运行状态时,如果执行了某个对象的
wait()
方法,
Java
虚拟机就会把线程放到这个对象的等待池中,这涉及到“
线程通信
”
的内容。
位于对象锁池中的阻塞状态(
Blocked in object
’
s lock pool
):
当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java
虚拟机就会把这个线程放到这个对象的锁池中,这涉及到“
线程同步
”
的内容。
其他阻塞状态(
Otherwise Blocked
):
当前线程执行了
sleep()
方法,或者调用了其他线程的
join()
方法,或者发出了
I/O请求时,就会进入这个状态。
死亡状态(
Dead
)
当线程退出
run()
方法时,就进入死亡状态,该线程结束生命周期。
我们运行之前的那个死锁代码
SimpleDeadLock.java
,然后尝试输出信息
(
/* 时间,jvm 信息 */
2017-11-01 17:36:28
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed
mode):
/* 线程名称:DestroyJavaVM
编号:#13
优先级:5
系统优先级:0
jvm 内部线程 id:0x0000000001c88800
对应系统线程 id(NativeThread ID):0x1c18
线程状态: waiting on condition [0x0000000000000000] (等待某个条件)
线程详细状态:java.lang.Thread.State: RUNNABLE 及之后所有*/
"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000001c88800
nid=0x1c18 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread-1" #12 prio=5 os_prio=0 tid=0x0000000018d49000
nid=0x17b8 waiting for monitor entry [0x0000000019d7f000]
/* 线程状态:阻塞(在对象同步上)
代码位置:at
com.dljd.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)
等待锁:0x00000000d629b4d8
已经获得锁:0x00000000d629b4e8*/
java.lang.Thread.State: BLOCKED (on object monitor)
at
com.dljd.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)
- waiting to lock <0x00000000d629b4d8> (a java.lang.Object)
- locked <0x00000000d629b4e8> (a java.lang.Object)
"Thread-0" #11 prio=5 os_prio=0 tid=0x0000000018d44000 nid=0x1ebc
waiting for monitor entry [0x000000001907f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at
com.dljd.interview.SimpleDeadLock$A.run(SimpleDeadLock.java:34)
- waiting to lock <0x00000000d629b4e8> (a java.lang.Object)
- locked <0x00000000d629b4d8> (a java.lang.Object)
"Service Thread" #10 daemon prio=9 os_prio=0
tid=0x0000000018ca5000 nid=0x1264 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread2" #9 daemon prio=9 os_prio=2
tid=0x0000000018c46000 nid=0xb8c waiting on condition
[0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #8 daemon prio=9 os_prio=2
tid=0x0000000018be4800 nid=0x1db4 waiting on condition
[0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #7 daemon prio=9 os_prio=2
tid=0x0000000018be3800 nid=0x810 waiting on condition
[0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0
tid=0x0000000018bcc800 nid=0x1c24 runnable [0x00000000193ce000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at
java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x00000000d632b928> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x00000000d632b928> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at
com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:6
4)
"Attach Listener" #5 daemon prio=5 os_prio=2
tid=0x0000000017781800 nid=0x524 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=2
tid=0x000000001778f800 nid=0x1b08 waiting on condition
[0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001776a800
nid=0xdac in Object.wait() [0x0000000018b6f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d6108ec8> (a
java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
- locked <0x00000000d6108ec8> (a
java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
"Reference Handler" #2 daemon prio=10 os_prio=2
tid=0x0000000017723800 nid=0x1670 in Object.wait()
[0x00000000189ef000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d6106b68> (a
java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x00000000d6106b68> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=2 tid=0x000000001771b800 nid=0x604 runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000001c9d800
nid=0x9f0 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000000001c9f000
nid=0x154c runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000000001ca0800
nid=0xcd0 runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000000001ca2000
nid=0x1e58 runnable
"VM Periodic Task Thread" os_prio=2 tid=0x0000000018c5a000
nid=0x1b58 waiting on condition
JNI global references: 33
/* 此处可以看待死锁的相关信息! */
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000017729fc8 (object
0x00000000d629b4d8, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000000017727738 (object
0x00000000d629b4e8, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
==============================================
=====
"Thread-1":
at
com.dljd.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)
-
-
waiting to lock <0x00000000d629b4d8> (a java.lang.Object)
locked <0x00000000d629b4e8> (a java.lang.Object)
"Thread-0":
at
com..interview.SimpleDeadLock$A.run(SimpleDeadLock.java:34)
-
-
waiting to lock <0x00000000d629b4e8> (a java.lang.Object)
locked <0x00000000d629b4d8> (a java.lang.Object)
Found 1 deadlock.
/* 内存使用状况,详情得看 JVM 方面的书 */
Heap
PSYoungGen total 37888K, used 4590K [0x00000000d6100000,
0x00000000d8b00000, 0x0000000100000000)
eden space 32768K, 14% used
[
[
[
0x00000000d6100000,0x00000000d657b968,0x00000000d8100000)
from space 5120K, 0% used
0x00000000d8600000,0x00000000d8600000,0x00000000d8b00000)
to space 5120K, 0% used
0x00000000d8100000,0x00000000d8100000,0x00000000d8600000)
ParOldGen
x0000000087600000, 0x00000000d6100000)
object space 86016K, 0% used
0x0000000082200000,0x0000000082200000,0x0000000087600000)
Metaspace used 3474K, capacity 4500K, committed 4864K,
reserved 1056768K
total 86016K, used 0K [0x0000000082200000,
0
[
class space used 382K, capacity 388K, committed 512K, reserved
1048576K
格式有些错误,弄成了图片如下:
20、为什么我们调用 start()方法时会执行 run()方法,为什么我们不能直接调用 run()方法?
当你调用
start()
方法时你将创建新的线程,并且执行在
run()
方法里的代码。
但是如果你直接调用
run()
方法,它不会创建新的线程也不会执行调用线程的代码,只会把 run
方法当作普通方法去执行。
21、Java 中你怎样唤醒一个阻塞的线程?
在
Java
发展史上曾经使用
suspend()
、
resume()
方法对于线程进行阻塞唤醒,但随之出现很多问题,比较典型的还是死锁问题。
解决方案可以使用以对象为目标的阻塞,即利用
Object
类的
wait()
和
notify()
方法实现线程阻塞。
首先,
wait
、
notify
方法是针对对象的,调用任意对象的
wait()
方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()
方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行;其次,wait
、
notify
方法必须在
synchronized
块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait
、
notify
方法的对象是同一个,如此一来在调用 wait
之前当前线程就已经成功获取某对象的锁,执行
wait
阻塞后当 前线程就将之前获取的对象锁释放。
22、在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?
CyclicBarrier
可以重复使用,而
CountdownLatch
不能重复使用。
Java
的
concurrent
包里面的
CountDownLatch
其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是同时只能有一个线程去减这个计数器里面的值。
你可以向
CountDownLatch
对象设置一个初始的数字作为计数值,任何调用这个对象上的 await()
方法都会阻塞,直到这个计数器的计数值被其他的线程减为
0
为止。
所以在当前计数到达零之前,
await
方法会一直受阻塞。之后,会释放所有等待的线程,await
的所有后续调用都将立即返回。这种现象只出现一次
——
计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier
。
CountDownLatch
的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个 CountDownLatch
对象的
await()
方法,其他的任务执行完自己的任务后调用同一个 CountDownLatch
对象上的
countDown()
方法,这个调用 await()
方法的任务将一直阻塞等待,直到这个
CountDownLatch
对象的计数值减到 0
为止。
CyclicBarrier
一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)
。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier
很有用。因为该
barrier
在释放等待线程后可以重用,所以称它为循环 的 barrier
。
23、什么是不可变对象,它对写并发应用有什么帮助?
不可变对象
(Immutable Objects)
即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)
。
不可变对象的类即为不可变类
(Immutable Class)
。
Java
平台类库中包含许多不可变类,如 String
、基本类型的包装类、
BigInteger
和
BigDecimal
等。
不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们的状态无法修改,这些常量永远不会变。
不可变对象永远是线程安全的。
只有满足如下状态,一个对象才是不可变的;
它的状态不能在创建后再被修改;
所有域都是
final
类型;并且,它被正确创建(创建期间没有发生 this
引用的逸出)。
24、什么是多线程中的上下文切换?
在上下文切换过程中,
CPU
会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的“
页码
”
信息是保存在进程控制块(
PCB
)中的。
PCB
还经常被称作“
切换桢
”
(
switchframe
)。
“
页码
”
信息会一直保存到
CPU
的内存中,直到他们被再次使用。
上下文切换是存储和恢复
CPU
状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。
25、Java 中用到的线程调度算法是什么?
计算机通常只有一个
CPU,
在任意时刻只能执行一条机器指令
,
每个线程只有获得CPU 的使用权才能执行指令
.
所谓多线程的并发运行
,
其实是指从宏观上看
,
各个线程轮流获得 CPU
的使用权
,
分别执行各自的任务
.
在运行池中
,
会有多个处于就绪状态的线程在等待 CPU,JAVA
虚拟机的一项任务就是负责线程的调度
,
线程调度是指按照特定机制为多个线程分配 CPU
的使用权
.
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得
cpu
的使用权
,
并且平均分配每个线程占用的 CPU
的时间片这个也比较好理解。
java
虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃
CPU
。
要想了解更多:
千题千解·Java面试宝典_时光の尘的博客-CSDN博客