多线程之间的调度同步以及通信


在这里插入图片描述

认识进程和线程

(1)计算机的操作系统大多采用多任务和分时设计,多任务是指在一个操作系统中可以同时运行多个程序。
(2)例如在使用QQ聊天的同时听音乐,即有多个独立运行的任务,每个任务对应一个进程,每个进程又可以产生多个线程

进程

  1. 程序(Program)是对数据描述与操作的代码的集合,如Office中的Word、暴风影音等应用程序。
  2. 进程(Process)是程序的一次动态执行过程,它对应了从代码加载、执行至执行完毕的一个完成过程,这个过程也是进程本身从产生、发展至消亡的过程。
  3. 操作系统同时管理一个计算机系统中的多个进程,让计算机系统中的多个进程轮流使用CPU资源,或者共享操作系统的其它资源。
  4. 进程有如下特点:
    –>进程是系统运行程序的基本单位。
    –>每一个进程都有自己独立的一块内存空间、一组系统资源。
    –>每一个进程的内部数据和状态都是完全独立的。

线程

  1. 线程是进程中执行运算的最小单位,一个进程在其执行过程中可以产生多个线程,而线程必须在某个进程内执行。
  2. 线程是进程内部的一个执行单元,是可完成一个独立任务的顺序控制流程,如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为多线程。
  3. 线程按处理级别可以分为核心级线程和用户级线程。
  • 核心级线程
    –>核心级线程是和系统任务相关的线程,它负责处理不同进程之间的多个线程。
    –>允许不同进程中的线程按照同一相对优先调度方法对线程进行调度,使它们有条不紊地工作,可以发挥多处理器的并发优势,以充分利用计算机的软/硬件资源。
  • 用户级线程
    –>在开发程序时,由于程序的需要而编写的线程即用户级线程,这些线程的创建、执行和消亡都是编写在应用程序时进行控制的。
    –>对于用户级线程的切换,通常发生在一个应用程序的诸多线程之间,如迅雷中的多线程下载就属于用户线程。
    –>多线程可以改善用户体验。具有多个线程的进程能更好地表达和解决现实世界的具体问题,多线程是计算机应用开发和程序设计的一项重要的实用技术。

线程和进程既有联系又有区别

  1. 一个进程中至少要有一个线程。
  2. 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
  3. 处理机分配给线程,即真正在处理机上运行的是线程。

多线程的优势

  1. 多线程程序可以带来更好的用户体验,避免因程序执行过慢而导致计算机出现计算机死机或者白屏的情况。
  2. 多线程程序可以最大限度地提高计算机系统的利用效率。如迅雷的多线程下载。

编写线程类

(1)每个程序至少自动拥有一个线程,称为主线程。

(2)当程序加载到内存时启动主线程

(3)Java程序中的public static void main()方法是主线程的入口,运行Java程序时,会先执行这个方法。

(4)开发中,用户编写的线程一般都是指除了主线程之外的其他线程。

(5)使用一个线程的过程可以分为以下4个步骤:
第一步:定义一个线程,同时指明这个线程所要执行的代码,即期望完成的功能。
第二步:创建线程对象。
第三步:启动线程
第四部:终止线程。

(6)定义一个线程类通常有两种方法,分别是继承java.lang.Thread类实现java.lang.Runnable接口

  1. 使用Thread类创建线程
    –》Java提供了java.lang.Thread类支持多线程编程,该类提供了大量的方法来控制和操作线程,常用方法如下:
    在这里插入图片描述
    –》创建线程时继承Thread类并重写Thread类中的run()方法。
    –》Thread类的run()方法是线程要执行操作任务的方法,所以线程要执行的操作代码都需要写在run()方法中,并通过调用start()方法来启动线程。
  2. 使用Runnable接口创建线程
    –》使用继承Thread类的方式创建线程简单明了,符合大家的习惯,但它有一个缺点,如果定义的类已经继承了其他类则无法再继承Thread类。使用Runnable接口创建线程的方式可以解决上述问题。
    –》Runnable接口中声明了一个run()方法,即public void run()。
    –》一个类可以通过实现Runnable接口并实现run()方法完成线程的所有活动,已实现的run()方法称为该对象的线程体。
    –》任何一个实现Runnable接口的对象都可以作为一个线程的目标对象。

