22.多线程入门

本章目标

  1. 基本概念
  2. 多线程的实现
  3. 线程调度

本章内容

Java特性之一就是支持多线程,引用多线程的目的充分利用cpu资源。在了解多线程之前我们先几个相关概念:

一、基本概念

1 、程序

程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指代码的集合。

2、 进程

进程是指程序的一次动态执行过程,通常我们说计算机中正在执行的程序就是进程,每个程序都会对应着一个进程。一个进程包含了从代码加载到执行完成的一个完整过程。

进程是操作系统进行资源分配和调用的最小单位,每一个进程都有它自己的内存空间和系统资源

3、线程

线程是进程中执行运算的最小单位,是被系统(CPU)独立调度和分派的基本单位。每个进程至少有一个线程,反过来一个线程只能属于一个进程,可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。线程可以对进程所有的资源进行调度和运算。线程既可以由操作系统内核来控制调度,也可以由用户程序进行控制调度。

多个CPU会去执行这三个进程,其中每个进程都包含着至少一个线程。每个进程都有自己的资源,而进程内的所有线程都共享进程包含的资源。

4、并行与并发

  • 并发:是指一个或若干个CPU对多个进程或线程之间进行多路复用,即CPU轮着执行多个任务,每个任务都执行一小段时间,从宏观上看起来就像是全部任务都在同时执行一样。
  • 并行:则是指多个进程或线程同一时刻被执行,这是真正意义上的同时执行,它必须要有多个CPU的支持。

如上图,对于并发来说,线程一线执行一段时间,然后线程二再执行一段时间,接着线程三再执行一段时间。每个线程都轮流得到CPU的执行时间,这种情况下只需要一个CPU即能够实现。

对于并行来说,线程一、线程二和线程三是同时执行的,这种情况下需要三个CPU才能实现。并发和并行都提升了CPU的资源利用率

5、多任务

5.1、基于进程

单进程计算机只能做一件事情,现在的计算机可以一边玩游戏(游戏进程),一边听音乐(音乐进程),所以我们常见的操作系统都是多进程操作系统。例如:Windows,Mac和Linux等,能在同一时间段内执行多个任务。

对于单核计算机来讲,游戏进程和音乐进程也不是同时进行的,因为CPU在某个时间点上只能做一件事,计算机是在游戏进程和音乐进程间做着频繁切换,且切换速度很快,所以我们感觉游戏和音乐在同时进行,但并不是同时执行的,可以提高CPU的使用率。

5.2、基于多线程

  • 多线程的作用不是提高执行速度,而是为了提高应用程序的命中率(使用率)

    我们程序在运行的时候,都是在抢CPU的时间片(执行权),如果是多线程的程序,那么在抢到CPU的执行权的概率应该比较单线程程序抢到的概率要大,也就是说,CPU在多线程程序中执行的时间要比单线程多,所以就提高了程序的执行效率,提高了运行速度

  • 处理I/O阻塞

    在做IO读写操作时,如果程序中有一个操作是接收键盘输入操作,如果用户没有输入,我们只能一直处于等待状态,而有了多线程以后,通过CPU切换,我们在阻塞同时还可以完成其它的操作

5.3、进程与线程实现多任务的区别

  1. 切换:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。
  2. 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
  3. 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
  4. 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
  5. 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

二、多线程的实现

多线程的实现主要有四种方式:

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口 (如果需要返回值可以采用该种方式)
  4. 线程池(ExecutorService 和 Executors)

本章只讲解继承Thread和实现Runnable两种方式,Callable实现方式可自行了解

1、 继承Thread

1.1、 Thread类中的核心方法

方法名称 是否static 方法说明
start() 让线程启动,进入就绪状态,等待cpu分配时间片
run() 重写Runnable接口的方法,线程获取到cpu时间片时执行的具体逻辑
yield() 线程的礼让,使得获取到cpu时间片的线程进入就绪状态,重新争抢时间片
sleep(time) 线程休眠固定时间,进入阻塞状态,休眠时间完成后重新争抢时间片,休眠可被打断
join()/join(time) 调用线程对象的join方法,调用者线程进入阻塞,等待线程对象执行完或者到达指定时间才恢复,重新争抢时间片
isInterrupted() 获取线程的打断标记,true:被打断,false:没有被打断。调用后不会修改打断标记
interrupt() 打断线程,抛出InterruptedException异常的方法均可被打断,但是打断后不会修改打断标记,正常执行的线程被打断后会修改打断标记
interrupted() 获取线程的打断标记。调用后会清空打断标记
stop() 停止线程运行 不推荐
suspend() 挂起线程 不推荐
resume() 恢复线程运行 不推荐
currentThread() 获取当前线程

Object中与线程相关方法

方法名称 方法说明
wait()/wait(long timeout) 获取到锁的线程进入阻塞状态
notify() 随机唤醒被wait()的一个线程
notifyAll(); 唤醒被wait()的所有线程,重新争抢时间片

