JAVA
试题
解惑
系列
话说
多线程
JAVA面试题解惑系列(十)——话说多线程
关键字: java 面试题 多线程 thread 线程池 synchronized 死锁
作者:臧圩人(zangweiren) 5 Y, W' B' g( q! l1 q7 v- P7 ]9 W
网址:
4 D+ h2 a7 Z `6 k; J
>>>转载请注明出处!<<<
3 I! n! X/ p8 s$ }9 w$ y
线程或者说多线程,是我们处理多任务的强大工具。线程和进程是不同的,每个进程都是一个独立运行的程序,拥有自己的变量,且不同进程间的变量不能共享;而线程是运行在进程内部的,每个正在运行的进程至少有一个线程,而且不同的线程之间可以在进程范围内共享数据。也就是说进程有自己独立的存储空间,而线程是和它所属的进程内的其他线程共享一个存储空间。线程的使用可以使我们能够并行地处理一些事情。线程通过并行的处理给用户带来更好的使用体验,比如你使用的邮件系统(outlook、Thunderbird、foxmail等),你当然不希望它们在收取新邮件的时候,导致你连已经收下来的邮件都无法阅读,而只能等待收取邮件操作执行完毕。这正是线程的意义所在。
实现线程的方式
实现线程的方式有两种:
继承java.lang.Thread,并重写它的run()方法,将线程的执行主体放入其中。 * r* v: m: K+ p
实现java.lang.Runnable接口,实现它的run()方法,并将线程的执行主体放入其中。 - N$ d+ b% d3 V# p
7 y3 n/ z, c: r; R
这是继承Thread类实现线程的示例: 1 ]6 J, F6 x* k3 c
Java代码
public class ThreadTest extends Thread { 8 C- Y2 T0 H. r% `0 P% f
public void run() {
// 在这里编写线程执行的主体 0 _- X. S; {2 ^+ v7 I
// do something
} ; h& ^# n" z) F7 J2 s9 ?
} 5 \( P2 u' R2 H! B
P3 Z- w# W1 I
public class ThreadTest extends Thread {' M- A4 M3 }+ p, i
public void run() {
// 在这里编写线程执行的主体
// do something6 V$ c4 z% q$ E7 W" W$ |0 v
}
}* O7 A6 Y0 d9 ?0 H9 F5 p
I" p& s8 n# X9 }1 ~1 {
这是实现Runnable接口实现多线程的示例: : f- R7 g. H P5 z+ s4 H, A( _ @
Java代码 5 c* H+ I2 P7 z
public class RunnableTest implements Runnable { , O8 F% [* v" e2 G! Z7 d! T8 Q
public void run() { % F* p( J% Z+ j' g- v! h
// 在这里编写线程执行的主体 ! C3 [" u) k1 v. d% R
// do something
} " G( _" w# A& K* O+ d. ?! @
}
public class RunnableTest implements Runnable {
public void run() {% |4 S% u& k3 c
// 在这里编写线程执行的主体
// do something
}% ?5 p# {" k m; x& w/ ?
}
这两种实现方式的区别并不大。继承Thread类的方式实现起来较为简单,但是继承它的类就不能再继承别的类了,因此也就不能继承别的类的有用的方法了。而使用是想Runnable接口的方式就不存在这个问题了,而且这种实现方式将线程主体和线程对象本身分离开来,逻辑上也较为清晰,所以推荐大家更多地采用这种方式。 . s' I! {! c- t1 d0 q% K
) \6 I6 E# W. m
如何启动线程 3 @+ {! B+ N5 m
我们通过以上两种方式实现了一个线程之后,线程的实例并没有被创建,因此它们也并没有被运行。我们要启动一个线程,必须调用方法来启动它,这个方法就是Thread类的start()方法,而不是run()方法(既不是我们继承Thread类重写的run()方法,也不是实现Runnable接口的run()方法)。run()方法中包含的是线程的主体,也就是这个线程被启动后将要运行的代码,它跟线程的启动没有任何关系。上面两种实现线程的方式在启动时会有所不同。 : l& c3 K1 v, w+ b6 ~0 d4 J
继承Thread类的启动方式:
Java代码 ! b6 z* f$ o, y- |% T" f5 Y, V4 p" T
public class ThreadStartTest {
public static void main(String[] args) { ; K4 p. @, `' Z
// 创建一个线程实例
ThreadTest tt = new ThreadTest();
// 启动线程 T" H! ? H! }3 n- g5 w# E* y" ~
tt.start();
}
}
public class ThreadStartTest {
public static void main(String[] args) {4 v. b; X! p& N7 _: n
// 创建一个线程实例( q$ p2 y! q3 s
ThreadTest tt = new ThreadTest();+ s* X; z0 N, @
// 启动线程
tt.start();
}% Q0 G [4 ]3 g& t9 m! U* r1 M
}
/ G+ i' E/ M ^( p) y8 [
实现Runnable接口的启动方式: ) c# G0 _3 ?5 }. V
Java代码
public class RunnableStartTest {
public static void main(String[] args) {
// 创建一个线程实例 * L8 r0 ^1 L9 A9 k4 \" V# L
Thread t = new Thread(new RunnableTest());
// 启动线程
t.start(); 5 p L7 V- T0 D# J
} 2 x" m# v1 V$ S% @& O
}
& F( d% y" a- D) M& {7 e8 t
public class RunnableStartTest {. ~* z3 l- c, o; p
public static void main(String[] args) {! |+ A7 ~2 @8 q+ e" v
// 创建一个线程实例
Thread t = new Thread(new RunnableTest());3 t& e! J6 U: T+ Y9 x1 n( J5 A$ O
// 启动线程
t.start();
}: }1 T" t8 ]& A3 s7 S( @9 I
}- }0 P. W7 L$ k2 s8 s" y/ ?8 P
/ a7 z$ d6 u0 V+ ~% z8 W
实际上这两种启动线程的方式原理是一样的。首先都是调用本地方法启动一个线程,其次是在这个线程里执行目标对象的run()方法。那么这个目标对象是什么呢?为了弄明白这个问题,我们来看看Thread类的run()方法的实现:
Java代码 : r% g4 v1 U& ]$ G# w8 r
public void run() {
if (target != null) {
target.run();
}
}
7 n' S9 I5 n5 e5 K% E! n
public void run() {8 k- Q1 S7 F- h2 @* w2 f
if (target != null) {6 m3 T; z6 o; `- y# z' S
target.run();0 j; ?; O; y$ V* b+ J, T
}9 X) A' B$ S8 [( B0 t# o
}% h3 u8 S1 I, }: P) Z' Q) x
当我们采用实现Runnable接口的方式来实现线程的情况下,在调用new Thread(Runnable target)构造器时,将实现Runnable接口的类的实例设置成了线程要执行的主体所属的目标对象target,当线程启动时,这个实例的run()方法就被执行了。当我们采用继承Thread的方式实现线程时,线程的这个run()方法被重写了,所以当线程启动时,执行的是这个对象自身的run()方法。总结起来就一句话,线程类有一个Runnable类型的target属性,它是线程启动后要执行的run()方法所属的主体,如果我们采用的是继承Thread类的方式,那么这个target就是线程对象自身,如果我们采用的是实现Runnable接口的方式,那么这个target就是实现了Runnable接口的类的实例。 7 r! U# @$ i8 V+ O( x" E. p
线程的状态 ) q0 W8 v! D, ?* y* T1 v: _
在Java 1.4及以下的版本中,每个线程都具有新建、可运行、阻塞、死亡四种状态,但是在Java 5.0及以上版本中,线程的状态被扩充为新建、可运行、阻塞、等待、定时等待、死亡六种。线程的状态完全包含了一个线程从新建到运行,最后到结束的整个生命周期。线程状态的具体信息如下: + m' D! n, n' J
NEW(新建状态、初始化状态):线程对象已经被创建,但是还没有被启动时的状态。这段时间就是在我们调用new命令之后,调用start()方法之前。
RUNNABLE(可运行状态、就绪状态):在我们调用了线程的start()方法之后线程所处的状态。处于RUNNABLE状态的线程在JAVA虚拟机(JVM)上是运行着的,但是它可能还正在等待操作系统分配给它相应的运行资源以得以运行。 * d. N5 q, g% e( R1 R
BLOCKED(阻塞状态、被中断运行):线程正在等待其它的线程释放同步锁,以进入一个同步块或者同步方法继续运行;或者它已经进入了某个同步块或同步方法,在运行的过程中它调用了某个对象继承自java.lang.Object的wait()方法,正在等待重新返回这个同步块或同步方法。
WAITING(等待状态):当前线程调用了java.lang.Object.wait()、java.lang.Thread.join()或者java.util.concurrent.locks.LockSupport.park()三个中的任意一个方法,正在等待另外一个线程执行某个操作。比如一个线程调用了某个对象的wait()方法,正在等待其它线程调用这个对象的notify()或者notifyAll()(这两个方法同样是继承自Object类)方法来唤醒它;或者一个线程调用了另一个线程的join()(这个方法属于Thread类)方法,正在等待这个方法运行结束。 4 J' C7 W# y- P6 ^6 n
TIMED_WAITING(定时等待状态):当前线程调用了java.lang.Object.wait(long timeout)、java.lang.Thread.join(long millis)、java.util.concurrent.locks.LockSupport.packNanos(long nanos)、java.util.concurrent.locks.LockSupport.packUntil(long deadline)四个方法中的任意一个,进入等待状态,但是与WAITING状态不同的是,它有一个最大等待时间,即使等待的条件仍然没有满足,只要到了这个时间它就会自动醒来。 ) U. A* h4 d, Y. ]( e& v
TERMINATED(死亡状态、终止状态):线程完成执行后的状态。线程执行完run()方法中的全部代码,从该方法中退出,进入TERMINATED状态。还有一种情况是run()在运行过程中抛出了一个异常,而这个异常没有被程序捕获,导致这个线程异常终止进入TERMINATED状态。 # e" \/ {- ~( k* D3 R9 y- L: l
在Java5.0及以上版本中,线程的全部六种状态都以枚举类型的形式定义在java.lang.Thread类中了,代码如下:
Java代码
public enum State { {- L7 u: e0 v: s0 g
NEW,
RUNNABLE,
BLOCKED, # `' f; Q) ~% ?4 Y( f
WAITING,
TIMED_WAITING, 8 N( ]( u% j6 W! {
TERMINATED;
} 7 q8 Q! {6 x" H7 M1 b
& \. @2 R3 l) g* G P8 g
public enum State {% B. C1 g$ r5 h# m1 X
NEW,3 q6 d7 f+ k- W
RUNNABLE,
BLOCKED,
WAITING,* ^$ ?4 y' b8 Q- J1 }
TIMED_WAITING,5 `+ H" t& \$ U' v( e8 ^9 ~
TERMINATED;8 x# h; G( E) O8 x
}1 E, w" o1 R# D1 _! e
sleep()和wait()的区别
0 C. D: p, _) K1 R; r
sleep()方法和wait()方法都成产生让当前运行的线程停止运行的效果,这是它们的共同点。下面我们来详细说说它们的不同之处。
: [0 ~& M5 e& H/ b
sleep()方法是本地方法,属于Thread类,它有两种定义:
Java代码
public static native void sleep(long millis) throws InterruptedException;
{# R9 H a4 Y0 L
public static void sleep(long millis, int nanos) throws InterruptedException { & d2 {2 _7 u3 L2 o. j$ ?
//other code 5 @$ I1 k2 J( i' T
} / u. |9 |) [7 O& ]0 j
public static native void sleep(long millis) throws InterruptedException;
- r4 J: x2 X4 N5 _5 s3 _
public static void sleep(long millis, int nanos) throws InterruptedException {
//other code
}# T- Z9 N9 e; d( v' M5 {+ ]) V
其中的参数millis代表毫秒数(千分之一秒),nanos代表纳秒数(十亿分之一秒)。这两个方法都可以让调用它的线程沉睡(停止运行)指定的时间,到了这个时间,线程就会自动醒来,变为可运行状态(RUNNABLE),但这并不表示它马上就会被运行,因为线程调度机制恢复线程的运行也需要时间。调用sleep()方法并不会让线程释放它所持有的同步锁;而且在这期间它也不会阻碍其它线程的运行。上面的连个方法都声明抛出一个InterruptedException类型的异常,这是因为线程在sleep()期间,有可能被持有它的引用的其它线程调用它的interrupt()方法而中断。中断一个线程会导致一个InterruptedException异常的产生,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。 : Z( r$ r4 K' r! g4 V
为了更好地理解interrupt()效果,我们来看一下下面这个例子: : ]( n, {* s5 a% i0 \* R/ h3 W' w" {
Java代码 0 \' V9 u8 ?0 X( R, \% v
public class InterruptTest { 9 t: Q/ Q/ O9 g- P2 r
public static void main(String[] args) { / W3 H& q1 r6 x& |; s. X: s
Thread t = new Thread() { # G: P" R: Q- l8 M" E7 d3 _
public void run() { " S7 z) [& a) {4 p z8 }; O
try { 2 l; G# c8 M8 r1 F9 @" T/ E2 o
System.out.println("我被执行了-在sleep()方法前"); ; [4 {- T9 J1 [' d7 e
// 停止运行10分钟 ( o7 b0 `' X' u0 X, D
Thread.sleep(1000 * 60 * 10); 0 D; I- F' Y$ O- B
System.out.println("我被执行了-在sleep()方法后");
} catch (InterruptedException e) {
System.out.println("我被执行了-在catch语句块中");
}
System.out.println("我被执行了-在try{}语句块后");
} ) P0 D, L1 H( I" L o/ F
};
// 启动线程 % m2 s7 Y* m) I6 d, V
t.start();
// 在sleep()结束前中断它 6 N( F6 v% S( c: v7 ~- _8 I h% K
t.interrupt();
}
}
: |2 x1 ?* @0 b" b3 b( }- A! R
public class InterruptTest {
public static void main(String[] args) {
Thread t = new Thread() {
public void run() {3 i/ q0 M8 v+ @& y( X0 V
try {; L3 P: n; d9 o" ^% M7 L5 g1 L
System.out.println("我被执行了-在sleep()方法前");
// 停止运行10分钟' A; t' v5 U# x" d
Thread.sleep(1000 * 60 * 10);! [- ^/ L* r3 `% ]; S- ^- z
System.out.println("我被执行了-在sleep()方法后");
} catch (InterruptedException e) {
System.out.println("我被执行了-在catch语句块中");3 e8 n P5 X" \+ [. B$ c
}
System.out.println("我被执行了-在try{}语句块后");
}7 E: V7 v) B& P" r2 d
};
// 启动线程
t.start();# w! M+ L$ f" h( v' g# O
// 在sleep()结束前中断它+ E, Z2 y' \- Z0 Y& t3 Q
t.interrupt();5 C- F3 d# h; U
}
}
1 b+ o$ Y2 \" \: r+ _8 G+ w
运行结果:
我被执行了-在sleep()方法前
我被执行了-在catch语句块中 ) m: y5 t+ d. B: N9 |5 v5 ]+ P- o* ?
我被执行了-在try{}语句块后
: u1 _9 f+ w. u# x$ `8 I
wait()方法也是本地方法,属于Object类,有三个定义:
Java代码
public final void wait() throws InterruptedException {
//do something R; l" Y# D) I) Y9 ^0 a
} # P# ~' c ?% x# X, h" p6 N
) x2 _/ y$ Z& Y/ M% a# t+ l! h# M/ L
public final native void wait(long timeout) throws InterruptedException;
2 p" m _8 D" |3 h' G8 D6 p
public final void wait(long timeout, int nanos) throws InterruptedException { & d, Y( E; k) T& G8 K- o
//do something d' V3 O- F; ^7 `8 ~9 h
}
public final void wait() throws InterruptedException {2 }- i: ^# ]/ e. J7 e& e
//do something) C, p( s' G A+ C( \
}( l7 U' ]+ \$ w6 _$ H
public final native void wait(long timeout) throws InterruptedException;5 u$ P# f/ L- v# f U, M% [) h
. O1 x1 t3 N* w, H K Q4 a: Y7 U
public final void wait(long timeout, int nanos) throws InterruptedException {4 f+ D. J! S: K; u; Y1 G, q+ ]6 \' |
//do something% e- U4 f1 \$ O; O. F) s) G
}
8 R1 J ~( C: d9 ?6 V0 \
wari()和wait(long timeout,int nanos)方法都是基于wait(long timeout)方法实现的。同样地,timeout代表毫秒数,nanos代表纳秒数。当调用了某个对象的wait()方法时,当前运行的线程就会转入等待状态(WAITING),等待别的线程再次调用这个对象的notify()或者notifyAll()方法(这两个方法也是本地方法)唤醒它,或者到了指定的最大等待时间,线程自动醒来。如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。wait()方法同样会被Thread类的interrupt()方法中断,并产生一个InterruptedException异常,效果同sleep()方法被中断一样。
实现同步的方式 & t# ]8 b* B: |# o9 o& b, O
) r2 F" I( V% O+ v, x
同步是多线程中的重要概念。同步的使用可以保证在多线程运行的环境中,程序不会产生设计之外的错误结果。同步的实现方式有两种,同步方法和同步块,这两种方式都要用到synchronized关键字。 : o4 P8 x9 v) s* j! J2 d% S8 I1 C; @
" t j6 K9 Y: d. h' k0 v* N
给一个方法增加synchronized修饰符之后就可以使它成为同步方法,这个方法可以是静态方法和非静态方法,但是不能是抽象类的抽象方法,也不能是接口中的接口方法。下面代码是一个同步方法的示例:
Java代码
public synchronized void aMethod() {
// do something
} . K8 ^3 I/ R$ U) h, a7 {! C
public static synchronized void anotherMethod() {
// do something ; k; q. [* T; i/ N1 b E
}
public synchronized void aMethod() {9 Y( u/ M0 B$ B# n
// do something9 ^$ E+ ]$ S5 L! k& ^; m: i
}
5 n: u' }# d% d, ^: M/ r- p! D
public static synchronized void anotherMethod() {4 W$ {: t# H, y, ~$ r0 g3 ~
// do something
}
线程在执行同步方法时是具有排它性的。当任意一个线程进入到一个对象的任意一个同步方法时,这个对象的所有同步方法都被锁定了,在此期间,其他任何线程都不能访问这个对象的任意一个同步方法,直到这个线程执行完它所调用的同步方法并从中退出,从而导致它释放了该对象的同步锁之后。在一个对象被某个线程锁定之后,其他线程是可以访问这个对象的所有非同步方法的。
同步块的形式虽然与同步方法不同,但是原理和效果是一致的。同步块是通过锁定一个指定的对象,来对同步块中包含的代码进行同步;而同步方法是对这个方法块里的代码进行同步,而这种情况下锁定的对象就是同步方法所属的主体对象自身。如果这个方法是静态同步方法呢?那么线程锁定的就不是这个类的对象了,也不是这个类自身,而是这个类对应的java.lang.Class类型的对象。同步方法和同步块之间的相互制约只限于同一个对象之间,所以静态同步方法只受它所属类的其它静态同步方法的制约,而跟这个类的实例(对象)没有关系。
下面这段代码演示了同步块的实现方式: . ], y0 Q2 }2 z2 u: c/ T ?+ u
Java代码 ( _4 E* t0 E4 S# k5 e( u2 d
public void test() {
// 同步锁 ( X1 r ]% `# l: Q3 k& j& k( I
String lock = "LOCK"; % L2 p9 K5 g: a3 T8 a
// 同步块
synchronized (lock) { 4 j! M. @; I' ?' |7 t: i- J5 `( W
// do something
} / S4 `8 r1 T m& G1 Z
int i = 0;
// ... $ p8 |( B0 x; l$ Y7 t
}
$ L! g4 ]9 _# J: B( W: ^
public void test() {
// 同步锁
String lock = "LOCK";2 p: y( ]/ l$ X. J1 V S# }( D
2 `0 h7 A: s5 r9 O% }
// 同步块 \: K0 z3 P- G2 f2 H( V4 A: G0 a
synchronized (lock) {7 O) P, B. g5 N
// do something
}: p5 K: K* r7 w2 @
int i = 0;
// ...) V$ u3 x# ]. n7 U- P
}1 g# f2 A. e7 q
对于作为同步锁的对象并没有什么特别要求,任意一个对象都可以。如果一个对象既有同步方法,又有同步块,那么当其中任意一个同步方法或者同步块被某个线程执行时,这个对象就被锁定了,其他线程无法在此时访问这个对象的同步方法,也不能执行同步块。 % U' A, W0 Q; c% h! y1 G$ \8 Z2 V# Z
synchronized和Lock
- [, R5 B) i1 f( }/ Z/ D( }/ m
Lock是一个接口,它位于Java 5.0新增的java.utils.concurrent包的子包locks中。concurrent包及其子包中的类都是用来处理多线程编程的。实现Lock接口的类具有与synchronized关键字同样的功能,但是它更加强大一些。java.utils.concurrent.locks.ReentrantLock是较常用的实现了Lock接口的类。下面是ReentrantLock类的一个应用实例: 8 X$ d) E! K$ X8 b0 o
Java代码
private Lock lock = new ReentrantLock(); # m) I2 U: t' S+ z9 h8 b! a! l. P
public void testLock() {
// 锁定对象 7 F. _% l$ l! Q) n% ]7 B
lock.lock();
try {
// do something
} finally {
// 释放对对象的锁定 , ^$ n& {; \* o$ c8 `
lock.unlock(); " G2 {6 m* _1 H0 O, O9 }
}
} . y0 T$ Q1 V9 }" P6 S5 m
private Lock lock = new ReentrantLock();( X" I. e" o! d
* N3 o6 y9 h4 ?( w- |0 q
public void testLock() {% {; F: i5 H4 ]5 b( R- l
// 锁定对象
lock.lock();
try {
// do something9 b- e ]' j- M, P: i7 c' ~
} finally {6 `* x2 p9 A w8 K- V
// 释放对对象的锁定1 t2 U4 _* T% ], ]
lock.unlock();
}
}
% ^7 J& K. T, m! ^2 P; I. t
lock()方法用于锁定对象,unlock()方法用于释放对对象的锁定,他们都是在Lock接口中定义的方法。位于这两个方法之间的代码在被执行时,效果等同于被放在synchronized同步块中。一般用法是将需要在lock()和unlock()方法之间执行的代码放在try{}块中,并且在finally{}块中调用unlock()方法,这样就可以保证即使在执行代码抛出异常的情况下,对象的锁也总是会被释放,否则的话就会为死锁的产生增加可能。
7 A: d8 {5 `& u9 m3 P. j( I
使用synchronized关键字实现的同步,会把一个对象的所有同步方法和同步块看做一个整体,只要有