97-Java多线程:概述、创建方式

多线程

一、线程

1、概述
  • 线程(thread)是一个程序内部的一条执行路径。

  • 我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。

    在这里插入图片描述

  • 程序中如果只有一条执行路径,那么这个程序就是单线程的程序。



二、多线程

1、概述
  • 多线程是指从软硬件上实现多条执行流程的技术。

2、好处
  • 举例说明:

    在这里插入图片描述

    • 1、比如网上购票,肯定是一堆用户在购票吧?所以在购票系统后台就需要多条线程来处理多个用户的购票请求。
    • 2、比如你在百度网盘,上传文件的同时有没有可能同时也在下载文件?所以在后台也需要多条线程来处理这些请求。
    • 3、再例如:消息通信、淘宝、京东系统都离不开多线程技术。


3、关于多线程需要学会什么?
(1)多线程的创建
  • 如何在程序中实现多线程,有哪些方式,各自有什么优缺点。

(2)Thread类的常用方法
  • 线程的代表是Thread类,Thread提供了哪些线程的操作给我们呢?

(3)线程安全、线程同步
  • 多个线程同时访问一个共享的数据的时候会出现问题,如何去解决?

(4)线程通信、线程池
  • 线程与线程间需要配合完成一些业务。
  • 线程池是一种线程优化方案,可以用一种更好的方式使用多线程。

(5)定时器、线程状态等
  • 如何在程序中实现定时器?
  • 线程在执行的过程中会有很多不同的状态,理解这些状态有助于理解线程的执行原理,也有利于面试。



4、多线程的创建
  • Thread类
    • Java是通过java.lang.Thread类来代表线程的。
    • 按照面向对象的思想,Thread类应该提供了实现多线程的方式。
(1)方式一
  • 继承Thread类

    • 1、定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
    • 2、创建MyThread类的对象
    • 3、调用线程对象的start()方法启动线程(启动后还是执行run()方法的)
    package com.app.d1_create_thread;
    
    /**
        1、定义一个MyThread类,继承Thread类
     */
    public class MyThread extends Thread {
        /**
            重写run方法,里面是定义线程的任务
         */
        @Override
        public void run() {
            for (int i = 1; i <= 3; i++) {
                System.out.println("第 " + i + " 个子线程在执行输出!");
            }
        }
    }
    
    package com.app.d1_create_thread;
    
    /**
        目标:多线程的创建方式一:继承Thread类
     */
    public class ThreadDemo01 {
        public static void main(String[] args) {
            // 2、创建MyThread类的对象
            Thread t = new MyThread();
    
            // 3、调用线程对象的start()方法启动线程(执行的还是run()方法)
            t.start();
    
            for (int i = 1; i <= 3; i++) {
                System.out.println("第 " + i + " 个主线程在执行输出~");
            }
        }
    }
    

    第一次运行:主、子线程会同时执行,顺序时随机的

    第 1 个主线程在执行输出~
    第 1 个子线程在执行输出!
    第 2 个子线程在执行输出!
    第 2 个主线程在执行输出~
    第 3 个主线程在执行输出~
    第 3 个子线程在执行输出!
    
    Process finished with exit code 0
    
    

    第二次运行:主、子线程会同时执行,顺序时随机的

    第 1 个主线程在执行输出~
    第 2 个主线程在执行输出~
    第 3 个主线程在执行输出~
    第 1 个子线程在执行输出!
    第 2 个子线程在执行输出!
    第 3 个子线程在执行输出!
    
    Process finished with exit code 0
    

方式一的优缺点:
  • 优点:编码简单。
  • 缺点:线程类已继承了Thread,无法再继承其他类,不利于扩展,如果线程有执行结果是不可以直接返回的(因为重写的run方法的返回值类型是void:无返回值)。

重点问题?

1、为什么不直接调用run()方法,而是调用start()启动线程?

  • 直接调用run()方法会被当成普通方法执行,此时相当于还是单线程执行

  • 只有调用start()方法才是启动一个新的线程执行(就是告诉CPU,run()方法是要以线程的方式启动)

    在这里插入图片描述



  • 第一次运行:直接调用run()方法被当成普通方法执行,此时相当于还是单线程执行。

    在这里插入图片描述

  • 第二次运行:直接调用run()方法被当成普通方法执行,此时相当于还是单线程执行。

    在这里插入图片描述

  • 第三次运行:直接调用run()方法被当成普通方法执行,此时相当于还是单线程执行。

    在这里插入图片描述



2、为什么不能把主线程任务放在子线程之前?

  • 这样主线程一直是先跑完的,相当于还是单线程执行

    在这里插入图片描述

  • 第一次运行:这样主线程一直是先跑完的,相当于还是单线程执行

    在这里插入图片描述

  • 第二次运行:这样主线程一直是先跑完的,相当于还是单线程执行

    在这里插入图片描述

  • 第三次运行:这样主线程一直是先跑完的,相当于还是单线程执行

    在这里插入图片描述