(7)两种创建线程的方式有各自的特点和应用领域:
–》直接继承Thread类的方式编写简单,可以直接操作线程,适用于单重继承的情况;
–》实现Runnable接口的方式,当一个线程继承了另一个类时,就只能用实现Runnable接口的方法来创建线程,而且这种方式还可以使多个线程之间使用同一个Runnable对象。

线程状态和线程调度

线程的状态

(1)线程的生命周期可以分为4个阶段,即线程的4种状态,分别为新生状态、可运行状态、阻塞状态和死亡状态。
(2)一个具有生命的线程,总是处于上述4种状态之一。

  1. 新生状态(New Thread)
    –>创建线程对象之后,尚未调用其start()方法之前,这个线程就有了生命,此时线程仅仅是一个空对象,系统没有为其分配资源。此时只能启动和终止线程,任何其它操作都会引发异常。
  2. 可运行状态(Runnable)
  • 当调用了start()方法启动线程之后,系统为该线程分配除CPU外的所需资源,这个线程就有了运行的机会,线程处于可运行的状态,在这个状态当中,该线程对象可能正在运行,也可能尚未运行。
  • 对于只有一个CPU的机器而言,任何时刻只能有一个处于可运行状态的线程占用处理机,获得CPU资源,此时系统真正运行线程的run()方法。
  1. 阻塞状态(Blocked)
  • 一个正在运行的线程因某种原因不能继续运行时,进入阻塞状态。
  • 阻塞状态是一种“不可运行”的状态,而处于这种状态的线程在得到一个特定的事件之后会转回可运行状态。
  • 导致一个线程被阻塞有以下原因:
    1.调用了Thread类的静态方法sleep()。
    2.一个线程执行到一个I/O操作时,如果I/O操作尚未完成,则线程将被阻塞。
    3.如果一个线程的执行需要用一个对象的锁,而这个对象的锁正被别的线程占用,那么此线程被阻塞。
    4.线程的suspend()方法被调用而使线程被挂起时,线程进入阻塞状态。但suspend()容易导致死锁,已经被JDK列为过期方法,基本不再使用。
  • 处于阻塞状态的线程可以转回到可运行状态,例如,在调用sleep()方法之后,这个线程的睡眠时间已经达到了指定的间隔,那么它就有可能重新回到可运行状态。或当一个线程等待的锁变得可用的时候,那么这个线程也会从被阻塞的状态转入可运行状态。
  1. 死亡状态(Dead)
    一个线程的run()方法运行完毕、stop()方法被调用或者在运行过程中出现未捕获的异常时,线程进入死亡状态。

线程调度

(1)当同一时刻有多个线程处于可运行状态,它们需要排队等待CPU资源,每个线程会自动获得一个线程的优先级(Priority),优先级的高低反映线程的重要或紧急程度。
(2)可运行的线程按优先级排队,线程调度依据建立在优先级基础上的“先到先服务”原则。
(3)线程调度管理器负责线程排队和在线程间分配CPU,并按线程调度算法进行调度。当线程调度管理器选中某个线程时,该线程获得CPU资源进入运行状态。
(4)线程调度是抢占式调度,即在当前线程执行过程中如果有一个更高优先级的线程进入可运行状态,则这个更高优先级的线程立即被调度执行。

线程优先级

  1. 线程的优先级用1~10表示,10表示优先级最高,默认值是5。
  2. 每个优先级对应一个Thread类的公用静态常量。
	public static final int NORM_PRIORITY=5;
	public static final int MIN_PRIORITY=1;
	public static final int MAX_PRIORITY=10;
  1. 每个线程的优先级都介于Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间
  2. 线程的优先级可以通过setPriority(int
    grade)方法更改,此方法的参数表示要设置的优先级,它必须是一个1-10之间的整数。

实现线程调度的方法

  1. join()方法
    join()方法使当前线程暂停执行,等待调用该方法的线程结束后再继续执行本线程。它有3种重载形式:
public final void join()
public final void join(long mills)
public final void join(long mills,int nanos)
  1. sleep()方法
    sleep()方法会让当前线程睡眠(停止执行)millis毫秒,线程由运行中的状态进入不可运行状态,睡眠时间过后线程会再次进入可运行状态。语法结构如下:
public static void sleep(long millis)
  1. yield()方法
    yield()方法可让当前线程暂停执行,允许其它线程执行,但该线程仍处于可运行状态,并不变为阻塞状态。此时,系统选择其他相同或更高优先级线程执行,若无其它相同或更高优先级线程,则该线程继续执行。
  2. sleep()方法和yield()方法的区别
    在这里插入图片描述

线程同步和线程间的通信

线程同步的必要性

前面说的线程都是独立的,而且异步执行,也就是说每个线程都包含了运行时所需要的数据或方法,而不需要外部资源或方法,也不必关心其它线程的状态或行为。
但是经常有一些同时运行的线程需要共享数据,此时就需要考虑其他线程的状态和行为,否则就不能保证程序运行结果的正确性。

实现线程同步

(1)当两个线程或多个线程需要访问同一资源时,需要以某种顺序来确保该资源在某一时刻只能被一个线程使用的方式称为线程同步。
(2)采用同步来控制线程的执行有两种方式,即同步方法和同步代码块。这两种方式都使用synchronized关键字实现。

1. 同步方法

  • 通过在方法声明中加入synchronized关键字来声明同步方法。
  • 使用synchronized修饰的方法控制对类成员变量的访问。每个类实例对应一把锁,方法一旦执行,就独占该锁,直到该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
  • 这种机制确保了同一时刻对应每一个实例,其所有声明为synchronized的方法只能有一个处于可执行状态,从而有效的避免了类成员变量的访问冲突。
  • 同步方法的语法格式如下:

访问修饰符 synchronized 返回类型 方法名(){} 或者 synchronized 访问修饰符 返回类型 方法名(){}

–》synchronized是同步关键字
–》访问修饰符是指public、private等。

2. 同步代码块

  • 同步代码块的语法格式如下:
	synchronized(){
				//需要同步访问控制的代码
			}
  • synchronized块中的代码必须获得对象syncObject的锁,具体实现机制与同步方法一样。
  • 由于同步代码块可以针对任意代码块,且可任意指定上锁的对象,故灵活性比较高。

3. 死锁
–>多线程在使用同步机制时,存在“死锁”的潜在危险。如果多个线程都处于等待状态而无法唤醒时,就构成了死锁(DeadLock),此时处于等待状态的多个线程占用系统资源,但无法运行,因此不会释放自身的资源。

  • 在编程时应注意死锁问题,避免死锁的有效方法是
    线程因某个条件未满足而受阻,不能让其继续占有资源;
    如果有多个对象需要互斥访问,应确定线程获得锁的顺序,并保证整个程序以相反的顺序释放锁。

线程间通信的必要性

在前面的介绍中,了解了多线程编程中使用同步机制的重要性,并介绍了如何通过同步来正确地访问共享资源。这些线程之间是相互独立的,并不存在任何的依赖关系。
它们各自竞争CPU资源,互不相让,并且还无条件地阻止其他线程对共享资源的异步访问。然而,有很多现实问题要求不仅要同步地访问同一共享资源,而且线程间还被彼此牵制,相互通信。

实现线程间通信

  1. Java提供了如下3个方法实现线程之间的通信:
  • wait()方法:调用wait()方法会挂起当前线程,并释放共享资源的锁。
  • notify()方法:调用任意对象的notify()方法会在因调用该对象的wait()而阻塞的线程中随机选择一个线程解除阻塞,但要等到获得锁后才真正执行。
  • notifyAll()方法:调用了notifyAll()方法会将因调用该对象的wait()方法而阻塞的所有线程一次性全部解除阻塞。
  • wait()、notify()和notifyAll()这3个方法都是Object类中final方法,被所有的类继承且不允许重写。这3个方法只能在同步方法或者同步代码块中使用,否则会抛出异常

练习

模拟多人爬山

需求说明:

  1. 每个线程代表一个人
  2. 可设置每人爬山速度
  3. 每爬完100米显示信息
  4. 爬到终点时给出相应提示

实现思路:

  1. 创建线程类ClimbThread
  2. 属性:爬100米时长(time),爬多少个100米(num)构造方法完成属性初始化
  3. 实现run()方法
  4. 线程休眠模拟爬山中的延时实现测试类Test
  5. 创建多个线程对象模拟多个人,设置人名、爬100米时长

在这里插入图片描述
在这里插入图片描述

ClimbThread类

package cn.bdqn.demo01;

public class ClimbThread extends Thread {

