多线程进阶
一、线程之间的通信
为了更有效地协调不同线程的工作,需要在线程间建立沟通渠道,通过线程间的“对话”来解决线程间的同步问题。java.lang.Object
类的一些方法为线程间的通讯提供了有效手段。
-
wait()
wait()方法: 如果当前状态不适合本线程执行,正在执行同步代码(synchronized)的某个线程A调用该方法(在对象x上),该线程暂停执行而进入对象x的等待池,并释放已获得的对象x的锁旗标。线程A要一直等到其他线程在对象x上调用notify或notifyAll方法,才能够再重新获得对象x的锁旗标后继续执行(从wait语句后继续执行)。
-
notify()
随机唤醒一个等待的线程,本线程继续执行
- 线程被唤醒以后,还要等发出唤醒消息者释放监视器,这期间关键数据仍可能被改变
- 被唤醒的线程开始执行时,一定要判断当前状态是否适合自己运行
-
notifyAll()
notifyAll() 唤醒所有等待的线程,本线程继续执行。
- wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
- wait()使当前线程阻塞,前提是必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized同步代码块里使用 wait()、notify/notifyAll() 方法。
- 由于 wait()、notify/notifyAll() 在synchronized代码块执行,说明当前线程一定是获取了锁的。当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程
二、死锁
(1)死锁相关概念
-
阻塞:暂停一个线程的执行以等待某个条件发生。
-
临界资源:各进程采取互斥的方式,实现共享的资源称作临界资源。属于临界资源的硬件有打印机、磁带机等,软件有消息缓冲队列、变量、数组、缓冲区等。
-
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
-
死锁必要条件
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
(2)死锁的例子
设想一个游戏,规则为3个人站在三角形的三个顶点的位置上,三个边上放着三个球,如图所示。每个人都必须先拿到自己左手边的球,才能再拿到右手边的球,两手都有球之后,才能够把两个球都放下.
创建3个线程模拟3个游戏者的行为:
public class Game{
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号游戏者
}
}
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);}catch(Exception e){}; //放下后休息1ms
}
}
}
}
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.flag2==true){};
if(ball.flag0==true && ball.flag2==false)
{ ball.flag2=true;
System.out.println("Player1 has got two balls!");
ball.flag0=false;
ball.flag2=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("Player2 has got two balls!");
ball.flag1=false;
ball.flag2=false;
try{ sleep(1);}catch(Exception e){};
}
}
}
}
运行结果
若干次后将陷入死锁,不再有输出信息,即任何人都不能再同时拥有两侧的球
程序说明
如果刚好3个人都拿到了左手边的球,都等待那右手边的球,则因为谁都不能放手,则这3个线程都将陷入无止尽的等待当中,这就构成了死锁
三、后台(守护)线程
守护线程:指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。
- 后台(守护)线程会随着主程序的结束而结束,但是用户线程则不会;或者说,只要有一个用户线程未退出,进程就不会终止;
- 后台(守护)线程在不执行finally子句的情况下就会终止其run方法。后台(守护)线程创建的子线程也是后台线程。
- 默认情况下,程序员创建的线程是用户线程;用setDaemon(true)可以设置线程为后台(守护)线程;而用isDaemon( )则可以判断一个线程是前台线程还是后台(守护)线程;
- jvm的垃圾回收器其实就是一个后台(守护)线程;
- setDaemon函数必须在start函数之前设定,否则会抛出IllegalThreadStateException异常;
四、线程的生命周期
- 诞生状态
线程刚刚被创建 - 就绪状态
线程的 start 方法已被执行
线程已准备好运行 - 运行状态
处理机分配给了线程,线程正在运行 - 阻塞状态(Blocked)
在线程发出输入/输出请求且必须等待其返回
遇到用synchronized标记的方法而未获得其监视器暂时不能进入执行时 - 休眠状态(Sleeping)
执行sleep方法而进入休眠 - 死亡状态
线程已完成或退出
五、线程的优先级
Java虚拟机支持一种非常简单的、确定的调度算法,叫做固定优先级算法。这个算法基于线程的优先级对其进行调度
(1)优先级
- 每个Java线程都有一个优先级,其范围都在1和10之间。默认情况下,每个线程的优先级都设置为5
- 在线程A运行过程中创建的新的线程对象B,初始状态具有和线程A相同的优先级
- 如果A是个后台线程,则B也是个后台线程
- 可在线程创建之后的任何时候,通过setPriority(int priority)方法改变其原来的优先级
(2)基于线程优先级的线程调度
- 具有较高优先级的线程比优先级较低的线程优先执行
- 对具有相同优先级的线程,Java的处理是随机的
- 底层操作系统支持的优先级可能要少于10个,这样会造成一些混乱。因此,只能将优先级作为一种很粗略的工具使用。最后的控制可以通过明智地使用yield()函数来完成
- 我们只能基于效率的考虑来使用线程优先级,而不能依靠线程优先级来保证算法的正确性