方式一总结

1、方式一是如何实现多线程的?

  • 继承Thread类
  • 重写run()方法
  • 创建线程对象
  • 调用start()方法启动

2、方式一的优缺点是什么?

  • 优点:编码简单
  • 缺点:存在单继承的局限性,线程类继承Thread后,不能继承其他类,不便于扩展



(2)方式二
Thread的构造器:
构造器说明
public Thread(String name)可以为当前线程指定名称
public Thread(Runnable target)封装Runnable对象成为线程对象
public Thread(Runnable target, String name)封装Runnable对象成为线程对象,并指定线程名称

  • 实现Runnable接口

    • 1、定义一个线程类MyRunnable实现Runnable接口,重写run()方法
    • 2、创建MyRunnable任务对象
    • 3、把MyRunnable任务对象交给Thread处理
    • 4、调用线程对象的start()方法启动线程
    package com.app.d1_create_thread;
    
    /**
        1、定义一个线程任务类MyRunnable,实现Runnable接口
     */
    public class MyRunnable implements Runnable {
        /**
            重写run()方法,里面是定义线程的任务
         */
        @Override
        public void run() {
            for (int i = 0; i < 6; i++) {
                System.out.println("子线程执行输出:" + i);
            }
        }
    }
    
    package com.app.d1_create_thread;
    
    /**
        目标:多线程的创建方式二,实现Runnable接口
     */
    public class ThreadDemo02 {
        public static void main(String[] args) {
            // 2、创建一个任务对象
            Runnable target = new MyRunnable();
    
            // 3、把任务对象交给Thread处理
            Thread t = new Thread(target);
    
            // 4、调用线程对象的start()方法启动线程(执行的还是run方法)
            t.start();
    
            // 主线程任务
            for (int i = 0; i < 6; i++) {
                System.out.println("主线程执行输出:" + i);
            }
        }
    }
    

    第一次运行:主、子线程会同时执行,顺序时随机的

    在这里插入图片描述


    第二次运行:主、子线程会同时执行,顺序时随机的

    在这里插入图片描述



方式二的优缺点
  • 优点:线程任务类只是实现接口,可以继续继承其他类和实现其他接口,扩展性强。
  • 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的(因为重写的run方法的返回值类型是void:无返回值)。


方式二总结

1、方式二如何实现创建线程的?

  • 定义一个线程任务类实现Runnable接口
  • 重写run()方法
  • 创建任务对象
  • 把任务对象交给Thread(线程对象)处理
  • 调用线程对象的start()方法启动线程

2、方式二的优缺点是什么?

  • 优点:线程任务类由于只是实现接口,因此可以继续继承一个其他类和实现多个其他接口,扩展性强
  • 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的(因为重写的run方法的返回值类型是void:无返回值)


(3)方式二的其他语法形式
  • 匿名内部类方式实现

    • 1、创建一个任务对象
    • 2、把任务对象交给Thread(线程对象)处理
    • 3、调用线程对象的start()方法启动线程
    package com.app.d1_create_thread;
    
    /**
        目标:多线程创建方式二的其他语法形式(匿名内部类方式实现)
     */
    public class ThreadDemo02Other {
        public static void main(String[] args) {
            // 1、创建一个任务对象
            Runnable target = new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 4; i++) {
                        System.out.println("子线程1执行输出:" + i);
                    }
                }
            };
            // 2、把任务对象交给Thread(线程对象)处理
            Thread t = new Thread(target);
            // 3、调用线程对象的start()方法启动线程
            t.start();
    
    
            // 简化1:
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 4; i++) {
                        System.out.println("子线程2执行输出:" + i);
                    }
                }
            });
            t2.start();
    
    
            // 简化2:
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 4; i++) {
                        System.out.println("子线程3执行输出:" + i);
                    }
                }
            }).start();
    
    
            // 简化3(Lambada):
            new Thread(() -> {
                    for (int i = 0; i < 4; i++) {
                        System.out.println("子线程4执行输出:" + i);
                    }
            }).start();
    
    
            // 主线程任务
            for (int i = 0; i < 4; i++) {
                System.out.println("主线程执行输出:" + i);
            }
        }
    }
    

    第一次运行:主、子线程会同时执行,顺序是随机的

    子线程1执行输出:0
    子线程2执行输出:0
    主线程执行输出:0
    子线程4执行输出:0
    主线程执行输出:1
    主线程执行输出:2
    子线程2执行输出:1
    子线程1执行输出:1
    子线程1执行输出:2
    子线程1执行输出:3
    子线程2执行输出:2
    子线程2执行输出:3
    主线程执行输出:3
    子线程3执行输出:0
    子线程4执行输出:1
    子线程4执行输出:2
    子线程4执行输出:3
    子线程3执行输出:1
    子线程3执行输出:2
    子线程3执行输出:3
    
    Process finished with exit code 0
    
    

    第二次运行:主、子线程会同时执行,顺序是随机的

    主线程执行输出:0
    子线程2执行输出:0
    子线程2执行输出:1
    子线程4执行输出:0
    子线程4执行输出:1
    子线程4执行输出:2
    子线程4执行输出:3
    子线程2执行输出:2
    子线程2执行输出:3
    子线程1执行输出:0
    子线程3执行输出:0
    子线程3执行输出:1
    子线程3执行输出:2
    子线程3执行输出:3
    主线程执行输出:1
    子线程1执行输出:1
    子线程1执行输出:2
    主线程执行输出:2
    子线程1执行输出:3
    主线程执行输出:3
    
    Process finished with exit code 0
    
    



