【Java程序设计】多线程进阶

本文深入探讨Java多线程的高级话题,包括线程间的通信机制如wait、notify和notifyAll,死锁的概念、条件及示例,以及后台线程的特性。此外,还介绍了线程的生命周期和优先级管理,帮助开发者理解并避免线程同步中的常见问题。
摘要由CSDN通过智能技术生成

多线程进阶

在这里插入图片描述

一、线程之间的通信

为了更有效地协调不同线程的工作,需要在线程间建立沟通渠道,通过线程间的“对话”来解决线程间的同步问题。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()函数来完成
  • 我们只能基于效率的考虑来使用线程优先级,而不能依靠线程优先级来保证算法的正确性
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BkbK-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值