线程相关方法说明总结

前言

今天我们来看下在多线程环境处理中可能会遇到的一些方法,来了解下它们。

正文

sleep

sleep即线程休眠,该方法属于Thread类,是一个static native方法。

调用sleep方法需要指定线程休眠时间。

需要注意的是,执行该方法是让程序暂停一定时间,让出cpu资源,但是它的监控状态依然保持,当指定的时间到了又会自动恢复运行状态,并且在调用该方法的过程中,线程不会释放对象锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class SleepTest {
private static final Object o = new Object();

public static void main(String[] args) {
Thread t1 = new Thread(new MySleep(o));
t1.start();
Thread t2 = new Thread(new MySleep(o));
t2.start();

}

}
class MySleep implements Runnable{
private Object o;

public MySleep(Object o) {
this.o = o;
}

@Override
public void run() {
synchronized (o){
try {
System.out.println(Thread.currentThread().getId()+" --> 开始休眠 start:"+System.currentTimeMillis());
Thread.sleep(5000);
System.out.println(Thread.currentThread().getId()+" --> 休眠结束 end: "+System.currentTimeMillis());
}catch (Exception e){
e.printStackTrace();
}
}
}
}

输出结果:

上述例子我们可以看到,在线程休眠时,线程会一直保持对象锁,其它线程无法拿到该对象锁。

sleepJVM基于操作系统底层的实现而封装实现的。

其原理大家可以了解一下:

  1. 挂起进程(或线程)并修改其运行状态,即让出CPU控制权限;

  2. sleep()提供的参数来设置一个定时器;

  3. 当时间结束,定时器会触发,内核收到中断后修改进程(或线程)的运行状态。例如线程会被标志为就绪而进入就绪队列等待调度。

wait

wait方法也是常被用到的一个方法,其经常和sleep方法一起被问到来比较区别。

首先应该知道的是wait方法属于Object类,是其一个普通方法。

当然,wait最后调用了Object类中的wait(long timeout)方法,它是一个native方法。

1
public final native void wait(long timeout) throws InterruptedException;

需要注意的是,当线程调用wait方法时,该线程会放弃(释放)当前持有的对象锁,同时该线程进入线程等待队列(挂起)。

线程调用wait方法后,我们需要显式的调用notify()notiftAll方法来结束线程的等待状态。

我们来看下下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class WaitTest {
private static final Object o =new Object();

public static void main(String[] args) throws Exception{
Thread t1 = new Thread(new MyWait1(o));
t1.start();
Thread.sleep(2000);
Thread t2 = new Thread(new MyWait2(o));
t2.start();
}
}
class MyWait1 implements Runnable{
private Object o;

public MyWait1(Object o) {
this.o = o;
}

@Override
public void run() {
synchronized (o){
System.out.println(Thread.currentThread().getId() +"获得对象锁");
try {
o.wait();
}catch (Exception e){
e.printStackTrace();
}
try {
System.out.println(Thread.currentThread().getId() +"开始休眠");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getId() +"休眠结束");
}catch (Exception e){
e.printStackTrace();
}

}
}
}
class MyWait2 implements Runnable{
private Object o;

public MyWait2(Object o) {
this.o = o;
}

@Override
public void run() {
synchronized (o){
// doSomething
System.out.println(Thread.currentThread().getId() +"获得对象锁");
}
}
}

可以看到如下输出:

说明线程11获得对象锁,后续执行wait方法后,线程11释放了对象锁,而后被线程12获取。

并且我们看到程序无法停止,MyWait1也不会输出开始休眠或者结束休眠等日志,因为线程11已经被挂起。

此时需要调用notifynotiftAll方法来唤醒线程。

我们可以在线程MyWait2执行完后唤醒MyWait1线程,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyWait2 implements Runnable{
private Object o;

public MyWait2(Object o) {
this.o = o;
}

@Override
public void run() {
synchronized (o){
// doSomething
System.out.println(Thread.currentThread().getId() +"获得对象锁");
//唤醒
o.notify();
}

}
}

输出结果如下:

可以看到线程11被唤醒,执行后续逻辑。