1.2、 创建流程:

  1. 创建一个集成于Thread类的子类
  2. 重写Thread类的run()方法
  3. 创建Thread子类的对象
  4. 通过此对象调用start()方法

1.3、 示例

class MyThread extends Thread{  // 继承Thread类,作为线程的实现类

    public void run(){  // 覆写run()方法,作为线程 的操作主体
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName() + "运行,i = " + i) ;
        }
    }
};
public class ThreadDemo{
    public static void main(String args[]){
        MyThread mt1 = new MyThread("线程A ") ;    // 实例化对象
        MyThread mt2 = new MyThread("线程B ") ;    // 实例化对象
        mt1.start() ;   // 调用线程主体
        mt2.start() ;   // 调用线程主体
    }
};

两个线程对象是交错运行,哪个线程对象抢到了 CPU 资源,哪个线程就可以运行,所以程序每次的运行结果是不一样的

在线程启动时调用的是 start() 方法,但实际上调用的却是 run() 方法定义的主体。

如果直接调用run()方法会是什么结果?

2、实现Runnable接口

2.1、 创建流程

  1. 实现java.lang.Runable接口
  2. 在实现java.lang.Runable接口的新类中实现run()方法。
  3. 创建新类的实例。
  4. 利用新类的实例构造一个Thread的类实例。
  5. 在Thread类实例上调用start()方法。

2.2、 示例

class MyThread implements Runnable{ // 实现Runnable接口,作为线程的实现类

    public void run(){  // 覆写run()方法,作为线程 的操作主体
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName() + "运行,i = " + i) ;
        }
    }
};
public class RunnableDemo{
    public static void main(String args[]){
        MyThread mt1 = new MyThread("线程A ") ;    // 实例化对象
        //MyThread mt2 = new MyThread("线程B ") ;    // 实例化对象
        Thread t1 = new Thread(mt1) ;       // 实例化Thread类对象
        Thread t2 = new Thread(mt1) ;       // 实例化Thread类对象
        t1.start() ;    // 启动多线程
        t2.start() ;    // 启动多线程
    }
};

2.3、 比较创建线程的两种方式:

开发中,优先选择实现Runnable接口的方式:

  1. 实现的方式没有类的单继承性的局限性
  2. 实现的方式更适合用来处理多个线程有共享数据的情况

3、线程分类

Java中线程可以分为以下几类:

  • 用户线程
    • 主线程
    • 子线程
  • 守护线程(如垃圾回收线程,异常处理线程)

3.1、 主线程

在运行一个应用的时候,这个时候系统会开一个进程。然后这个进程启动了Main线程, Java进程确定虚拟机中没有线程运行的时候,退出进程 java应用程序中会有一个main函数,是作为某个类的方法出现的。当程序启动时,该函数就会第一个自动得到执行,并成为程序的主线程。就是说,main函数是一个应用的入口,也代表了这个应用主线程。

3.2、 子线程

子进程是由父进程创建并启动的,在Java中两者没有本质区别。JVM在启动时,首先创建main线程,去执行main方法。在main方法中创建其他的线程后,如果main线程执行完毕,其他线程也会继续执行。

如果main方法中又创建了其它线程,那么JVM就会在主线程和其它线程之间轮流切换,保证每个线程都有机会使用CPU资源,main方法即使执行完最后一句,JVM也不会结束程序,JVM直到所有用户线程都结束之后,才结束应用程序。

3.3、 守护线程

关于GC内容可以查看

所谓的守护线程,也叫后台线程,是指在程序运行的时候提供一种通用服务的线程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件.

典型的守护线程例子是JVM中的系统资源自动回收线程,后台音乐播放器也是守护线程,

守护线程特点:

  • 守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。
  • 守护线程并不属于程序中不可或缺的部分。因此当所有的非守护线程 (用户线程) 结束时,程序也就终止,同时会杀死所有守护线程。
  • 只要有任何非守护线程还在运行,程序就不会终止。
  • 守护线程在不执行finally子句的情况下就会终止其run方法。守护线程创建的子线程也是守护线程
  • 守护线程在没有用户线程可服务时自动离开,这个线程具有最低的优先级,用于为系统中的其它对象和线程提供服务。
  • 将一个用户线程设置为守护线程的方式是在线程对象创建之前调用线程对象的setDaemon()方法
  • 非守护线程包括常规的用户线程或诸如用于处理GUI事件的事件调度线程。

示例:

import java.io.IOException;

