线程的生命周期
1、线程的生命周期
线程从产生到消亡的过程
一个线程在任何时刻都处于某种线程状态(thread state)
线程生命周期状态图
诞生状态
线程刚刚被创建
就绪状态
线程的 start 方法已被执行
线程已准备好运行
运行状态
处理机分配给了线程,线程正在运行
阻塞状态(Blocked)
在线程发出输入/输出请求且必须等待其返回
遇到用synchronized标记的方法而未获得其监视器暂时不能进入执行时
休眠状态(Sleeping)
执行sleep方法而进入休眠
死亡状态
线程已完成或退出
2、死锁问题
死锁
线程在运行过程中,其中某个步骤往往需要满足一些条件才能继续进行下去,如果这个条件不能满足,线程将在这个步骤上出现阻塞
线程A可能会陷于对线程B的等待,而线程B同样陷于对线程C的等待,依次类推,整个等待链最后又可能回到线程A。如此一来便陷入一个彼此等待的轮回中,任何线程都动弹不得,此即所谓死锁(deadlock)
对于死锁问题,关键不在于出现问题后调试,而是在于预防
设想一个游戏,规则为3个人站在三角形的三个顶点的位置上,三个边上放着三个球,如图所示。每个人都必须先拿到自己左手边的球,才能再拿到右手边的球,两手都有球之后,才能够把两个球都放下
创建3个线程模拟3个游戏者的行为。
class Balls { boolean flag0 = false; //0号球的标志变量,true表示已被人拿,false表示未被任何人拿 boolean flag1 = false; //1号球的标志变量 boolean flag2 = false; //2号球的标志变量 } class Player0 extends Thread { //0号游戏者的类 private Balls ball; public Player0(Balls b) { this.ball = b; } public void run() { while(true) { while(ball.flag1 == true){} //如果1号球已被拿走,则等待 ball.flag1 = true; //拿起1号球 while(ball.flag0 == true){} //如果0号球已被拿走,则等待 if(ball.flag1 == true && ball.flag0 == false) { ball.flag0 = true; //拿起0号球 System.out.println("Player0 has got two balls!"); ball.flag1 = false; //放下1号球 ball.flag0 = false; //放下0号球 try { sleep(1); //放下后休息1ms } catch (Exception e) { } } } } } class Player1 extends Thread { //1号游戏者的类 private Balls ball; public Player1(Balls b) { this.ball = b; } public void run() { while(true) { while(ball.flag0 == true){} ball.flag0 = true; while(ball.flag1 == true){} if(ball.flag0 == true && ball.flag1 == false) { ball.flag1 = true; System.out.println("Player0 has got two balls!"); ball.flag0 = false; ball.flag1 = false; try { sleep(1); } catch (Exception e) { } } } } } class Player2 extends Thread { //2号游戏者的类 private Balls ball; public Player2(Balls b) { this.ball = b; } public void run() { while(true) { while(ball.flag2 == true){} ball.flag2 = true; while(ball.flag1 == true){} if(ball.flag2 == true && ball.flag1 == false) { ball.flag1 = true; System.out.println("Player0 has got two balls!"); ball.flag2 = false; ball.flag1 = false; try { sleep(1); } catch (Exception e) { } } } } } public class playball { public static void main(String[] args) { Balls ball = new Balls(); //创建一个球类对象 Player0 p0 = new Player0(ball); //创建0号游戏者 Player1 p1 = new Player1(ball); //创建1号游戏者 Player2 p2 = new Player2(ball); //创建2号游戏者 p0.start(); //启动0号游戏者 p1.start(); //启动1号游戏者 p2.start(); //启动2号游戏者 } }
运行结果:
若干次后将陷入死锁,不再有输出信息,即任何人都不能再同时拥有两侧的球
程序说明:
如果刚好3个人都拿到了左手边的球,都等待那右手边的球,则因为谁都不能放手,则这3个线程都将陷入无止尽的等待当中,这就构成了死锁
为了便于观察死锁发生的条件,我们在每个游戏者放下两边的球后增加了sleep语句
为了避免死锁,需要修改游戏规则,使每个人都只能先抢到两侧中号比较小的球,才能拿另一只球,这样就不会再出现死锁现象
3、控制线程的生命
结束线程的生命
用stop方法可以结束线程的生命
但如果一个线程正在操作共享数据段,操作过程没有完成就用stop结束的话,将会导致数据的不完整,因此并不提倡使用此方法
通常,可通过控制run方法中循环条件的方式来结束一个线程
线程不断显示递增整数,按下回车键则停止执行:
class TestThread1 extends Thread { private boolean flag = true; public void stopme() { //在此方法中控制循环条件 flag = false; } public void run() { int i = 0; while(flag) { System.out.println(i++); //如果flag为真则一直显示递增整数 } } } public class ext8_12 { public static void main(String[] args) throws IOException{ TestThread1 t = new TestThread1(); t.start(); new BufferedReader(new InputStreamReader(System.in)).readLine(); //等待键盘输入 t.stopme(); //调用stopme方法结束t线程 } }
运行效果为按下回车键后则停止显示
线程的优先级
线程调度
在单CPU的系统中,多个线程需要共享CPU,在任何时间点上实际只能有一个线程在运行
控制多个线程在同一个CPU上以某种顺序运行称为线程调度
Java虚拟机支持一种非常简单的、确定的调度算法,叫做固定优先级算法。这个算法基于线程的优先级对其进行调度
线程的优先级
每个Java线程都有一个优先级,其范围都在1和10之间。默认情况下,每个线程的优先级都设置为5
在线程A运行过程中创建的新的线程对象B,初始状态具有和线程A相同的优先级
如果A是个后台线程,则B也是个后台线程
可在线程创建之后的任何时候,通过setPriority(int priority)方法改变其原来的优先级
基于线程优先级的线程调度
具有较高优先级的线程比优先级较低的线程优先执行
对具有相同优先级的线程,Java的处理是随机的
底层操作系统支持的优先级可能要少于10个,这样会造成一些混乱。因此,只能将优先级作为一种很粗略的工具使用。最后的控制可以通过明智地使用yield()函数来完成
我们只能基于效率的考虑来使用线程优先级,而不能依靠线程优先级来保证算法的正确性
假设某线程正在运行,则只有出现以下情况之一,才会使其暂停运行
一个具有更高优先级的线程变为就绪状态(Ready);
由于输入/输出(或其他一些原因)、调用sleep、wait、yield方法使其发生阻塞;
对于支持时间分片的系统,时间片的时间期满
class TestThread2 extends Thread { private int tick = 1; private int num; public TestThread2(int i) { this.num = i; } public void run() { while(tick < 400000) { tick++; if((tick % 50000) == 0) { //每隔50000进行显示 System.out.println("Thread#" + num +",tick=" + tick); yield(); //放弃执行权 } } } } public class ext8_13 { public static void main(String[] args) { TestThread2[] runners = new TestThread2[2]; for(int i = 0; i < 2; i++) runners[i] = new TestThread2(i); runners[0].setPriority(2); runners[1].setPriority(3); for(int i = 0; i < 2; i++) runners[i].start(); } }
运行结果如下:
Thread #1, tick = 50000
Thread #1, tick = 100000
Thread #1, tick = 150000
Thread #1, tick = 200000
Thread #1, tick = 250000
Thread #1, tick = 300000
Thread #1, tick = 350000
Thread #1, tick = 400000
Thread #0, tick = 50000
Thread #0, tick = 100000
Thread #0, tick = 150000
Thread #0, tick = 200000
Thread #0, tick = 250000
Thread #0, tick = 300000
Thread #0, tick = 350000
Thread #0, tick = 400000
结果说明:
具有较高优先级的线程1一直运行到结束,具有较低优先级的线程0才开始运行
虽然具有较高优先级的线程1调用了yield方法放弃CPU资源,允许线程0进行争夺,但马上又被线程1抢夺了回去,所以有没有yield方法都没什么区别
如果在yield方法后增加一行sleep语句,让线程1暂时放弃一下在CPU上的运行,哪怕是1毫秒,则线程0也可以有机会被调度。修改后的run方法如下:
public void run() { while(tick < 400000) { tick++; if((tick % 50000) == 0) { //每隔50000进行显示 System.out.println("Thread#" + num +",tick=" + tick); yield(); //放弃执行权 try { sleep(1); } catch(Exception e) { } } } }
运行结果如下:
Thread #1, tick = 50000
Thread #1, tick = 100000
Thread #1, tick = 150000
Thread #1, tick = 200000
Thread #0, tick = 50000
Thread #1, tick = 250000
Thread #1, tick = 300000
Thread #0, tick = 100000
Thread #1, tick = 350000
Thread #1, tick = 400000
Thread #0, tick = 150000
Thread #0, tick = 200000
Thread #0, tick = 250000
Thread #0, tick = 300000
Thread #0, tick = 350000
Thread #0, tick = 400000
说明
具有较低优先权的线程0在线程1没有执行完毕前也获得了一部分执行,但线程1还是优先完成了执行
Java虚拟机本身并不支持某个线程抢夺另一个正在执行的具有同等优先级线程的执行权
通常,我们在一个线程内部插入yield()语句,这个方法会使正在运行的线程暂时放弃执行,这是具有同样优先级的线程就有机会获得调度开始运行,但较低优先级的线程仍将被忽略不参加调度