Java多线程--创建多线程的基本方式一:继承Thread类

发布时间:2024年01月20日

一、创建和启动线程

(1)概述

  • Java语言的JVM允许程序运行多个线程,使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。 (创建一个线程可以理解为创建这个类的一个对象,一个对象对应一个线程
  • Thread类的特性
    • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,因此把run()方法体称为线程执行体
    • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
    • 要想实现多线程,必须在主线程中创建新的线程对象。

image.png

(2)方式1:继承Thread类

1、使用步骤

Java通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

如下:

image.png

举例代码如下:

package com.atguigu.thread;
//自定义线程类
public class MyThread extends Thread {
    //定义指定线程名称的构造方法
    public MyThread(String name) {
        //调用父类的String参数的构造方法,指定线程的名称
        super(name);
    }
    /**
     * 重写run方法,完成该线程执行的逻辑
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+":正在执行!"+i);
        }
    }
}

测试类:

package com.atguigu.thread;

public class TestMyThread {
    public static void main(String[] args) {
        //创建自定义线程对象1
        MyThread mt1 = new MyThread("子线程1");
        //开启子线程1
        mt1.start();
        
        //创建自定义线程对象2
        MyThread mt2 = new MyThread("子线程2");
        //开启子线程2
        mt2.start();
        
        //在主方法中执行for循环
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程!"+i);
        }
    }
}

image.png

注意:

  1. 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
  2. run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
  3. 想要启动多线程,必须调用start方法。
  4. 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException”。

2、举例

2.1 案例1

🌋描述:创建一个分线程1,用于遍历100以内的偶数。

🚗分析

<1> 创建一个继承于Thread类的子类

public class EvenNumberTest {

}

//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{

}

<2> 重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中

public class EvenNumberTest {

}

//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{
    //2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if(i%2==0){
                System.out.println(i);
            }
        }
    }
}

<3> 创建当前Thread的子类的对象

public class EvenNumberTest {
    public static void main(String[] args) {
        //3.创建当前Thread的子类的对象
        PrintNumber t1=new PrintNumber();

    }
}

<4>通过对象调用start(): 1.启动线程 2.调用当前线程的run()

public class EvenNumberTest {
    public static void main(String[] args) {
        //3.创建当前Thread的子类的对象
        PrintNumber t1=new PrintNumber();

        //4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()
        t1.start();
    }
}

//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{
    //2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if(i%2==0){
                System.out.println(i);
            }
        }
    }
}

当前PrintNumber类里面没有重写start方法,这就意味着调用的是父类Thread里面的start方法。

这个start()方法有什么作用呢?

image.png

所以start方法有两个作用:1.启动线程 2.调用当前线程的run()方法。

在刚才的代码中,t1调用start()方法,这里的start()方法是父类中的

此时调用了当前线程的run()方法,这个方法在Thread类里面定义的,并且在子类PrintNumber里面被重写了,所以父类中的方法被覆盖了,此时调用的就是子类的run()方法

整体来看,调用t1.start(),子类中的run()方法就被执行了。


🌱整体代码

package yuyi01.thread;

/**
 * ClassName: EvenNumberTest
 * Package: yuyi01.thread
 * Description:
 *      创建一个分线程1,用于遍历100以内的偶数
 * @Author 雨翼轻尘
 * @Create 2024/1/19 0019 11:57
 */
public class EvenNumberTest {
    public static void main(String[] args) {
        //3.创建当前Thread的子类的对象
        PrintNumber t1=new PrintNumber();

        //4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()
        t1.start();
    }
}

//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{
    //2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if(i%2==0){
                System.out.println(i);
            }
        }
    }
}

🍺输出结果(部分)

image.png

2.2 案例2

刚才并没有感觉到线程的存在啊,现在修改一下代码:

package yuyi01.thread;

