Java多线程基础-4:详解Thread类及其基本用法 (一)

简介: Java 中的 `Thread` 类是用来管理线程的,每个线程都是通过 `Thread` 类的对象来描述。

一、认识线程-Thread


一个线程就是一个“执行流”。每个线程都可以按照顺序执行自己的代码,而多个线程可以 "同时" 执行多份代码。线程本身是操作系统中的概念。操作系统内核实现了线程这样的机制,并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库)。而Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装。


Thread 类是 JVM 用来管理线程的一个类。在Java中,每个线程执行流都是通过Thread类的对象来描述的。JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。


使用Thread类,我们可以创建和管理多个线程,并控制它们的执行顺序、优先级、暂停、恢复等。


1、Thread类的常见构造方法





Thread()  :无参的构造方法。


Thread(Runnable target)  :Runnable是一个接口类型。该构造方法需要一个Runnable类型的target参数。具体如何使用,在下文“创建线程”的部分会详细说明。


Thread(String name)  /  Thread(Runnable target, String name):在创建线程的同时自定义一个线程对象名。这个操作相当于是给线程起了一个别名,方便后续查找辨认该线程,不影响程序的正常运行。不指定名称,系统也会给线程一个默认的名称。我们可以用 jconsole 工具来查看Java程序中的线程:


先用Java代码创建两个线程(如何创建线程仍然会在下文说明),并运行程序:




在 jconsole 中可以看到:



这里的Thread-0,Thread-1就是系统默认给我们线程的命名。


此时,如果我们给线程 t1 指定一个name为"hello_t1",再次用 jconsole 查看线程情况:




可见此时,t1 的名称就显示为我们自定义的"hello_t1"了。而 t2 未自定义命名,因此还是默认的名称。但是,起名只是方便标识,无论名称如何,都不影响程序的正常运行。


2、Thread 的几个常见属性





  • ID 是线程的唯一标识,不同线程不会重复。


        System.out.println(t1.getId());
        System.out.println(t2.getId());



  • 名称就是我们上面提到的线程的命名,通常各种调试工具会用到。



  • 状态表示线程当前所处的一个情况,下文中我们会进一步说明。


  • 是否存活,可以简单地理解为 :run 方法是否运行结束了



  • 优先级高的线程理论上来说更容易被调度到,但并不绝对。因为优先级只是“建议”性质的,不是强制性质的。优先级高意味着“建议”操作系统优先调度某线程,但实际到底要不要优先调度,还是取决于操作系统。


  • 线程分后台线程和前台线程。关于后台线程:JVM会在一个进程的所有非后台线程(也就是前台线程)结束后,才会结束运行。创建的线程默认是前台线程,main线程也是一个前台线程。


Daemon 表示一个后台进程(也叫守护进程)。.isDaemon()方法可以获得线程是前台进程还是后台进程。true表示是后台进程,false表示是前台进程。


.setDaemon()方法可以手动设置一个线程为前台线程或后台线程。具体演示如下:



t1是后台线程,进程随着main线程(前台线程)的终止而终止


如果我们将 t1.setDaemon(true) 语句删除,会怎样呢?此时t1就不再是后台进程,而是一个前台进程了,只有等到t1线程也结束,整个进程才会结束。但由于t1中有死循环,因此进程不会结束。



t1是前台线程,由于t1中有死循环,进程没有终止



二、Thread类的基本用法


1、创建线程


方法一  继承 Thread 类


通过继承 Thread 类的方式来创建线程,主要分为 4 步:


  1. 自定义类 MyThread 类 继承 Thread类。


  1. 在 MyThread类 中重写 run() 入口方法。


  1. 在 main 中创建(new)MyThread类 的实例 t 。


  1. 通过 t 调用 start() 方法,启动线程。


具体代码演示如下:


 
// 1、通过继承Thread类来创建线程
class MyThread extends Thread {
    // 2、重写 run() 方法
 
