【Java】线程学习笔记


参考书目: Java 2实用教程(第5版) 耿祥义 张跃平 编著

一、 相关概念

1.1 进程

程序是一段静态的代码,是应用软件执行的蓝本。
进程是程序的一次动态执行过程,经历了从代码加载、执行、到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展甚至消亡的过程。

1.2 线程

进程和线程都是实现并发的一个基本单位。线程不是进程,是比进程更小的执行单位,在进程的基础之上进行的进一步划分。每个线程自身也有产生、存在和消亡的过程。

1.3 多进程

多线程是指一个进程在执行过程中可以产生多个线程,这些线程可以同时存在、同时运行。一个进程可能包含多个同时执行的线程。

多进程提供了一种更为快速的程序处理机制,也是Java的主要特点之一。多进程操作系统能同时运行多个进程,依据CPU分时机制,每个进程都能循环获得自己的CPU时间片。由于CPU执行速度非常快,使得所有程序好像是在同时运行。

二、Java中的线程

2.1 主线程 main线程

每个Java应用程序都有一个缺省的主线程。Java程序总是从主类的main方法开始执行。JVM加载代码发现main方法之后会启动主线程(main线程),该线程负责执行main方法。main方法执行中再创建的线程,成为程序的其他线程。

如果main方法中没有创建其他的线程,main方法执行完最后一个语句(main方法返回),JVM结束Java应用程序。

如果main方法中创建了其他线程,JVM会在主线程和其他线程之间轮流切换,保证每个线程都有机会使用CPU资源,即使主线程结束,JVM也不会结束Java应用程序,直到程序中的所有线程结束之后,才会结束Java应用程序。

2.2 线程的状态与生命周期

通过Thread类及其子类对象表示线程,新建的线程具有4种状态。

1)新建

当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。此时已经有了相应的内存空间和其他资源。

2)运行

线程创建之后就具有了运行条件,一旦轮到它来使用CPU资源时,此线程就可以脱离创建它的主线程独立开始生命周期。

线程创建后仅仅是占有内存资源,在JVM管理的线程中还没有这个线程,必须调用从父类继承的start()方法通知JVM,此时才会将该线程加入等候队列等待切换了。

JVM将CPU使用权切换给线程时,如果线程是Thread的子类创建的,该类中的run()方法 (规定了该线程的具体使命) 就立刻执行。因此程序必须在子类中重写父类的run()方法(Thread类中的run()方法没有具体内容,需要重写子类中的run()方法覆盖父类的run()方法)。

⭐ 注意:线程没有结束run()方法之前不要再次调用start()方法,否则会发生IllegalThreadStateException 异常。

3)中断

中断的4种原因

  • JVM 将CPU资源从当前线程切换给其他线程,本线程让出CPU使用权,处于中断状态。

  • 线程使用CPU期间,执行了sleep(int millsecond)方法,使当前线程进入休眠状态。

    程序一旦执行sleep(int millsecond)方法,立刻让出CPU使用权,当前线程处于中断状态。经过millsecond指定的毫秒数之后,该线程重新进入线程队列排队等待CPU资源,进而从中断处继续运行。

  • 线程使用CPU期间,执行了wait()方法,使当前线程进入等待状态。

    等待状态的线程不会主动进入线程队列中排队等待CPU资源,在notify()方法通知后,重新进入线程队列等待CPU资源,进而从中断处继续运行。

  • 线程使用CPU资源期间,执行某些操作可能会引起堵塞。

进入阻塞状态的线程无法进入排队队列,只有引起阻塞的原因消除,线程才能重新进去排队队列,从中断处开始继续运行。

4)死亡

死亡状态就是线程释放实体,即释放分配给线程对象的内存。处于死亡状态的线程无法继续运行。

死亡的两种原因:

  • 线程正常运行完成全部工作。run()方法中的全部语句执行完毕,结束run()方法。
  • 线程被提前强制终止,结束run()方法。

三、 线程优先级

处于就绪状态的线程首先进入就绪队列等候CPU资源,同一时刻在就绪队列中的线程可能有多个。Java虚拟机中的线程调度器负责管理线程,用常数1-10表示优先级,若未明确设置线程的优先级,通常为5。

线程的优先级可以通过setPriority(int grade)方法调整,该方法需要一个int类型参数。如果参数不在1~10的范围内,那么setPriority便产生一个legalArgumenException异常。

