Java基础知识(15)- Java 多线程编程(四) | 线程控制、线程死锁
1. 线程控制
1) 启动线程
通过调用 Thread 类的 start 方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。
得到 CPU 时间片后,线程就开始自动执行 run 方法,run 方法被称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,此线程随即终止。
run 方法当作普通方法的方式调用。程序还是要顺序执行,要等待 run 方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。
2) 暂停、恢复和停止线程
暂停(suspend)、恢复(resume)和停止(stop),这些 API 是已过期的,不建议使用。
不建议使用的原因:
(1) 以 suspend 方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题;
(2) stop 方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下;
(3) 因为 suspend、resume 和 stop 方法带来的副作用,这些方法才被标注为不建议使用的过期方法;
3) 中断线程
(1) 使用退出标志
当 run 方法执行完后,线程就会退出。但有时 run 方法是永远不会结束的,如在服务端程序中使用线程进行监听客户端请求,或是其他的需要循环处理的任务。
在这种情况下,一般是将这些任务放在一个循环中,如 while 循环。如果想使 while 循环在某一特定条件下退出,最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false来控制 while 循环是否退出。
(2) 使用 interrupt 方法
interrupt 方法中断线程:设置线程的中断状态位为 true,线程不时地检测中断标示位,判断线程是否应该被中断 (中断标示值是否为 true)。
interrupt 方法只是改变中断状态,不会中断一个正在运行的线程,需要用户自己去监视线程的状态为并做处理。
这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程检查到中断标识,就得以退出阻塞的状态。
更确切的说,如果线程被Object.wait, Thread.join 和 Thread.sleep 三种方法之一阻塞,此时调用该线程的 interrupt() 方法,那么该线程将抛出一个 InterruptedException 中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到 wait()、 sleep() 或 join() 时,才会抛出 InterruptedException。
a) 使用 interrupt() + InterruptedException 来中断线程:
线程处于阻塞状态,如Thread.sleep、wait、IO阻塞等情况时,调用interrupt方法后,sleep等方法将会抛出一个InterruptedException。
b) 使用 interrupt() + isInterrupted() 来中断线程:
this.interrupted(): 测试当前线程是否已经中断(静态方法)。如果连续调用该方法,则第二次调用将返回false。在api文档中说明 interrupted()方法具有清除状态的功能。执行后具有将状态标识清除为false的功能。
this.isInterrupted(): 测试线程是否已经中断,但是不能清除状态标识。
4) join 线程
join 方法是 Thread类 提供的让一个线程等待另一个线程完成的方法。格式:
thread.start();
thread.join();
5) 后台线程
后台线程(Daemon Thread)是在后台运行的,它的任务是为其他的线程提供服务,也被称为守护线程或精灵线程。后台线程的特征:如果所有的前台线程都死亡,后台线程会自动死亡。
调用 Thread 对象的 setDaemon(true) 方法可以将一个指定的线程设置为后台线程。格式:
thread.setDaemon(true);
thread.start();
6) 线程睡眠
线程调用 sleep() 方法进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行。
Thread 类为睡眠线程提供了两种方法:
public static void sleep(long miliseconds)
public static void sleep(long miliseconds,int nanos)
参数声明:
miliseconds - 以毫秒为单位的睡眠时间。
nanos - 这是 0-999999 额外纳秒的睡眠时间。
7) 线程让步
线程让步 yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
但是,实际中无法保证 yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
8) sleep() 和 yield()的区别:
(1) sleep() 方法让线程进入阻塞状态,其他所有处于就绪状态的线程都有机会执行,yield() 方法会让线程重新回到就绪状态,但是只有优先级等于或者大于他的线程才会被执行;
(2) 使用 sleep() 方法需要捕获异常,yield() 不需要;
(3) sleep() 方法比 yield() 方法有更好的移植性,不建议使用yield();
实例:
1 public class App { 2 public static volatile boolean stopFlag = false; 3 4 public static void main( String[] args ) { 5 6 // 使用退出标志中断线程 7 testFlagExit(); 8 System.out.println("----------------------------------------"); 9 10 // 或使用 interrupt() + InterruptedException 中断线程 11 testInterruptExit(); 12 System.out.println("----------------------------------------"); 13 14 // 使用 interrupt() + isInterrupted() 中断线程 15 testIsInterruptedExit(); 16 System.out.println("----------------------------------------"); 17 18 // Thread join 19 ThreadJoinRunnable threadJoin1 = new ThreadJoinRunnable("Thread Join 1"); 20 ThreadJoinRunnable threadJoin2 = new ThreadJoinRunnable("Thread Join 2"); 21 ThreadJoinRunnable threadJoin3 = new ThreadJoinRunnable("Thread Join 3"); 22 try { 23 threadJoin1.start(); 24 threadJoin1.join(); 25 threadJoin2.start(); 26 threadJoin2.join(); 27 threadJoin3.start(); 28 threadJoin3.join(); 29 } catch (InterruptedException e) { 30 e.printStackTrace(); 31 } 32 33 } 34 35 // 使用退出标志中断线程 36 public static void testFlagExit() { 37 38 Thread t = new Thread(new Runnable() { 39 public void run() { 40 String name = Thread.currentThread().getName(); 41 int i = 1; 42 try { 43 44 for (; i <= 10; i++) { 45 if (stopFlag) { 46 break; 47 } 48 System.out.println(name + ": (" + i + ") stopFlag = " + stopFlag); 49 Thread.sleep(1000); 50 } 51 } catch (InterruptedException e) { 52 System.out.println(name + ": interrupted"); 53 } 54 System.out.println(name + ": (" + i + ") stopFlag = " + stopFlag + ", exit"); 55 } 56 }, "Thread Flag" ); 57 t.start(); 58 59 try { 60 Thread.sleep(5000); 61 62 System.out.println("main: stopping " + t.getName() + ", set stopFlag = true"); 63 stopFlag = true; 64 65 Thread.sleep(1000); 66 67 } catch (InterruptedException e) { 68 e.printStackTrace(); 69 } 70 71 } 72 73 // 或使用 interrupt() + InterruptedException 中断线程 74 public static void testInterruptExit() { 75 Thread t = new Thread(new Runnable() { 76 public void run() { 77 String name = Thread.currentThread().getName(); 78 try { 79 80 for (int i = 1; i <= 10; i++) { 81 System.out.println(name + ": (" + i + ")"); 82 Thread.sleep(1000); 83 } 84 } catch (InterruptedException e) { 85 System.out.println(name + ": interrupted"); 86 } 87 System.out.println(name +": exit"); 88 } 89 }, "Thread Interrupt" ); 90 t.start(); 91 92 try { 93 Thread.sleep(5000); 94 95 System.out.println("main: stopping " + t.getName() + ", call interrupt()"); 96 t.interrupt(); 97 98 Thread.sleep(1000); 99 } catch (InterruptedException e) { 100 e.printStackTrace(); 101 } 102 } 103 104 // 使用 interrupt() + isInterrupted() 中断线程 105 public static void testIsInterruptedExit() { 106 Thread t = new Thread(new Runnable() { 107 public void run() { 108 String name = Thread.currentThread().getName(); 109 Boolean b = Thread.currentThread().isInterrupted(); 110 int i = 1; 111 112 while(!b) { 113 System.out.println(name + ": isInterrupted(" + i + ") = " + b); 114 b = Thread.currentThread().isInterrupted(); 115 i++; 116 } 117 118 System.out.println(name +": isInterrupted(" + i + ") = " + b + ", exit"); 119 } 120 }, "Thread IsInterrupted" ); 121 t.start(); 122 123 try { 124 Thread.sleep(1); 125 126 System.out.println("main: stopping " + t.getName() + ", call interrupt()"); 127 t.interrupt(); 128 129 Thread.sleep(1000); 130 } catch (InterruptedException e) { 131 e.printStackTrace(); 132 } 133 } 134 } 135 136 class ThreadJoinRunnable extends Thread implements Runnable { 137 138 ThreadJoinRunnable(String name) { 139 this.setName(name); 140 } 141 142 public void run() { 143 try { 144 System.out.println(getName()); 145 Thread.sleep(1000); 146 } catch (InterruptedException e) { 147 System.out.println(getName() + ": interrupted"); 148 } 149 System.out.println(getName() + ": exit"); 150 } 151 }
输出:
Thread Flag: (1) stopFlag = false
Thread Flag: (2) stopFlag = false
Thread Flag: (3) stopFlag = false
Thread Flag: (4) stopFlag = false
Thread Flag: (5) stopFlag = false
main: stopping Thread Flag, set stopFlag = true
Thread Flag: (6) stopFlag = true, exit
----------------------------------------
Thread Interrupt: (1)
Thread Interrupt: (2)
Thread Interrupt: (3)
Thread Interrupt: (4)
Thread Interrupt: (5)
main: stopping Thread Interrupt, call interrupt()
Thread Interrupt: interrupted
Thread Interrupt: exit
----------------------------------------
Thread IsInterrupted: isInterrupted(1) = false
Thread IsInterrupted: isInterrupted(2) = false
Thread IsInterrupted: isInterrupted(3) = false
...
Thread IsInterrupted: isInterrupted(19) = false
Thread IsInterrupted: isInterrupted(20) = false
main: stopping Thread IsInterrupted, call interrupt()
Thread IsInterrupted: isInterrupted(21) = false
Thread IsInterrupted: isInterrupted(22) = true, exit
----------------------------------------
Thread Join 1
Thread Join 1: exit
Thread Join 2
Thread Join 2: exit
Thread Join 3
Thread Join 3: exit
2. 线程死锁
线程死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
下面我们通过一些实例来说明死锁现象。
生活中的一个实例:两个人面对面过独木桥,甲和乙都已经在桥上走了一段距离,即占用了桥的资源,甲如果想通过独木桥的话,乙必须退出桥面让出桥的资源,让甲通过,但是乙不服,为什么让我先退出去,我还想先过去呢,于是就僵持不下,导致谁也过不了桥,这就是死锁。
1) 死锁产生的原因
(1) 系统资源的竞争
通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
(2) 进程推进顺序非法
进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都会因为所需资源被占用而阻塞。
2)死锁发生时的条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。独木桥每次只能通过一个人。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。乙不退出桥面,甲也不退出桥面。
(3) 不剥夺条件: 进程已获得的资源,在未使用完之前,不能强行剥夺。甲不能强制乙退出桥面,乙也不能强制甲退出桥面。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。如果乙不退出桥面,甲不能通过,甲不退出桥面,乙不能通过。
3) 如何避免死锁
在有些情况下死锁是可以避免的。下面介绍三种用于避免死锁的技术:
(1) 加锁顺序(线程按照一定的顺序加锁)
(2) 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
(3) 死锁检测
Java 中死锁最简单的情况是,一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞,导致死锁。
这是最容易理解也是最简单的死锁的形式。但是实际环境中的死锁往往比这个复杂的多。可能会有多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1,从而导致了死锁。
实例:
1 public class App { 2 public static void main( String[] args ) { 3 4 // 死锁的情况1 5 Thread t1 = new Thread(new DeadLock(true), "Thread DeadLock 1"); 6 Thread t2 = new Thread(new DeadLock(false), "Thread DealLock 2"); 7 8 // 死锁的情况2 9 //Thread t1 = new Thread(new DeadLock(false), "Thread DeadLock 1"); 10 //Thread t2 = new Thread(new DeadLock(true), "Thread DealLock 2"); 11 12 // 不死锁的情况1 13 //Thread t1 = new Thread(new DeadLock(true), "Thread DeadLock 1"); 14 //Thread t2 = new Thread(new DeadLock(true), "Thread DealLock 2"); 15 16 // 不死锁的情况2 17 //Thread t1 = new Thread(new DeadLock(false), "Thread DeadLock 1"); 18 //Thread t2 = new Thread(new DeadLock(false), "Thread DealLock 2"); 19 20 t1.start(); 21 t2.start(); 22 23 } 24 } 25 26 class DeadLock implements Runnable{ 27 28 private static Object obj1 = new Object(); 29 private static Object obj2 = new Object(); 30 private boolean flag; 31 32 public DeadLock(boolean flag){ 33 this.flag = flag; 34 } 35 36 @Override 37 public void run(){ 38 String name = Thread.currentThread().getName(); 39 System.out.println(name + ":running ..."); 40 41 /* 42 * 同时开启线程1和线程2: 43 * 1. 如果让线程1执行 "代码1"(flag==true),让线程2执行 "代码2" (flag==false), 会死锁; 44 * 2. 如果让线程1执行 "代码2"(flag==false),让线程2执行 "代码1" (flag==true), 会死锁; 45 * 3. 如果让线程1执行 "代码1"(flag==true),让线程2执行 "代码1" (flag==true), 不会死锁; 46 * 4. 如果让线程1执行 "代码2"(flag==false),让线程2执行 "代码2" (flag==false), 不会死锁; 47 */ 48 if (flag) { 49 // 代码1 50 synchronized(obj1){ 51 System.out.println(name + ": lock obj1"); 52 System.out.println(name + ": try to lock obj2 ..."); 53 try { 54 Thread.sleep(1000); 55 } catch (InterruptedException e) { 56 e.printStackTrace(); 57 } 58 synchronized(obj2){ 59 // 死锁时执行不到这里 60 System.out.println(name +": after 1 second, lock obj2"); 61 } 62 } 63 } else { 64 // 代码2 65 synchronized(obj2){ 66 System.out.println(name + ": lock obj2"); 67 System.out.println(name + ": try to lock obj1 ..."); 68 try { 69 Thread.sleep(1000); 70 } catch (InterruptedException e) { 71 e.printStackTrace(); 72 } 73 synchronized(obj1){ 74 // 死锁时执行不到这里 75 System.out.println(name +": after 1 second, lock obj1"); 76 } 77 } 78 } 79 } 80 }
输出:
Thread DeadLock 1:running ...
Thread DeadLock 1: lock obj1
Thread DeadLock 1: try to lock obj2 ...
Thread DealLock 2:running ...
Thread DealLock 2: lock obj2
Thread DealLock 2: try to lock obj1 ...
// 此时程序死锁
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)