前言 今天我们来看下在多线程环境处理中可能会遇到的一些方法,来了解下它们。
正文 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(); } } } }
输出结果:
上述例子我们可以看到,在线程休眠时,线程会一直保持对象锁,其它线程无法拿到该对象锁。
sleep
是JVM
基于操作系统底层的实现而封装实现的。
其原理大家可以了解一下:
挂起进程(或线程)并修改其运行状态,即让出CPU
控制权限;
用sleep()
提供的参数来设置一个定时器;
当时间结束,定时器会触发,内核收到中断后修改进程(或线程)的运行状态。例如线程会被标志为就绪而进入就绪队列等待调度。
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){ System.out.println(Thread.currentThread().getId() +"获得对象锁" ); } } }
可以看到如下输出:
说明线程11
获得对象锁,后续执行wait
方法后,线程11
释放了对象锁,而后被线程12
获取。
并且我们看到程序无法停止,MyWait1
也不会输出开始休眠
或者结束休眠
等日志,因为线程11
已经被挂起。
此时需要调用notify
或notiftAll
方法来唤醒线程。
我们可以在线程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){ System.out.println(Thread.currentThread().getId() +"获得对象锁" ); o.notify(); } } }
输出结果如下:
可以看到线程11
被唤醒,执行后续逻辑。
我们来总结下sleep
和wait
方法的不同之处:
属于不同的两个类,sleep
是Thread
类里的static native
方法,wait
是Object
类里的方法; sleep
方法不会释放对象锁,wait
方法会释放对象锁;sleep
方法可以在任何地方使用,wait方法则只能在同步方法或同步块中使用 ;sleep
需要指定休眠时间,wait
可以指定时间,也可以不指定,如果不指定时间,需要调用notify
或notiftAll
方法来唤醒线程;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()); t1.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
方法后应该显式调用notify
或notiftAll
方法来唤醒线程,那么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 ; } target = null ; 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()); t1.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.wait
、Thread.join
、Thread.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 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.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 { 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 ; bool throw_illegal_thread_state = false ; { MutexLocker mu (Threads_lock) ; if (java_lang_Thread::thread (JNIHandles::resolve_non_null (jthread)) != NULL ) { throw_illegal_thread_state = true ; } else { jlong size = java_lang_Thread::stackSize (JNIHandles::resolve_non_null (jthread)); size_t sz = size > 0 ? (size_t ) size : 0 ; native_thread = new JavaThread (&thread_entry, sz); if (native_thread->osthread () != NULL ) { 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 ) { 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
异常并修复对象的原因:
一个线程几乎可以在任何地方抛出一个ThreadDeath
异常。考虑到这一点,所有同步的方法和块都必须详细研究处理。 一个线程可以抛出第二个ThreadDeath
异常,同时从第一个线程清除(在 catch
或 finally
子句中)。清理将不得不重复进行,直到它成功。确保这一点的代码将非常复杂。 所以捕获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." ); 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(); 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
,线程类中的suspend
、resume
、destroy
等方法也已经过时弃用了。
它们的弃用原因大家可以看下这篇官方文档,这儿我就不过多叙述了。
Why Are Thread.stop
, Thread.suspend
, Thread.resume
and Runtime.runFinalizersOnExit
Deprecated?
其原因主要是不安全、容易死锁等。
官方给出的策略是:
对于stop
方法,可以使用interrupt
或者sleep
等替代; 对于suspend
和resume
方法,可以使用 Object.wait
和Object.notify
来实现; 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" ); } } } }
上述例子中,对象o1
、o2
,当线程t1
获取到o1
对象锁后,进入suspend
,而此时线程t2
来临,获取到了o2
对象锁,并再尝试获取o1
对象锁,这样t1
持有o1
并尝试获取o2
,t2
持有o2
并尝试获取t1
,从而导致程序死锁。
总结 本篇文章讲述了线程的一些常使用的方法,及它们使用中要注意到的一些点。
参考资料 Hotspot 源码 Why Are Thread.stop
, Thread.suspend
, Thread.resume
and Runtime.runFinalizersOnExit
Deprecated? sleep与wait的区别