getPriority方法返回线程的优先级。需要注意是,有些操作系统只识别3个级别:1、5和10。

在采用时间片的系统中,每个线程都有机会获得CPU的使用权,以便使用CPU资源执行线程中的操作。当线程使用CPU资源的时间到时后,即使线程没有完成自己的全部操作,JVM也会中断当前线程的执行,把CPU的使用权切换给下一个排队等待的线程,当前线程将等待CPU资源的下一次轮回,然后从中断处继续执行。JVM的线程调度器的任务是使高优先级的线程能始终运行,一旦时间片有空闲,则使具有同等优先级的线程以轮流的方式顺序使用时间片。

e.g. 如果有A、B、C、D四个线程,A和B的级别高于C和D,那么,Java调度器首先以轮流的方式执行A和B,一直等到B都执行完毕进入死亡状态,才会在C、D之间轮流切换。

四、Thread类与线程的创建

4.1. 使用Thread类

使用Thread子类创建线程的优点是:可以在子类中增加新的成员变量,使线程具有某种属性,也可以在子类中新增加方法,使线程具有某种功能。

创建线程的另一个途径就是用Thread类直接创建线程对象。使用Thread创建线程通常使拥的构造方法是:Thread(Runnable target)。该构造方法中的参数是一个Runnable类型的接口。因此,在创建线程对象时必须向构造方法的参数传递一个实现Runnable接口类的实例,该实例对象称作所创建线程的目标对象,当线程调用start()方法后,一旦轮到它来享用CPU资源,目标对象就会自动调用接口中的run)方法(接口回调),这一过程是自动实现的,用程序只需要让线程调用start方法即可。线程绑定于Runnable接口,也就是说,当线程被调度并转入运行状态时,所执行的就是runO方法中所规定的操作。

public class Example{
	public static void main(String args[]){
		Thread speakElephant; // 用Thread声明线程
		Thread speakCar;
		ElphantTarget elephant; // elephant是目标对象
		CarTarget car; 
		elephant = new ElphantTarget(); // 创建目标对象
		car = new Car();
		speakElephant = new Thread(elephant);
		speakCar = new Thread(car);
		speakElephant.start(); // 启动线程
		speakCar.start();
		for(int i=0;i<=3;i++){
			System.out,println("类目:" + i + " ");
		}
	}
}
/**
 * 实现runnable接口
 */