    @Override
    public void run() {
        System.out.println("i am t!");
    }
}
 
public class Test {
    public static void main(String[] args) {
        // 3、创建 MyThread 实例
        MyThread t = new MyThread();
 
        // 4、调用 实例t 的 start() 方法
        t.start();
 
        // main 线程中的方法
        System.out.println("i am main!");
    }
}


程序运行结果:



方法二  实现 Runnable 接口


与上面的继承Thread类的方法类似,通过 Runnable 接口创建线程,主要有以下 4 个步骤:


  1. 实现 Runnable 接口。


  1. 重写run()方法。


  1. 创建 Thread 类实例 , 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数。Thread有多个重载的构造方法,其中有一个是通过实现过 Runnable 接口的实例来构造,如图:



    因此,我们直接在构造器的实参处,new一个实现了Runnable接口的类的实例即可。


  1. 调用 start() 方法启动线程。


// 1、通过实现Runnable接口来创建线程
class MyRunnable implements Runnable {
    // 2、重写 run() 方法
    @Override
    public void run() {
        System.out.println("i am t!");
    }
}
 
public class Test {
    public static void main(String[] args) {
        // 3、创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数
        Thread t = new Thread(new MyRunnable());
 
        // 4、调用 实例t 的 start() 方法
        t.start();
 
        // main 线程中的方法
        System.out.println("i am main!");
    }
}


包括该方法在内的所有创建线程的程序运行结果均同上。


方法三  使用匿名内部类


通过匿名内部类实现也有两种方式,一种是使用Thread的匿名内部类,另一种是使用Runnable的匿名内部类:


匿名内部类创建 Thread 子类对象


public class Test {
    public static void main(String[] args) {
        //匿名内部类 Thread
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("i am t!");
            }
        };
 
        t.start();
 
        // main 线程中的方法
        System.out.println("i am main!");
    }
}


匿名内部类创建 Runnable 子类对象


特别注意:Runnable匿名内部类以及大括号的书写位置。由于Runnable实例是要作为Thread构造方法参数传入的,因此Runnable的匿名内部类应当写在new Thread()的括号内。


public class Test {
    public static void main(String[] args) {
        // 匿名内部类 Runnable
        // 注意:Runnable实例作为Thread构造器的参数传入
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("i am t!");
            }
        });
 
        t.start();
 
        // main 线程中的方法
        System.out.println("i am main!");
    }
}


方法四(最常用)  采用lambda表达式


查看Runnable接口的Java源码可以发现,Runnable接口是一个函数式接口。因此,我们可以通过lambda表达式来简便地创建一个线程。



public class Test {
    public static void main(String[] args) {
        // lambda表达式
        Thread t  = new Thread(() -> {
            System.out.println("i am t!");
        });
 
        t.start();
 
        // main 线程中的方法
        System.out.println("i am main!");
    }
}


2、启动线程        start()


(1)start()方法


start()方法是启动线程的方法,调用该方法会使线程进入就绪状态,等待CPU分配时间片后开始执行。run()方法是线程的执行体,它包含了线程要执行的代码;当调用start()方法启动线程后,线程会在独立的执行路径上自动执行run()方法中的代码(也就是说,当调用start()启动线程后,系统会自动调用run()方法执行线程的执行体)。


但要注意的是,run()方法并不标识新的线程的创建;调用 start 方法,才真的在操作系统的底层创建出了一个线程。


start()方法的使用:在创建完线程后,通过线程的实例调用start()即可。上面已经演示了很多,这里就不再多说。


(2)start()方法与run()方法的区别--代码演示


事实上,也可以通过线程对象的实例调用run()方法:




但这两个方式有本质区别。


t1.start() 方法,才是真正创建了一个新的线程。当我们运行如下代码,可以看到,"i am t!"与"i am main!"两句交替打印,这时t1线程与main线程并发执行的结果,两个线程即不断打印"i am t!"和不断打印"i am main!"是同时执行的:




但当我们对比着执行下面的代码,会发现程序只打印了"i am t!",并不打印"i am main!"。道理很简单:此时并没有创建出新的线程,打印"i am t!"与打印"i am main!"是同一线程中的先后关系。但由于打印"i am t!"是一个死循环,所以程序就卡在了这里,无法向下执行到打印"i am main!"了。



上述代码与下面这样写的实际运行效果相同:




(3)start()方法与run()方法的区别--总结


由上面的代码演示可知:在调用start()方法启动线程后,系统会自动调用线程的run()方法;如果直接调用run()方法,那么线程不会启动,而是在当前线程中直接执行run()方法中的代码,这种情况下不会有新的线程产生。


因此,run()方法只是普通的方法调用,而start()方法则会创建一个新的线程并启动它,让它在新的线程中执行run()方法中的代码。正确使用start()方法可以实现并发执行多个任务,从而提高程序的性能。


概括来说有如下几点区别:


a. 作用功能不同:


  1. run方法的作用是描述线程具体要执行的任务。


  1. start方法的作用是真正的去申请系统线程。


b. 运行结果不同:


  1. run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;


  1. start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。


ava多线程基础-4:详解Thread类及其基本用法(二)+

https://developer.aliyun.com/article/1520504?spm=a2c6h.13148508.setting.14.61564f0er7vvAy

相关文章
|
2天前
|
Java 调度
【Java基础】 线程状态转化
Java线程状态转化
15 2
|
5天前
|
缓存 NoSQL Java
Java高并发实战:利用线程池和Redis实现高效数据入库
Java高并发实战:利用线程池和Redis实现高效数据入库
22 0
|
1天前
|
Java 程序员
Java多线程编程是指在一个进程中创建并运行多个线程,每个线程执行不同的任务,并行地工作,以达到提高效率的目的
【6月更文挑战第18天】Java多线程提升效率,通过synchronized关键字、Lock接口和原子变量实现同步互斥。synchronized控制共享资源访问,基于对象内置锁。Lock接口提供更灵活的锁管理,需手动解锁。原子变量类(如AtomicInteger)支持无锁的原子操作,减少性能影响。
15 3
|
1天前
|
数据采集 安全 算法
Java并发编程中的线程安全与性能优化
在Java编程中,多线程并发是提升程序性能的关键之一。本文将深入探讨Java中的线程安全性问题及其解决方案,并介绍如何通过性能优化技术提升多线程程序的效率。
9 3
|
1天前
|
Java 调度
【Java基础】 多线程
Java、多线程编程
11 0
|
4天前
|
监控 Java API
Java 程序设计 第八章 线程
Java 程序设计 第八章 线程
|
4天前
|
存储 安全 Java
Java多线程编程--JUC
Java多线程编程
|
4天前
|
Java
深入理解 Java 8 函数式接口:定义、用法与示例详解
深入理解 Java 8 函数式接口:定义、用法与示例详解
7 1
|
5天前
|
Java API
详细探究Java多线程的线程状态变化
Java多线程的线程状态主要有六种:新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)和终止(TERMINATED)。线程创建后处于NEW状态,调用start()后进入RUNNABLE状态,表示准备好运行。当线程获得CPU资源,开始执行run()方法时,它处于运行状态。线程可以因等待锁或调用sleep()等方法进入BLOCKED或等待状态。线程完成任务或发生异常后,会进入TERMINATED状态。
|
5天前
|
存储 安全 Java
Java多线程中线程安全问题
Java多线程中的线程安全问题主要涉及多线程环境下对共享资源的访问可能导致的数据损坏或不一致。线程安全的核心在于确保在多线程调度顺序不确定的情况下,代码的执行结果始终正确。常见原因包括线程调度随机性、共享数据修改以及原子性问题。解决线程安全问题通常需要采用同步机制,如使用synchronized关键字或Lock接口,以确保同一时间只有一个线程能够访问特定资源,从而保持数据的一致性和正确性。