我们来总结下sleepwait方法的不同之处:

  • 属于不同的两个类,sleepThread类里的static native方法,waitObject类里的方法;
  • sleep方法不会释放对象锁,wait方法会释放对象锁;
  • sleep方法可以在任何地方使用,wait方法则只能在同步方法或同步块中使用
  • sleep需要指定休眠时间,wait可以指定时间,也可以不指定,如果不指定时间,需要调用notifynotiftAll方法来唤醒线程;
  • sleep方法使线程进入阻塞状态(线程休眠),wait方法使线程进入等待队列(线程挂起),也就是阻塞类别不同;

join

我们再来看下join方法。

我们知道join方法可以把指定线程加入到当前线程,可以将两个并行的线程合并,让其顺序执行。

那么它是怎么实现的呢?

我们直接来看下源码。

join方法位于Thread类中,如下:

我们上面说了wait方法后,这段代码就非常容易理解了。

可以看到join方法内部调用了wait方法,其实现逻辑就是等加入的线程执行完(或者执行若干时间后)在执行其他线程。

我们来测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class JoinTest {
public static void main(String[] args) {
Thread t1 = new Thread(new MyJoin1());
Thread t2 = new Thread(new MyJoin2());
t1.start();
t2.start();
}

}
class MyJoin1 implements Runnable{

@Override
public void run() {
try {
Thread.sleep(2000);

for (int i=0;i<10;i++){
System.out.println("线程"+Thread.currentThread().getId()+"; 值:"+i);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
class MyJoin2 implements Runnable{

@Override
public void run() {
try {
Thread.sleep(2000);

for (int i=10;i<20;i++){
System.out.println("线程"+Thread.currentThread().getId()+"; 值:"+i);
}
}catch (Exception e){
e.printStackTrace();
}
}
}

我们首先构造两个线程,线程1输出0-9,线程2输出10-19.

我们运行可以看到结果乱序输出。

我们改造上述代码,让线程2加入线程1,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class JoinTest {
public static void main(String[] args) {
Thread t1 = new Thread(new MyJoin1());
//Thread t2 = new Thread(new MyJoin2());
t1.start();
//t2.start();
}

}
class MyJoin1 implements Runnable{

@Override
public void run() {
try {
Thread.sleep(2000);

for (int i=0;i<10;i++){
System.out.println("线程"+Thread.currentThread().getId()+"; 值:"+i);
if(i == 5){
Thread t2 = new Thread(new MyJoin2());
t2.start();
t2.join();
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
class MyJoin2 implements Runnable{

@Override
public void run() {
try {
Thread.sleep(2000);

for (int i=10;i<20;i++){
System.out.println("线程"+Thread.currentThread().getId()+"; 值:"+i);
}
}catch (Exception e){
e.printStackTrace();
}
}
}

可以看到,我们线程1执行判断当i==5时,join线程2.

可以看到线程1执行到5后,线程2开始执行,线程1进入wait,待线程2退出后线程1继续执行。

疑问:不知道大家有没有这个疑问,我们上面说到wait方法时说过,调用wait方法后应该显式调用notifynotiftAll方法来唤醒线程,那么join后,它在哪儿被执行的呢?

解答:我们找到Thread类的exit方法,可以看到如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}

该段代码在线程结束时都会被调用,其中的group.threadTerminated(this)方法内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void threadTerminated(Thread t) {
synchronized (this) {
remove(t);

if (nthreads == 0) {
notifyAll();
}
if (daemon && (nthreads == 0) &&
(nUnstartedThreads == 0) && (ngroups == 0))
{
destroy();
}
}
}

看到了我们所说的notifyAll方法。

也就是比如两个线程1和2,线程2调用join方法加入线程1后,线程1进入wait,等到线程2结束时触发notifyAll,线程1被唤醒,继续执行。

如果是有超时时间的join呢,我们改造下代码,继续来看下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class JoinTest {
public static void main(String[] args) {
Thread t1 = new Thread(new MyJoin1());
//Thread t2 = new Thread(new MyJoin2());
t1.start();
//t2.start();
}

}
class MyJoin1 implements Runnable{

@Override
public void run() {
try {
Thread.sleep(2000);

for (int i=0;i<10;i++){
System.out.println("线程"+Thread.currentThread().getId()+"; 值:"+i);
if(i == 5){
Thread t2 = new Thread(new MyJoin2());
t2.start();
t2.join(2000);
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
class MyJoin2 implements Runnable{

@Override
public void run() {
try {
Thread.sleep(2000);

for (int i=10;i<20;i++){
System.out.println("线程"+Thread.currentThread().getId()+"; 值:"+i);
}
}catch (Exception e){
e.printStackTrace();
}
}
}

这儿可以看到我把join时间设置为了2000ms,线程2需要sleep 2000ms后才会执行。

输出结果如下:

可以看到,线程1继续执行,不会等待线程2执行完后再执行。也就是wait(long timeout)方法当达到超时时间后,会由底层去唤醒线程继续执行。

也就是比如两个线程1和2,线程2调用join方法加入线程1后,此时设置了join的超时时间,线程1进入wait,会wait该超时时间,时间过后会被唤醒,如果此时先从2还没有执行完,会和线程2并行执行。

interrupt

interrupt方法其作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。

但使用时我们需要注意的地方如下。

  • interrupt不能改变正在运行中的线程,它只是改变中断状态而已,具体线程何去何从,是由代码决定的;
  • 相关的两个方法:isInterrupted可以判断线程中断状态;interrupted可以恢复线程中断状态。

我们根据例子来看下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class InterruptTest {
public static void main(String[] args) {
Thread t1 = new Thread(new MyInterrupt());
t1.start();
}
}
class MyInterrupt implements Runnable{

@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("线程 "+Thread.currentThread().getId()+" 中断状态:"+Thread.currentThread().isInterrupted());
if(i == 2){
Thread.currentThread().interrupt();
}
System.out.println("线程 "+Thread.currentThread().getId()+" 正在运行 i = "+i);
}
}
}

上述代码还是比较好理解的,我们在i=2是中断线程,但我们可以看到输出结果是说明中断后线程仍是在运行着的。

因此这种情况下,我们需要结合代码来控制线程的中断、执行等逻辑。

如何中断呢?显然,我们直接在i=2时添加return就行。

可以看到线程执行到i=2时退出。

这个方法不中断正在运行的线程,它有什么作用呢?

其实,interrupt的主要作用目标是阻塞线程,它会给阻塞线程发出中断信号,以结束受阻塞线程的阻塞状态。

或者直接这样说,如果一个线程被Object.waitThread.joinThread.sleep阻塞,那么调用interrupt可以快速结束其阻塞状态,释放资源。

但要注意,此时调用interrupt将会抛出InterruptedException异常,我们需要处理它。

我们以上述例子为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class InterruptTest {
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(new MyInterrupt());
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
class MyInterrupt implements Runnable{

@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("线程 "+Thread.currentThread().getId()+" 中断状态:"+Thread.currentThread().isInterrupted());
if(i == 2){
try {
Thread.sleep(5000);
}catch (InterruptedException e){
e.printStackTrace();
return;
}
}
System.out.println("线程 "+Thread.currentThread().getId()+" 正在运行 i = "+i);
}
}
}

上述代码我们线程启动后,在i=2时进入sleep,让线程中断,可以看到抛出InterruptedException,后续线程的流程由我们异常处理处的方法控制,可以看到我们直接return表示退出线程,线程退出。

需要注意的是,我们也可以先设置中断状态,而后线程阻塞的时候就会直接抛出异常,这两个的先后关系并不重要。

上图我们可以看到在i=1的时候设置中断状态,在i=2的时候尝试阻塞线程,抛出异常,线程结束。

以上就是关于interrupt的全部内容,需要知道的点:

  • 调用interrupt只会改变线程中断状态;
  • 如果线程处于运行状态,则对线程无影响;如果线程处于阻塞或者进入阻塞状态,就会抛出中断异常,我们应该捕获异常并规定线程的后续逻辑。

yield

这个方法用到的不多,我们来了解一下。

yield方法可以使当前线程回到可运行状态,以允许其他线程获得运行机会。但要注意的是,yield无法保证100%让步,因为让步的线程可能还会被线程调度选中。

我们结合例子来看下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class YieldTest {
private static final ExecutorService e = Executors.newFixedThreadPool(5);
public static void main(String[] args) {
for (int i=0;i<5;i++){
e.submit(new MyYield(i));
}
}
}
class MyYield implements Runnable{

private int k;

public MyYield(int k) {
this.k = k;
}

@Override
public void run() {
System.out.println("线程 "+Thread.currentThread().getId()+ " 获得执行机会, k ="+k);
if(k == 0){
Thread.yield();
}
System.out.println("线程 "+Thread.currentThread().getId()+ " 执行完成, k ="+k);
}
}

上述代码,我们构建一个5个线程的定长线程池,每个线程记录序号k,当k=0的时候,就是第一个放入的线程,我们让它yield一下。

我们多次运行测试代码,可以看到如下结果。

可以看到k=0线程作出让步,但线程调度是有可能在调用到它的,所以上述输出结果会多样。这也说明yield不能保证100%让步。

另外,yield让步后,具体哪个线程拿到CPU运行权取决于优先级,但优先级也不能保证100%优先。

如下,我们设置5个线程,设置其优先级为1-5(优先级范围1-10,默认5,越小优先级越低)。

1
2
3
4
5
6
7
8
9
10
@Override
public void run() {
System.out.println("线程 "+Thread.currentThread().getId()+ " 获得执行机会, k ="+k);
Thread.currentThread().setPriority(k+1);
if(k == 0){
Thread.yield();
}

System.out.println("线程 "+Thread.currentThread().getId()+ " 执行完成, k ="+k);
}

可以看到,大多数情况优先级高的会优先执行完成,同时k=0线程优先级最低,还作了yield,理论上会在最后执行完成。

但也会有一些特殊情况。

以上就是关于yield的全部内容。

通常情况下,如果使用yield,其主要作用是可能想让相同优先级的线程之间能适当的轮转执行。

run

run方法来自接口Runnable.

Thread方法中的run实现如下:

1
2
3
4
5
6
7
8
9
/* What will be run. */
private Runnable target;
......
@Override
public void run() {
if (target != null) {
target.run();
}
}

可以看到十分简单,也就是线程Thread里定义了Runnable,当Runnable不为空时,就会调用它的run方法。

我们知道,我们要使用实现Runnable的方法来实现一个线程,必须有如下操作。

1
2
Thread t = new Thread(new Runnable(......));
t.start;

如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RunTest {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRun());
Thread t2 = new Thread(new MyRun());
t1.start();
t2.start();
}
}
class MyRun implements Runnable{

@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println("线程 "+Thread.currentThread().getId()+" i ="+i);
}
}
}

这样两个线程就会并行执行。

如果我们不调用start,直接使用run,就会变为同步执行。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
Thread t1 = new Thread(new MyRun());
Thread t2 = new Thread(new MyRun());
// t1.start();
// t2.start();
t1.run();
t2.run();
}

输出:

其实就是相当于我们仅仅是调用了Thread类的run方法,而没有开启一个线程去执行相关操作。相当于还是主线程在串行做相关事情

我们上面也可以看到,调用start方法后,程序会开启一个线程,然后调用其run方法,所以我们再来看下start方法。

start

上面分析到调用start方法后,程序会开启一个线程执行run方法,我们直接来看相关代码吧。

start方法代码如下:

可以看到,其相关调用run方法的内容应该在原生方法start0里面。

start0方法实际上是通过Thread类里的如下方法registerNatives定义的。

1
2
3
4
5
6
7
8
9
public
class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
......
}

registerNatives方法的原生方法定义在Thread.c文件中,如下:http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/00cd9dc3c2b5/src/share/native/java/lang/Thread.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static JNINativeMethod methods[] = {
{"start0", "()V", (void *)&JVM_StartThread},
{"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
{"isAlive", "()Z", (void *)&JVM_IsThreadAlive},
{"suspend0", "()V", (void *)&JVM_SuspendThread},
{"resume0", "()V", (void *)&JVM_ResumeThread},
{"setPriority0", "(I)V", (void *)&JVM_SetThreadPriority},
{"yield", "()V", (void *)&JVM_Yield},
{"sleep", "(J)V", (void *)&JVM_Sleep},
{"currentThread", "()" THD, (void *)&JVM_CurrentThread},
{"countStackFrames", "()I", (void *)&JVM_CountStackFrames},
{"interrupt0", "()V", (void *)&JVM_Interrupt},
{"isInterrupted", "(Z)Z", (void *)&JVM_IsInterrupted},
{"holdsLock", "(" OBJ ")Z", (void *)&JVM_HoldsLock},
{"getThreads", "()[" THD, (void *)&JVM_GetAllThreads},
{"dumpThreads", "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
};

#undef THD
#undef OBJ
#undef STE

JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

从上述代码中可以看到,调用start0方法会执行JVM_StartThread方法。

我们可以在OpenJDK hotspot源码中找到关于JVM_StartThread方法的定义,其位于jvm.cpp中。

https://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/prims/jvm.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
......
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_StartThread");
JavaThread *native_thread = NULL;

// We cannot hold the Threads_lock when we throw an exception,
// due to rank ordering issues. Example: we might need to grab the
// Heap_lock while we construct the exception.
bool throw_illegal_thread_state = false;

// We must release the Threads_lock before we can post a jvmti event
// in Thread::start.
{
// Ensure that the C++ Thread and OSThread structures aren't freed before
// we operate.
MutexLocker mu(Threads_lock);

// Since JDK 5 the java.lang.Thread threadStatus is used to prevent
// re-starting an already started thread, so we should usually find
// that the JavaThread is null. However for a JNI attached thread
// there is a small window between the Thread object being created
// (with its JavaThread set) and the update to its threadStatus, so we
// have to check for this
if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
throw_illegal_thread_state = true;
} else {
// We could also check the stillborn flag to see if this thread was already stopped, but
// for historical reasons we let the thread detect that itself when it starts running

jlong size =
java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
// Allocate the C++ Thread structure and create the native thread. The
// stack size retrieved from java is signed, but the constructor takes
// size_t (an unsigned type), so avoid passing negative values which would
// result in really large stacks.
size_t sz = size > 0 ? (size_t) size : 0;
native_thread = new JavaThread(&thread_entry, sz);

// At this point it may be possible that no osthread was created for the
// JavaThread due to lack of memory. Check for this situation and throw
// an exception if necessary. Eventually we may want to change this so
// that we only grab the lock if the thread was created successfully -
// then we can also do this check and throw the exception in the
// JavaThread constructor.
if (native_thread->osthread() != NULL) {
// Note: the current thread is not being used within "prepare".
native_thread->prepare(jthread);
}
}
}

if (throw_illegal_thread_state) {
THROW(vmSymbols::java_lang_IllegalThreadStateException());
}

assert(native_thread != NULL, "Starting null thread?");

if (native_thread->osthread() == NULL) {
// No one should hold a reference to the 'native_thread'.
delete native_thread;
if (JvmtiExport::should_post_resource_exhausted()) {
JvmtiExport::post_resource_exhausted(
JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS,
"unable to create new native thread");
}
THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
"unable to create new native thread");
}

Thread::start(native_thread);

JVM_END
.......

上述代码比较多,简化如下:

1
2
3
4
5
6
7
8
9
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_StartThread");
JavaThread *native_thread = NULL;
.......
//创建一个线程
native_thread = new JavaThread(&thread_entry, sz);
......
//启动线程
Thread::start(native_thread);

创建线程关键部分就是native_thread = new JavaThread(&thread_entry, sz);,这个thread_entry的定义我们来看一下。

也是在当前文件里。

1
2
3
4
5
6
7
8
9
10
11
static void thread_entry(JavaThread* thread, TRAPS) {
HandleMark hm(THREAD);
Handle obj(THREAD, thread->threadObj());
JavaValue result(T_VOID);
JavaCalls::call_virtual(&result,
obj,
KlassHandle(THREAD, SystemDictionary::Thread_klass()),
vmSymbols::run_method_name(),
vmSymbols::void_method_signature(),
THREAD);
}

上述方法中有个参数vmSymbols::run_method_name(),定义了回调方法,其回调方法就是回调了Java中的run方法。

我们可以在vmSymbols.hpp里找到其定义,https://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/9d15b81d5d1b/src/share/vm/classfile/vmSymbols.hpp。

可以看到底层回调了run方法。

另外关于底层线程如何启动运行的,这儿暂时不过多叙述,有时间我再整理一下。

stop

这个方法目前已经过时了,在Java源码中,我们可以看到官方给出的过时原因。

上述注释中说明了该方法停掉一个线程将会导致所有已锁定的监听器被解锁(解锁的原因是当 ThreadDeath异常在堆栈中传播时,监视器被解锁),这个之前被监听器锁定的对象被解锁,其他线程就能随意操作这个对象,将导致任何可能的结果。

官方给出的网页说明了不能捕获ThreadDeath异常并修复对象的原因:

  1. 一个线程几乎可以在任何地方抛出一个ThreadDeath异常。考虑到这一点,所有同步的方法和块都必须详细研究处理。
  2. 一个线程可以抛出第二个ThreadDeath异常,同时从第一个线程清除(在 catchfinally 子句中)。清理将不得不重复进行,直到它成功。确保这一点的代码将非常复杂。

所以捕获ThreadDeath异常是不可取的。

我们下面举个例子来看一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class StopTest {
public static void main(String[] args) {
final Object lock = new Object();

try {
Thread t0 = new Thread(() -> {
try {
synchronized (lock) {
System.out.println("thread->" + Thread.currentThread().getName()
+ " acquire lock.");
// sleep for 3s
Thread.sleep(3000);
System.out.println("thread->" + Thread.currentThread().getName()
+ " release lock.");
}
} catch (Throwable ex) {
System.out.println("Caught in run: " + ex);
ex.printStackTrace();
}
});

Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("thread->" + Thread.currentThread().getName()
+ " acquire lock.");
}
});

t0.start();
// Thread.sleep(1000);
// t0.stop();
t1.start();
} catch (Throwable e) {
System.out.println("Caught in main: " + e);
e.printStackTrace();
}
}
}

这段代码的意思是,线程t0和线程t1共享对象lock,开始时t0启动,拿到lock,经过3s后释放lock,而后t1拿到lock

所以输出如下:

1
2
3
thread->Thread-0 acquire lock.
thread->Thread-0 release lock.
thread->Thread-1 acquire lock.

但当我们把上述注释部分打开时,即线程t0获得锁后stop,此时其会释放对象锁,t1获取到了锁,此时t0又尝试继续操作,就会出现异常。

如果想实现stop的相关功能,可以使用上面的interrupt方法来实现。

其它过时方法

除了上面说到的stop,线程类中的suspendresumedestroy等方法也已经过时弃用了。

它们的弃用原因大家可以看下这篇官方文档,这儿我就不过多叙述了。

Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?

其原因主要是不安全、容易死锁等。

官方给出的策略是:

  1. 对于stop方法,可以使用interrupt或者sleep等替代;
  2. 对于suspendresume 方法,可以使用 Object.waitObject.notify来实现;
  3. destroy方法就从来没被实现过。

我们来看下suspend造成死锁的情况,其实主要原因就是suspend不会释放对象锁,因此可能造成死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class SuspendAndResumeTest {
final static Object o1 = new Object();
final static Object o2 = new Object();

public static void main(String[] args) {
Thread t1 = new Thread(new MySuspend1(o1,o2));
Thread t2 = new Thread(new MySuspend2(o1,o2));
t1.start();
t2.start();
t1.resume();
}
}
class MySuspend1 implements Runnable{

private Object o1;
private Object o2;

public MySuspend1(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}

@Override
public void run() {
synchronized (o1){
System.out.println("线程 "+Thread.currentThread().getId() +"获得对象锁 o1");
Thread.currentThread().suspend();
synchronized (o2){
System.out.println("线程 "+Thread.currentThread().getId() +"获得对象锁 o2");
}
}
}
}
class MySuspend2 implements Runnable{

private Object o1;
private Object o2;

public MySuspend2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}

@Override
public void run() {
synchronized (o2){
System.out.println("线程 "+Thread.currentThread().getId() +"获得对象锁 o2");

synchronized (o1){
System.out.println("线程 "+Thread.currentThread().getId() +"获得对象锁 o1");
}
}
}
}

上述例子中,对象o1o2,当线程t1获取到o1对象锁后,进入suspend,而此时线程t2来临,获取到了o2对象锁,并再尝试获取o1对象锁,这样t1持有o1并尝试获取o2t2持有o2并尝试获取t1,从而导致程序死锁。

总结

本篇文章讲述了线程的一些常使用的方法,及它们使用中要注意到的一些点。

参考资料

  1. Hotspot 源码
  2. Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?
  3. sleep与wait的区别



-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

您的支持就是我创作的动力!

欢迎关注我的其它发布渠道