Java——多线程编程(一):多线程的概述、创建、生命周期及调度(概念理解+应用举例)

简介: Java——多线程编程(一):多线程的概述、创建、生命周期及调度(概念理解+应用举例)

文章目录:


1.概述

1.1 进程 

1.2 线程 

1.2.1 单线程与多线程 

2.线程的创建

2.1 线程的生命周期 

2.2 使用Thread类实现多线程

2.3 使用Runnable接口实现多线程

2.4 使用Runnable接口相比Thread类的优势

2.5 使用Callable接口实现多线程(不再举例......

3.线程的调度

3.1 线程的优先级 

3.2 线程休眠 

3.3 线程让步

3.4 线程插队 


1.概述


1.1 进程 

在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是正在运行的程序。例如下面这张图中:👇👇👇

在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐一边聊天,但实际上这些进程并不是在同一时刻运行的。在计算机中,所有应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程,操作系统会为每个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。由于CPU运行速度非常快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序

的感觉。


1.2 线程 


定义:在多任务操作系统中,每个运行的程序都是一个进程,用来执行不同的任务,而在一个进程中还可以有多个执行单元同时运行,来同时完成一个或多个程序任务,这些执行单元可以看做程序执行的一条条线索,被称为线程。

注意:操作系统中的每一个进程中都至少存在一个线程,当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。


1.2.1 单线程与多线程

单线程都是按照调用顺序依次往下执行,没有出现多段程序代码交替运行的效果,而多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行。

多线程可以充分利用CUP资源,进一步提升程序执行效率

多线程看似是同时并发执行的,其实不然,它们和进程一样,也是由CPU控制并轮流执行的,只不过CPU运行速度非常快,故而给人同时执行的感觉。

2.线程的创建


2.1 线程的生命周期

·       新建状态:

使用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由JVM为其分配了内存,没有表现出任何线程的动态特征。它保持这个状态直到程序start()这个线程。

·       就绪状态:

当新建状态的线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

·       运行状态:

如果就绪状态的线程获取CPU资源(获得JVM调度),就可以执行run(),此时线程便处于运行状态(如果存在多个CPU,那么允许多个线程并行运行)。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

·       阻塞状态:

如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

·       等待阻塞:运行状态中的线程执行wait()方法,使线程进入到等待阻塞状态。

·       同步阻塞:线程在获取synchronized同步锁失败(因为同步锁被其他线程占用)

·       其他阻塞:通过调用线程的sleep() join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

·       死亡状态:

一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态(生命周期结束)。


2.2 使用Thread类实现多线程


说明Thread类是java.lang包下的一个线程类,用来实现Java多线程。

步骤创建一个Thread线程类的子类(子线程),同时重写Thread类的run()方法;

          创建该子类的实例对象,并通过调用start()方法启动线程。

          下面来举几个例子帮助大家理解:👇👇👇


//MyThread类继承Thread线程类
class MyThread extends Thread {
  @Override
  public void run() {
    for(int i=0;i<10;i++) {
      System.out.println(getName() + ":" + i);
    }
  }
}
public class Thread01 {
  public static void main(String[] args) {
    //创建多线程对象
    MyThread th1=new MyThread();
    MyThread th2=new MyThread();
    MyThread th3=new MyThread();
    //设置每个对象的名字
    th1.setName("Hello");
    th2.setName("World");
    th3.setName("JavaProject");
    /*调用start()方法启动线程
      *其内部调用了run()方法,实现多线程
     */
    th1.start();
    th2.start();
    th3.start();
  }
}

在这里可以看到,我们创建了3个线程,并且分别命名为:HelloWorldJavaProject,但是在运行结果中,并不是按照这样的顺序来执行的,顺序是完全打乱的。

这是因为Java程序属于抢占式调度,这3个线程是抢着执行的,这也就是多线程的体现。而且在你每次运行这段多线程代码的时候,你会发现运行结果并不一样,这是因为每次运行时,这3个线程多会去抢夺CPU资源来执行,就一个字:抢!!!

在上面这两张图中,第一张里面的实例方法是由Thread类的对象调用的,第二张里面的静态方法是直接由Thread类直接调用的!!!


2.3 使用Runnable接口实现多线程


说明Java只支持类的单继承,如果某个类已经继承了其他父类,就无法再继承Thread类来实现多线程。在这种情况下,就可以考虑通过实现Runnable接口的方式来实现多线程。

步骤创建一个Runnable接口的实现类,同时重写接口中的run()方法;

          创建Runnable接口的实现类对象;

          使用Thread有参构造方法创建线程实例,并将Runnable接口的实现类的实例对象作为参数传入;

          调用线程实例的start()方法启动线程。


//MyRunnable类继承实现Runnable接口实现多线程
class MyRunnable implements Runnable { 
  @Override
  public void run() {
    for(int i=0;i<10;i++) {
      //Runnable接口中没有getName()方法
      //这里使用Thread线程类中的currentThread()方法进而通过getName()方法获取线程对象
      System.out.println(Thread.currentThread().getName()
                + ":" + i);
    }
  }
}
public class Thread02 {
  public static void main(String[] args) {
    //创建一个接口子类对象
    MyRunnable mr=new MyRunnable();
    //使用Thread线程类的有参构造方法创建线程实例
    //将Runnable接口实现类的实例对象作为参数传入
    Thread th1=new Thread(mr,"Hello");
    Thread th2=new Thread(mr,"World");
    Thread th3=new Thread(mr,"JavaProject");
    //调用线程实例的start()方法启动线程
    th1.start();
    th2.start();
    th3.start();
  }
}

注意使用Runnable接口实现多线程的一些方法,在这里的3个线程同样是抢着去运行的!!! 


2.4 使用Runnable接口相比Thread类的优势


适合多个相同的程序代码的线程去共享同一个资源。

可以避免Java中的单继承的局限性。

增加程序的健壮性,实现解耦(把设置线程和开启线程分开)操作,代码可以被多个线程共享,代码和线程独立。

线程池只能放入实现Runnable接口或Callable接口的线程,不能直接放入继承Thread类的线程。


2.5 使用Callable接口实现多线程(不再举例......


说明:通过Thread类和Runnable接口实现多线程时,需要重写run()方法,但是由于该方法没有返回值,因此无法从多个线程中获取返回结果。为了解决这个问题,从JDK 5开始,Java提供了一个新的Callable接口,来满足这种既能创建多线程又可以有返回值的需求。

使用Callable接口实现多线程是通过Thread类的有参构造方法传入Runnable接口类型的参数来实现多线程,不同的是,这里传入的是Runnable接口的子类FutureTask对象作为参数,而FutureTask对象中则封装带有返回值的Callable接口实现类。

步骤创建一个Callable接口的实现类,同时重写Callable接口的call()方法。

          创建Callable接口的实现类对象。

          通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象。

          使用参数为FutureTask类对象的Thread有参构造方法创建Thread线程实例。

          调用线程实例的start()方法启动线程。

3.线程的调度


定义:程序中的多个线程是并发执行的,但并不是同一时刻执行,某个线程若想被执行必须要得到CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。

分时调度模型:是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU时间片。

抢占式调度模型(JVM默认方法):是指让可运行池中所有就绪状态的线程争抢CPU的使用权,而优先级高的线程获取CPU执行权的概率自然大于优先级低的线程。


3.1 线程的优先级 


在应用程序中,要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小

线程的优先级用1~10之间的整数来表示,数字越大优先级越高

除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级。

static int MAX_PRIORITY:表示线程的最高优先级,相当于值10
static int MIN_PRIORITY:表示线程的最低优先级,相当于值1
static int NORM_PRIORITY:表示线程的普通优先级,相当于值5

说明:程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如main线程具有普通优先级。

使用可以通过Thread类的setPriority(int newPriority)方法对其进行设置,该方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。


3.2 线程休眠 


如果想要人为地控制线程执行顺序,使正在执行的线程暂停,将CPU使用权让给其他线程,这时可以使用静态方法sleep(long millis)

该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态,这样其他的线程就可以得到执行的机会。

sleep(long millis) 方法会声明抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。


3.3 线程让步

线程让步可以通过 yield() 方法来实现,该方法和 sleep(long millis) 方法有点类似,都可以让当前正在运行的线程暂停,区别在于 yield() 方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。

当某个线程调用 yield() 方法之后,与当前线程优先级相同或者更高的线程可以获得执行的机会。


3.4 线程插队 

Thread类中也提供了一个 join() 方法来实现线程插队功能。

当在某个线程中调用其他线程的 join() 方法时,调用的线程将被阻塞,直到被 join() 方法加入的线程执行完成后它才会继续运行。

Thread类中还提供了带有时间参数的线程插队方法 join(long millis)。当执行带有时间参数的join(long millis) 进行线程插队时,必须等待插入的线程指定时间过后才会继续执行其他线程

相关文章
|
3天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
12 1
|
26天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
109 6
|
24天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
29 2
|
29天前
|
数据采集 存储 数据处理
Python中的多线程编程及其在数据处理中的应用
本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
|
1月前
|
Java API 调度
Java 线程的生命周期
在JDK 1.5之前,线程的生命周期包括五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。JDK 1.5及之后增加了三种阻塞状态,共六种状态:新建、可运行、终止、锁阻塞、计时等待和无限等待。这些状态描述了线程在操作系统和JVM中的不同阶段。
Java 线程的生命周期
|
29天前
|
存储 监控 安全
深入理解ThreadLocal:线程局部变量的机制与应用
在Java的多线程编程中,`ThreadLocal`变量提供了一种线程安全的解决方案,允许每个线程拥有自己的变量副本,从而避免了线程间的数据竞争。本文将深入探讨`ThreadLocal`的工作原理、使用方法以及在实际开发中的应用场景。
56 2
|
1月前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
58 6
|
1月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
2月前
|
Java 调度
[Java]线程生命周期与线程通信
本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。
42 1
[Java]线程生命周期与线程通信
|
1月前
|
开发框架 Java .NET
.net core 非阻塞的异步编程 及 线程调度过程
【11月更文挑战第12天】本文介绍了.NET Core中的非阻塞异步编程,包括其基本概念、实现方式及应用示例。通过`async`和`await`关键字,程序可在等待I/O操作时保持线程不被阻塞,提高性能。文章还详细说明了异步方法的基础示例、线程调度过程、延续任务机制、同步上下文的作用以及如何使用`Task.WhenAll`和`Task.WhenAny`处理多个异步任务的并发执行。