public class EvenNumberTest {
    public static void main(String[] args) {
        //3.创建当前Thread的子类的对象
        PrintNumber t1=new PrintNumber();

        //4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()
        t1.start();

        //main()方法所在的线程执行的操作:
        for (int i = 1; i <= 100; i++) {
            if(i%2==0){
                System.out.println(i+"main()做的事情");
            }
        }
    }
}

//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{
    //2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if(i%2==0){
                System.out.println(i);
            }
        }
    }
}

此时有两个线程:

image.png

再次执行代码:(部分)

image.png

此时它们没有交互,如果数字足够多,会有交叉的数据出现,就是交替执行

这说明两个线程都在执行,前面的执行一下,后面的执行一下。谁先执行都有可能。这里就体现出了两个不同的线程

2.3 案例3

其实这里还有一种方式可以看出来是两个线程在交替执行,需要用到一个方法,后面再说,这里先用一下。

Thread有个方法叫做currentThread()获取当前执行的线程;然后它又有一个getName()方法,即获取线程的名字。

即:

Thread.currentThread().getName()

🌱代码

package yuyi01.thread;

/**
 * ClassName: EvenNumberTest
 * Package: yuyi01.thread
 * Description:
 *      创建一个分线程1,用于遍历100以内的偶数
 * @Author 雨翼轻尘
 * @Create 2024/1/19 0019 11:57
 */
public class EvenNumberTest {
    public static void main(String[] args) {
        //3.创建当前Thread的子类的对象
        PrintNumber t1=new PrintNumber();

        //4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()
        t1.start();

        //main()方法所在的线程执行的操作:
        for (int i = 1; i <= 10000; i++) {
            if(i%2==0){
                System.out.println(Thread.currentThread().getName() +":" +i+"**********");
            }
        }
    }
}

//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{
    //2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
        for (int i = 1; i <= 10000; i++) {
            if(i%2==0){
                System.out.println(Thread.currentThread().getName() +":" +i);
            }
        }
    }
}

🍺输出结果(部分)

image.png

可以看到,现在的执行结果,前面都有自己线程的名字。

t1线程也有自己的线程名字,默认叫Thread-0。因为此时new的是当前类PrintNumber的对象,默认的是调用super(),是父类Thread里面的构造器,如下:

image.png

所以,当我们不给线程起名字的时候,默认第一个线程就是Thread-0,第二个线程就是Thread-1

注意下面:

image.png

3、两个问题

3.1 问题1

?问题1

启动线程,包括调用上面的run方法都用的是start()

那么能否使用t1.run()来替换t1.start()的调用,实现分线程的创建和调用?

🍰分析

比如现在这样写:

image.png

运行看一下:

image.png

可以看到不仅没有交替执行,而且还都是main,没有Thread-0了。它认为run()方法是主线程main做的。

此时就是主线程造了一个t1对象,然后就调用了run()这个普通的方法,执行完之后再执行循环输出。只有一条线程了。这就不是多线程问题了。


再举个例子,看看下面的代码是不是多线程:

public class SingleThread {
    public void method1(String str) {
        System.out.println(str);
    }

    public void method2(String str) {
        method1(str);
    }

    public static void main(String[] args) { //main线程
        SingleThread s = new SingleThread();
        s.method2("hello!");
    }

}

首先main方法进去,造了一个当前类的一个对象s,这个对象s调用method2(),method2()里面调用method1()。这里是单线程
判断单线程还是多线程,就看能不能拿一条线穿起来。

比如此时就可以拿一条线穿起来,只有一条执行路径,那就是一个线程,即单线程。如下:

image.png

之前的那个例子,也是类似,就是一个线程。如下:(用鼠标手画的有点不堪入目哈哈)

image.png

start()不一样,一个作用是启动线程,然后是调用run方法

若是仅仅调用run方法的话,线程并没有启动,相当于还是只有主线程。

所以,不能使用t1.run()来替换t1.start()的调用。

3.2 问题2

?问题2

比如现在启动了一个线程,调用一下start(),打印了1000以内的偶数。代码如下:

package yuyi01.thread;

public class EvenNumberTest {
    public static void main(String[] args) {
        //3.创建当前Thread的子类的对象
        PrintNumber t1=new PrintNumber();

        //4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()
        t1.start();

        //main()方法所在的线程执行的操作:
        for (int i = 1; i <= 1000; i++) {
            if(i%2==0){
                System.out.println(Thread.currentThread().getName() +":" +i+"**********");
            }
        }
    }
}

//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{
    //2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
        for (int i = 1; i <= 1000; i++) {
            if(i%2==0){
                System.out.println(Thread.currentThread().getName() +":" +i);
            }
        }
    }
}

现在需要再创建一个分线程,同样要去遍历一下1000以内的偶数

那么此时要怎样去做呢?

🍰分析

直接用t1再次调用start()方法可以吗?如下:

image.png

运行结果:

image.png

可以看到,出现了IllegalThreadStateException的异常。

当我们首次调用start()方法的时候,threadStatus的值是0 ,就没有抛异常。当我们再次调用start()之后,threadStatus状态就不是0了,便会抛异常。

image.png

所以,线程不能start()多次。

不能让已经start()的线程,再次执行start()操作,否则报IllegalThreadStateException非法线程状态的异常。


🎲解决方案

既然上面的方法不行,那么究竟应该怎么做呢?

需要重新创建一个对象,类PrintNumber不需要重新创建了,因为要做的事情一样,都是遍历1000以内的偶数。

所以现在只需要再建立一个对象,然后用这个新的对象去调用start()方法即可。

如下:

image.png

🌱整体代码

package yuyi01.thread;

public class EvenNumberTest {
    public static void main(String[] args) {
        //3.创建当前Thread的子类的对象
        PrintNumber t1=new PrintNumber();

        //4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()
        t1.start();

        /*
        * 问题2:再提供一个分线程,用于100以内偶数的遍历
        *
        * 注意:不能让已经start()的线程,再次执行start()操作,否则报IllegalThreadStateException非法线程状态的异常。
        * */
        //t1.start();
        PrintNumber t2=new PrintNumber();
        t2.start();



        //main()方法所在的线程执行的操作:
        for (int i = 1; i <= 1000; i++) {
            if(i%2==0){
                System.out.println(Thread.currentThread().getName() +":" +i+"**********");
            }
        }
    }
}

//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{
    //2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
        for (int i = 1; i <= 1000; i++) {
            if(i%2==0){
                System.out.println(Thread.currentThread().getName() +":" +i);
            }
        }
    }
}

🍺输出结果(部分)

image.png

输出默认的线程名也很好理解,造的第二个对象就是Thread-1

从上面的输出结果可以看到,三个线程交替执行。

4、代码及总结

线程的创建方式一:继承Thread类

步骤:

① 创建一个继承于Thread类的子类

② 重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中

③ 创建当前Thread的子类的对象

④ 通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法

start()是父类Thread的方法。

🌱整体代码总结

package yuyi01.thread;

/**
 * ClassName: EvenNumberTest
 * Package: yuyi01.thread
 * Description:
 *      创建一个分线程1,用于遍历100以内的偶数
 * @Author 雨翼轻尘
 * @Create 2024/1/19 0019 11:57
 */
public class EvenNumberTest {
    public static void main(String[] args) {
        //3.创建当前Thread的子类的对象
        PrintNumber t1=new PrintNumber();

        //4.通过对象调用start(): 1.启动线程 2.调用当前线程的run()
        t1.start();

        /*
        问题1:能否使用t1.run()来替换t1.start()的调用,实现分线程的创建和调用?    不能
        */
        //t1.run();


        /*
        * 问题2:再提供一个分线程,用于100以内偶数的遍历
        *
        * 注意:不能让已经start()的线程,再次执行start()操作,否则报IllegalThreadStateException非法线程状态的异常。
        * */
        //t1.start();
        PrintNumber t2=new PrintNumber();
        t2.start();



        //main()方法所在的线程执行的操作:
        for (int i = 1; i <= 1000; i++) {
            if(i%2==0){
                System.out.println(Thread.currentThread().getName() +":" +i+"**********");
            }
        }
    }
}