(4)方式三
a. 重点问题?

1、前两种线程的创建方式都存在一个问题?

  • 它们重写的run()方法均不能直接返回结果
  • 不适合需要返回线程执行结果的业务场景

2、怎么解决这个问题呢?

  • JDK 5.0提供了CallableFutureTask来实现
  • 这种方式的优点是:可以得到线程执行的结果

b. FutureTask的API:
方法名称说明
public FutureTask<>(Callable call)把Callable对象封装成FutureTask对象
public V get() throws Exception获取线程执行call方法返回的结果

c. 利用Callable、FutureTask接口实现:

1、得到任务对象:

  • a. 定义线程任务类实现Callable接口,重写call()方法,封装线程的任务。
  • b. 创建Callable对象。
  • c. 用FutureTaskCallable对象封装成线程任务对象。

2、把线程任务对象交给Thread(线程对象)处理。

3、调用线程对象的start()方法启动线程,执行任务。

4、线程执行完毕后,通过FutureTaskget()方法去获取线程任务执行的结果。

package com.app.d1_create_thread;

import java.util.concurrent.Callable;

/**
    1、定义一个线程任务类MyCallable,实现Callable接口
        应该声明线程任务执行完毕后结果的数据类型:目前用的是String
 */
public class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    /**
        2、重写call()方法:里面定义的是线程的要执行的任务
     */
    @Override
    public String call() throws Exception {
        int sum = 0;    // 用于累加 1-n的和
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "子线程执行的结果是:" + sum;  // 返回线程任务执行完毕后的结果
    }
}
package com.app.d1_create_thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
    目标:线程的创建方式三,实现Callable接口,结合FutureTask的get方法去获取线程任务执行的结果
 */
public class ThreadDemo03 {
    public static void main(String[] args) {
        // 3、创建Callable任务对象
        Callable<String> call1 = new MyCallable(10); // 求1-10的和

        /**
            4、用FutureTask把Callable对象封装成线程任务对象:
                作用1:
                    因为Callable无法直接交给Thread处理,
                    FutureTask实现了RunnableFuture接口,RunnableFuture接口又继承了Runnable接口,
                    所以把Callable任务对象交给FutureTask对象,这样就可以把FutureTask对象交给Thread处理。
                作用2:
                    可以在线程任务执行完毕之后,通过调用其get()方法得到线程任务执行完成的结果。
         */
        FutureTask<String> f1 = new FutureTask<>(call1);

        // 5、把线程任务对象交给Thread(线程对象)处理
        Thread t1 = new Thread(f1);

        // 6、调用线程对象的start()方法启动线程,执行任务
        t1.start();



        Callable<String> call2 = new MyCallable(100); // 求1-10的和
        FutureTask<String> f2 = new FutureTask<>(call2);
        Thread t2 = new Thread(f2);
        t2.start();



        // 7、线程执行完毕后,通过FutureTask的get()方法,获取线程任务执行的结果
        try {
            // 如果f1任务没有执行完毕,这里的代码会等待,直到f1(线程1)执行完毕才提取结果
            String rs1 = f1.get();
            System.out.println("第一个结果:" + rs1);
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            // 如果f2任务没有执行完毕,这里的代码会等待,直到f2(线程2)执行完毕才提取结果
            String rs2 = f2.get();
            System.out.println("第二个结果:" + rs2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
第一个结果:子线程执行的结果是:55
第二个结果:子线程执行的结果是:5050

Process finished with exit code 0



d. 方式三的优缺点:
  • 优点
    • 线程任务类只是实现接口,可以继续继承一个其他类,实现多个其他接口,扩展性强。
    • 可以在线程任务执行完毕后去获取线程执行的结果。
  • 缺点
    • 编码复杂一点。



(5)三种方式总结

1、三种方式对比如何?

方式优点缺点
继承Thread类编程比较简单,可以直接使用Thread类中的方法扩展性较差,不能再继承其他类,
不能返回线程任务执行完成的结果
实现Runnable接口扩展性强,实现该接口的同时还可以继承其他的类编程相对复杂一点,
不能返回线程任务执行完成的结果
实现Callable接口扩展性强,实现该接口的同时还可以继承其他的类,
可以得到线程执行完成的结果
编程相对复杂
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值