public class TestThread extends Thread {
    public void run() {// 线程的run方法,它将和其他线程同时运行
        for (int i = 1; i <= 100; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            } finally {
                System.out.println("finally" + i);
            }
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
        TestThread test = new TestThread();
        test.setDaemon(true);
        test.start();
        System.out.println("isDaemon = " + test.isDaemon());
        try {
        // 接受输入,使程序在此停顿,一旦接收到用户输入,main线程结束,守护线程自动结束,而不会有任何希望出现的确认形式,如finally子句不执行
            System.in.read();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

}

从结果可以看出,子线程并没有无线循环的打印,而是在主线程(main())退出后,JVM强制关闭所有后台线程。而不会有任何希望出现的确认形式,如finally子句不执行。 test.setDaemon(true);可以先把它注释掉,这时TestThread为非守护线程,这样即使接收到数据也不会停止

4、线程状态

线程的状态可以从以下几方面进行划分:

4.1、 几种状态

操作系统层面分为五种状态 :

  1. 新建状态:新建线程对象,并没有调用start()方法之前
  2. 就绪状态:调用start()方法之后线程就进入就绪状态,但是并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态。
  3. 运行状态:线程获取到cpu的时间片,执行run()方法的逻辑
  4. 阻塞状态:线程被阻塞,放弃cpu的时间片,等待解除阻塞重新回到就绪状态争抢时间片
  5. 死亡状态:线程执行结束

从java api层面分为六种状态:

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

说明:

状态 解释
初始(NEW) 新创建了一个线程对象,但还没有调用start()方法
运行(RUNNABLE) 就绪(ready)和运行中(running)两种状态笼统的称为“运行”
阻塞(BLOCKED) 表示线程阻塞于锁,线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态
等待(WAITING) 进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断),它们要等待被显式地唤醒,否则会处于无限期等待的状态,调用wait()、join()等方法后的状态
超时等待(TIMED_WAITING) 该状态不同于WAITING,它可以在指定的时间后自行返回, 调用sleep(time)、wait(time)、join(time)
终止(TERMINATED) 表示该线程已经执行完毕

4.2、 图解

4.3、 示例

注意try catch的位置,要把整个循环括起来,不能放到while里面,否则不起作用

class MyThread extends Thread {

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        try {
            int i=0;
            while (!isInterrupted()) {
                Thread.sleep(100); // 休眠100ms
                i++;
                System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException.");
        }
    }
}

public class Demo {

    public static void main(String[] args) {
        try {
            Thread t1 = new MyThread("t1");  // 新建“线程t1”
            System.out.println(t1.getName() +" ("+t1.getState()+") is new.");

            t1.start();                      // 启动“线程t1”
            System.out.println(t1.getName() +" ("+t1.getState()+") is started.");

            // 主线程休眠300ms,然后主线程给t1发“中断”指令。
            Thread.sleep(300);
            t1.interrupt();
            System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");

            // 主线程休眠300ms,然后查看t1的状态。
            Thread.sleep(300);
            System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

如何终止线程?

1、当线程由于被调用了sleep(), wait(), join()等方法而进入阻塞状态;若此时调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时产生一个InterruptedException异常。接合isInterrupted()方法,将InterruptedException放在适当的为止就能终止线程

2、如果没有满足上面条件,while (!isInterrupted())中如果拿到中断标记为true,也会退出循环,从而结束当前线程

3、可以改变Thread.sleep(100)中的时间来看到不同效果,当300时会是抛异常,而当10时并没有抛异常,而是程序正常结束

更多参考:https://blog.csdn.net/fmwind/article/details/83624415

运行结果

t1 (NEW) is new.
t1 (RUNNABLE) is started.
t1 (RUNNABLE) loop 1
t1 (RUNNABLE) loop 2
t1 (TIMED_WAITING) is interrupted.
t1 (RUNNABLE) catch InterruptedException.
t1 (TERMINATED) is interrupted now.

结果说明

  1. 主线程main中通过new MyThread(“t1”)创建线程t1,之后通过t1.start()启动线程t1。
  2. 启动之后,会不断的检查它的中断标记,如果中断标记为“false”;则休眠100ms。
  3. t1休眠之后,会切换到主线程main;主线程再次运行时,会执行t1.interrupt()中断线程t1。
  4. t1收到中断指令之后,而且会抛出InterruptedException异常。
  5. 在t1的run()方法中,是在循环体while之外捕获的异常;因此循环被终止。

三、线程调度

1、 调度策略:

  • 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片

  • 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。

    JVM默认采用抢占式调度模型,通常情况下程序员不需要去关心它,但在某些特定的需求下需要改变这种模式,由程序自己来控制CPU的调度。

2、线程的优先级

2.1、 Java中的线程优先级是在Thread类中定义的常量:

  • NORM_PRIORITY:值为 5
  • MAX_PRIORITY:值为 10
  • MIN_PRIORITY:值为 1

默认优先级为: NORM_PRIORITY。

2.2、 有关优先级的方法:

  • 更改线程的优先级: final void setPriority(int newPriority)
  • 返回线程的优先级:final int getPriority()

注意!:高优先级的线程要抢占低优先级的线程的cpu的执行权。但是仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完以后,低优先级的线程才执行

示例:

public class PriThread
    {
        public static void main(String args[ ])
        {
            ThreadA a=new ThreadA();
            ThreadB b=new ThreadB();
            a.setPriority(2);//设置优先级别,数值越大优先级越高
            b.setPriority(3);
            a.start();
            b.start();
        }
    }
posted @ 2025-04-09 14:40  icui4cu  阅读(31)  评论(0)    收藏  举报