//1.创建一个继承于Thread类的子类
class PrintNumber extends Thread{
    //2.重写Thread类的run() --->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
        for (int i = 1; i <= 1000; i++) {
            if(i%2==0){
                System.out.println(Thread.currentThread().getName() +":" +i);
            }
        }
    }
}

🍺输出结果(部分)

image.png

二、练习

🌋题目描述

练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数。

(1)方式一

🍰分析

【回顾】

线程的创建方式一:继承Thread类

步骤:

① 创建一个继承于Thread类的子类

② 重写Thread类的run() —>将此线程要执行的操作,声明在此方法体中

③ 创建当前Thread的子类的对象

④ 通过对象调用start(): 1.启动线程 2.调用当前线程的run()方法

【分析】

现在是两个线程,做的事情不一样。run方法里面执行的方法体就不一样了。

两个分线程,这里就不要使用主线程了。

既然两个线程指定的事情不一样,那就写两个run,这就意味着要声明两个类了(要是两个线程做的事情一样,那就只用写一个类,就像上面的案例)。

🌱代码(方式一)

package yuyi01.thread.exer1;

/**
 * ClassName: PrintNumberTest
 * Package: yuyi01.thread.exer1
 * Description:
 *
 * @Author 雨翼轻尘
 * @Create 2024/1/19 0019 22:38
 */
public class PrintNumberTest {
    public static void main(String[] args) {
        //方式一
        EvenNumberPrint t1=new EvenNumberPrint();
        OddNumberPrint t2=new OddNumberPrint();

        t1.start();
        t2.start();
    }
}

//打印偶数
class EvenNumberPrint extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":"+i);
            }
        }
    }
}


//打印奇数
class OddNumberPrint extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + "-->"+i);
            }
        }
    }
}

🍺输出结果(部分)

image.png

(2)方式二

🍰分析

上面的方式一是一种标准写法,之前还讲过匿名子类的方式,需要提供Thread子类的对象。

所以直接new一个Thread(),因为是匿名子类,后面直接加一对大括号,这样对象就造好了,如下:

new Thread(){}

这时可以声明为t1,然后通过t1去调start(),如下:

Thread t1=new Thread(){};
t1.start();

当然,我们可以不声明它为t1,直接在后面.start(),如下:

new Thread(){}.start();

在大括号里面,可以将run()方法重写一下。如下:

//方式二
new Thread(){
    //打印偶数
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":"+i);
            }
        }
    }
}.start();


new Thread(){
    //打印奇数
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + "-->"+i);
            }
        }
    }
}.start();

方法二可以叫做,创建Thread类的匿名子类的匿名对象,造完对象之后还顺便将start()方法给调用了。

🌱代码(方式二)

package yuyi01.thread.exer1;

/**
 * ClassName: PrintNumberTest
 * Package: yuyi01.thread.exer1
 * Description:
 *
 * @Author 雨翼轻尘
 * @Create 2024/1/19 0019 22:38
 */
public class PrintNumberTest {
    public static void main(String[] args) {

        //方式二
        new Thread(){
            //打印偶数
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + ":"+i);
                    }
                }
            }
        }.start();


        new Thread(){
            //打印奇数
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    if (i % 2 != 0) {
                        System.out.println(Thread.currentThread().getName() + "-->"+i);
                    }
                }
            }
        }.start();

    }
}

🍺输出结果(部分)

image.png

可以看到,方法一与方法二的效果一样。

若是以后,有一个临时需求,造一个线程做个事情,可能都不会按照方式一那么正规地写,都是采用匿名的方式做一下。

文章来源:https://blog.csdn.net/m0_55746113/article/details/135708814
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。