	private int time;
	private int num;

	public int getTime() {
		return time;
	}

	public void setTime(int time) {
		this.time = time;
	}

	public int getNum() {
		return num;
	}

	public void setNum(int num) {
		this.num = num;
	}

	public ClimbThread(int time, int num) {
		super();
		this.time = time;
		this.num = num;
	}

	@Override
	public void run() {
		int i=0;
		while ( i < this.num * 1000) {
			try {
				Thread.sleep(this.time);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			i+=100;
			System.out.println(Thread.currentThread().getName() + "爬完"+i+"米");

			
//			System.out.println(i);
		}
		System.out.println(Thread.currentThread().getName() + "到达终点!");
	}

}

Test类

package cn.bdqn.demo01;

public class Test {

	public static void main(String[] args) {
		ClimbThread ct1 = new ClimbThread(1000, 1);
		ClimbThread ct2 = new ClimbThread(2000, 1);

		ct1.setName("年轻人");
		ct2.setName("老年人");
		
		ct1.start();
		ct2.start();
		
	}

}

线程的优先级

需求说明

  1. 显示主线程、子线程默认优先级
  2. 将主线程设置为最高优先级、子线程设置为最低优先级并显示

在这里插入图片描述
ThreadPriority类

package cn.bdqn.demo02;

public class ThreadPriority {

	public static void main(String[] args) {
		Thread th1 = new Thread();
		
		System.out.println("主线程名:"+Thread.currentThread().getName()+",主线程优先级:"+Thread.currentThread().getPriority());
		System.out.println("子线程名:"+th1.getName()+",子线程优先级:"+th1.getPriority());
		
		Thread.currentThread().setPriority(10);
		th1.setPriority(1);
		
		System.out.println("主线程名:"+Thread.currentThread().getName()+",主线程优先级:"+Thread.currentThread().getPriority());
		System.out.println("子线程名:"+th1.getName()+",子线程优先级:"+th1.getPriority());
	}

}

模拟叫号看病

需求说明

  1. 某科室一天需看普通号50个,特号10个
  2. 特需号看病时间是普通号的2倍
  3. 开始时普通号和特需号并行叫号叫到特需号的概率比普通号高
  4. 当普通号叫完第10号时,要求先看完全部特需号,再看普通号
  5. 使用多线程模拟这一过程
    在这里插入图片描述
    Hospitals类
package cn.bdqn.demo03;

public class Hospitals implements Runnable{

	@Override
	public void run() {
		
		for(int i=1;i<=10;i++){
			
			System.out.println(Thread.currentThread().getName()+i+"\t号病人在看病");
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		}		
		
	}
}

Test类

package cn.bdqn.demo03;

public class Test {

	public static void main(String[] args) {
		Thread.currentThread().setName("普通号");
		Hospitals hp1 = new Hospitals();
		Thread thread1 = new Thread(hp1,"特需号");
		
		thread1.setPriority(10);
		thread1.start();

		for(int i=1;i<=50;i++){
			if(i == 10){
				try {
					thread1.join();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName()+i+"\t号病人在看病");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
		
	}

}

注解与多线程章节总结

  1. java.lang包中提供了3种标准的注解类型,称为内建注解,分别是@Override注解、@Deprecated注解以及@SuppressWarnings注解。
  2. java.lang.annotation包提供了4种元注解,用来修饰其他的注解定义。分别是@Target注解、@Retention注解、@Documented注解以及@Inherited注解。
  3. 线程是进程中执行运算的最小单位。一个进程在其执行过程中可以产生多个线程,而线程必须在某个进程内执行。
  4. 定义一个线程类通常由两种方法,分别是继承java.lang.Thread类和实现java.lang.Runnable接口。
  5. 线程有新生、可运行、阻塞、死亡4种状态。
  6. 线程的优先级用1~10表示,10表示优先级最高,默认值是5。每个优先级对应一个Thread类的公用静态常量。
  7. 使用join()方法、sleep()方法、yield()方法可以改变线程的状态。
  8. 线程同步有两种方式,即同步方法和同步代码块。这两种方式都使用synchronized关键字来实现。
  9. Java提供了3个方法来实现线程之间的通信,即wait()方法、notify()方法和notifyAll()方法。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

道长爱睡懒觉

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

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

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

打赏作者

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

抵扣说明:

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

余额充值