public class ElephantTarger implements Runnable{
	public void run(){
		for(int i=0;i<=5;i++){
			System.out,println("大象:" + i + " ");
	}
}

public class CarTarger implements Runnable{
	public void run(){
		for(int i=0;i<=5;i++){
			System.out,println("轿车:" + i + " ");
	}
}

线程间可以共享相同的内存单元(代码、数据),并利用共享单元实现数据交换、实时通信与必要的同步操作。

对于Thread创建的线程,轮到它来使用CPU资源时,目标兑现挥自动调用接口中的run()方法,对于同一目标对象的线程,目标对象的成员变量就是这些线程共享的数据单元。

创建目标对象的类在必要时还可以某个特定的子类,因此使用Runnabele接口比使用Thread的子类更灵活。

-----------------------------------不重要的分割线-------------------------------------------

public class Example2{
	public static void main(String args[]){
		House house = new House();
		house.setWater(10);
		Thread dog,cat;
		dog = new Thread(house); // dog和cat的目标对象相同
		cat = new Thread(house);
		dog.setName("狗");
		cat.setName("猫");
		dog.start();
		cat.start();
	}
}

public class House implements Runnable{
	int waterAmount; //用int变量模拟水量
	public void setWater(int w){
		waterAmount = W;
	}
	public void run(){
		while(true){
			String name=Thread.currentThread().getName();
			if(name.equals("狗")){
				System.out.println(name+"喝水");
				waterAmount=waterAmount-2; // 狗喝的多
			}else if((name.equals("猫")){
				System.out.println(name+”喝水");
				waterAmount=waterAmount-1; // 猫喝的少
			}
			Sysem.out.printin("剩"+waterAmount);
			try{
				Thread.sleep(2000); // 间隔时间
			}
			catch(InterruptedException e){
				if(wat erAmount<=0){
					return;
				}
			}
		}
	}

该例使用Thread类创建两个模拟猫和狗的线程,猫和狗共享房屋中的一桶水,房屋是线程目标的对象。水喝完时,结束线程。在轮流喝水过程中,调用sleep(int n)进入休眠状态,而不是强制中断喝水。

4.2 目标对象与线程的关系

从对象和对象之间的关系角度看,目标对象和线程有以下两种关系

  • 完全解耦
    Example2创建目标对象的House类不包含对cat和dog线程对象的引用(完全解耦)。此时,目标对象无法获得线程对象的引用,需要* 通过获得线程的名字* 确定哪个线程正在占用CPU资源(被JVM执行的线程)。
String name = Thread.currentThread().getName();
  • 目标对象组合线程(弱耦合)
    目标对象将线程作为自己的成员,比如让线程cat和dog在House中。当创建目标对象的类组合线程对象时,可 通过线程 获得线程对象的引用。
Thread.currentThread();

e.g.

public class Example4{
	public static void main(String args[]){
		House house = new House();
		house.setWater(10);
		house.dog.start();
		house.cat.start();
	}
}

public class House implements Runnable{
	int waterAmount; // int模拟水量
	Thread dog,cat;  // 线程是目标对象的成员
	House(){
		dog = new Thread(this);  // 当前house对象作为线程的目标对象
		cai = new Thread(this);
	}
	
	public void setWater(int w){
		waterAmount = w;
	}

	public void run(){
		while(true){
			Thread t = Thread.currentThread();
			if(t==dog){
				System.out.println("狗喝水");
				waterAmount = waterAmount-2; 
			}else if(t==cat){
				System.out.println("猫喝水");
				waterAmount = waterAmount-1; 
			}
			System.out.println("还剩"+waterAmount);
			try{
				Thread.sleep(2000);
			}catch(InterruptedException e){
			}
			if(waterAmount<=0){
				return;
			}
		}
	}
	
}

五、线程的常用方法

  • start() 启动线程,从新建→就绪
    一旦该线程使用CPU资源时,就可以脱离创建它的线程独立开始自己的生命周期

  • run()
    Thread类的run(方法与Runnable接口中的run0方法的功能和作用相向,都用来定义线程对象被调度之后所执行的操作,都是系统自动调用而用户程序不得引用的方法。系统的Thread类中,run()方法没有具体内容,所以用户程序需要创建自己的Thread类的子类,并重写run()方法来覆盖原来的run()方法。当run方法执行完毕,线程就变成死亡状态,所谓死亡状态就是线程释放了实体,即释放分配给线程对象的内存。在线程没有结束run()方法之前,不赞成让线程再调用start()方法,否则将发生IllegalThreadStateException异常。

  • sleep(int millsecond)
    线程的调度执行是按照其优先级的高低顺序进行的,当高级别的线程未死亡时,低级别线程没有机会获得CPU资源。有时,优先级高的线程需要优先级低的线程做一些工作来配合它,或者优先级高的线程需要完成一些费时的操作,此时优先级高的线程应该让出CPU资源,使优先级低的线程有机会执行。为达到这个目的,优先级高的线程可以在它的run()方法中调用sleep方法来使自己放弃CPU资源,休眠一段时间。休眠时间的长短由sleep方法的参数决定,millsecond是以毫秒为单位的休眠时间。如果线程在休眠时被打断,JVM就抛出InterruptedException异常。因此,必须在try-catch语句块中调用sleep方法。

  • isAlive()
    线程处于新建状态时,线程调用isAlive(O方法返回false。当一个线程调用start)方法,并占有CPU资源后,该线程的run()方法就开始运行,在线程的run0方法结束之前,即没有进入死亡状态之前,线程调用isAlive()方法返回true。当线程进入死亡状态后(实体内存被释放),线程仍可以调用方法isAlive(),这时返回的值是false。

  • currentThread()
    currentThread()方法是Thread类中的类方法,可以用类名调用,该方法返回当前正在用CPU资源的线程。

  • interrupt()
    interrupt方法经常用来“吵醒”休眠的线程。当一些线程调用sleep方法处于休眠状态时一个占有CPU资源的线程可以让休眠的线程调用interrupt()方法“吵醒”自己,即导致休的线程发生InterruptedException异常,从而结束休眠,重新排队等待CPU资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值