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

相关文章
|
7天前
|
存储 安全 Java
java.util的Collections类
Collections 类位于 java.util 包下,提供了许多有用的对象和方法,来简化java中集合的创建、处理和多线程管理。掌握此类将非常有助于提升开发效率和维护代码的简洁性,同时对于程序的稳定性和安全性有大有帮助。
35 17
|
3天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
26 4
|
3天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
11 2
|
3月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
107 1
|
6月前
|
设计模式 监控 Java
Java多线程基础-11:工厂模式及代码案例之线程池(一)
本文介绍了Java并发框架中的线程池工具,特别是`java.util.concurrent`包中的`Executors`和`ThreadPoolExecutor`类。线程池通过预先创建并管理一组线程,可以提高多线程任务的效率和响应速度,减少线程创建和销毁的开销。
188 2
|
6月前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
57 1
|
3月前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
75 6
|
3月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(中)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
83 5
|
3月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(上)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
79 3
|
4月前
|
设计模式 存储 安全
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
Java面试题:设计一个线程安全的单例类并解释其内存占用情况?使用Java多线程工具类实现一个高效的线程池,并解释其背后的原理。结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
61 1