创建线程的4种方法

目录

一.前言

1.关于进程调度

(1)为什么要调度?

(2)调度的真正对象

(3)调度的资源

2.线程 

(1).线程的写法

 (2)线程创建的方法

1.继承Thread

(1)使用继承Thread,重写run的方式来创建线程

(2)继承Thread,使用匿名内部类

2.实现Runnable

(1)使用实现Runnable,重写run

(2)实现Runnable,使用匿名内部类

3.基于lambda表达式

4.实现callable

三.优缺点总结

继承Thread类

实现Runnable接口

基于Lambda表达式

实现Callable接口

四.总结体会


一.前言

1.关于进程调度

(1)为什么要调度?

通俗来说,就是狼多肉少.

计算机中的CPU,内存等资源都是很有限的.

(2)调度的真正对象

CPU是按照并发的方式来执行进程的

引入进程,就是为了能够实现多个任务并发执行这样的效果

进程有个重大的问题就是比较重量,如果频繁的创建/销毁进程,成本会比较高

进程里面包括线程,一个进程里可以有一个线程,或者多个线程
每个线程都是一个独立的执行流.多个线程之间,也是并发执行的

多个线程可能是在多个 CPU 核心上, 同时运行
也可能是在一个 CPU 核心上, 通过快速调度,进行运行

 操作系统,真正调度的,是在调度线程,而不是进程

线程是 提作系统 调度运行 的基本单位
进程是 操作系统 资源分配 的基本单位

前面所说的进程调度,指的是这些进程里面只有一个线程

(3)调度的资源

当我们创建了一个进程之后,操作系统会创建一个PCB,把这个PCB加入到链表上

PCB中提供了一些属性,进程的优先级,进程的状态,进程的上下文,进程的记账信息...

一个进程中的多个线程之间,共用同一份系统资源

1)内存空间

2) 文件描述符表

只有在进程启动,创建第一个线程的时候,需要花成本去申请系统资源一旦进程(第一个线程)创建完毕,此时,后续再创建的线程,就不必再申请资源了,创建/销毁 的效率就提高了不少了.

既然线程的效率这么高,那是不是线程越多越好呢?

当然不是

CPU的核心数是固定的,此时创建出大量线程,没法立即被处理的线程就只能阻塞等待,就算此时强行进行调度,调度上了一个线程,那也势必会挤掉其它线程,总并发程度仍然是固定的.

真正有效果的是,再搞几个CPU,也就是再搞一个主机,这也就是我们所说的分布式系统

关于分布式系统,详情可见我的另一篇文章http://t.csdn.cn/DdQHj

由于线程就是进程的一部分,因此,如果一个线程出现异常,那么很有可能其它线程也会不能运行.

因此,我们要能够明确区分进程和线程之间的区别:

  • 进程包含线程
  • 进程有自己独立的内存空间和文件描述符表.同一个进程中的多个线程之间,共享同一份地址空间和文件描述符表
  • 进程是操作系统资源分配的基本单位,线程是操作系统调度执行的基本单位.
  • 进程之间具有独立性,一个进程挂了,不会影响到别的进程:同一个进程里的多个线程之间,一个线程挂了,可能会把整个进程带走,影响到其他线程的

那么Java怎么进行多线程编程呢?

首先,大家可能会有一个疑问,为什么Java不学习多进程编程呢?

虽然Java里提供了一组多进程编程的API,但是JDK里面没有封装这些多进程的API,因此Java里不提倡多进程编程.

2.线程 

接下来,我们就来具体学习多线程编程

(1).线程的写法

我们先来了解一下线程. 

Java标准库里提供了一个类Thread能够表示一个线程.

package thread;

class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("Hello Thread");
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t=new MyThread();
        t.start();
    }
}

我们来分析这段代码

创建线程的4种方法_第1张图片

上述代码涉及到两个线程

1.main方法所对应的线程(一个进程里面至少有一个线程),也可以称为主线程

2.通过t.start创建新的线程

我们现在对代码进行调整,具体体会一下,"每一个线程是一个独立的执行流"

代码如下:

package thread;

class MyThread extends Thread{
    @Override
    public void run(){
        while(true){
            System.out.println("Hello Thread");
        }
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t=new MyThread();
        t.start();
        while(true){
            System.out.println("hello main");
        }
    }

}

 

 运行结果如下创建线程的4种方法_第2张图片

我们可以看到,hello Thread 和hello main都能打印出来

创建线程的4种方法_第3张图片

run()方法

run叫做入口方法,不是构造方法

run方法不是我们随便写的一个方法,是重写了父类的方法

这种重写一般是功能的扩展,一般这样的重写方法不需要我们自己手动调用,已经有其它代码来调用

run方法可以成为是一个特殊的方法,也就是线程的入口方法

而start方法,是调用操作系统中的api,创建新线程,新的线程里面调用run方法

 (2)线程创建的方法

线程创建主要有以下几种方法:

1.继承Thread

2.实现Runnable

3.基于lambda

4.实现callable

接下来,我们来详细介绍这几类方法

1.继承Thread
(1)使用继承Thread,重写run的方式来创建线程
class MyThread extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("Hello t");
            }
        }
    }
    
public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t=new MyThread();
        //start会创建新的线程
        t.start();
        //run不会创建新的线程,run是在main的线程中执行的
        while(true){
            System.out.println("Hello main");
        }
    }
}

运行结果如下: 

创建线程的4种方法_第4张图片

同时打印的原理是由于两个线程在同时执行,并且每个线程都有自己的输出流。在这段代码中,主线程和MyThread线程都在执行无限循环,分别打印"Hello main"和"Hello t"。

当两个线程同时执行时,它们会竞争CPU的资源,操作系统会根据调度算法来决定哪个线程获得CPU的执行权。由于线程的执行速度非常快,所以看起来就像是同时执行。

那这里的hello main的打印和hello t的打印有什么规律吗?

实际上是没有的,这是由于调度的随机性

当两个线程同时执行时,它们会竞争CPU的资源,操作系统会根据调度算法来决定哪个线程获得CPU的执行权。由于线程的执行速度非常快,所以看起来就像是同时执行。

每个线程都有自己的输出流,所以它们可以独立地打印输出。

当主线程执行System.out.println("Hello main")时,它会将输出发送到主线程的输出流中。而MyThread线程执行System.out.println("Hello t")时,它会将输出发送到MyThread线程的输出流中。

由于输出流是独立的,所以两个线程的输出可以同时显示在控制台上。但是由于输出的速度和顺序是不确定的,所以可能会出现交错的情况,即"Hello t"和"Hello main"的输出顺序可能会不一致。

(2)继承Thread,使用匿名内部类
public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t=new Thread(){
            @Override
            public void run() {
                while(true){
                    System.out.println("Hello t");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {//sleep睡眠过程中就将其打断
                        // throw new RuntimeException(e);
                        e.printStackTrace();
                    }
                }
            }
        };

        t.start();
        while(true){
            System.out.println("Hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {//sleep睡眠过程中就将其打断
                // throw new RuntimeException(e);
                e.printStackTrace();
            }
        }
    }
}

运行结果如下:

创建线程的4种方法_第5张图片

2.实现Runnable
(1)使用实现Runnable,重写run
class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("Hello t");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {//sleep睡眠过程中就将其打断
               // throw new RuntimeException(e);
                e.printStackTrace();
            }
        }
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        MyRunnable runnable=new MyRunnable();
        Thread t=new Thread(runnable);
        t.start();

        while(true){
            System.out.println("Hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {//sleep睡眠过程中就将其打断
                // throw new RuntimeException(e);
                e.printStackTrace();
            }
        }
    }

}

我们可以看到打印的结果如下: 

创建线程的4种方法_第6张图片

Runnable的字面意思是可运行的,使用Runnable 来描述一个具体的任务

第一种写法是使用Thread的run来描述线程入口

这一种是使用Runnable interface 描述线程入口

这两种方法之间并没有本质区别,只是使用方法的不同

(2)实现Runnable,使用匿名内部类
public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("Hello t");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {//sleep睡眠过程中就将其打断
                        // throw new RuntimeException(e);
                        e.printStackTrace();
                    }
                }
            }
        });

        t.start();
        while(true) {
            System.out.println("Hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {//sleep睡眠过程中就将其打断
                // throw new RuntimeException(e);
                e.printStackTrace();
            }
        }
    }
}

运行结果如下: 

创建线程的4种方法_第7张图片

3.基于lambda表达式

这个方法是创建线程最推荐的写法,使用lambda表达式,这也是最简单直观的写法.

在Java里面,函数(方法)是无法脱离类的,但是lambda就相当于一个例外,所以这样的函数一般都是一次性的,用完就会被销毁.

lambda表达式的基本写法

()->{


}

()里面放参数,如果只有一个参数,可以省略() 

{}里面存放函数体,如果这里面只有一行代码,也可以省略{}

举一个代码例子:

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t=new Thread(() -> {
            while(true){
                System.out.println("Hello t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {//sleep睡眠过程中就将其打断
                    // throw new RuntimeException(e);
                    e.printStackTrace();
                }
            }
        });


        t.start();
        while(true) {
            System.out.println("Hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {//sleep睡眠过程中就将其打断
                // throw new RuntimeException(e);
                e.printStackTrace();
            }
        }
    }
}

运行结果如下:

创建线程的4种方法_第8张图片

4.实现callable

Callable的用法非常类似于Run

使用Runnable写出的代码描述了一个任务,也就是一个线程要做什么.

然而,Runnable通过run方法描述,返回类型是void.但是很多时候,我们是希望任务有返回值的.二+而callable的call方法就是由返回值的.

比如说,我们写个代码,创建一个线程,用这个来计算1+2+...+1000.

我们来看具体的代码:

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

public class ThreadDemo27 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        Callable callable = new Callable() {
            int sum = 0;

            @Override
            public Integer call() throws Exception {
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        //找一个线程完成这个任务
        //Thread不能直接传入callable,需要再包装一层
        FutureTask futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        System.out.println(futureTask.get());

    }
}

运行结果如下: 

创建线程的4种方法_第9张图片

我们对代码进行分析: 

创建线程的4种方法_第10张图片

三.优缺点总结

  1. 继承Thread类
    • 优点:继承Thread类可以直接重写run()方法,非常简单直观。
    • 缺点:由于Java不支持多继承,所以如果使用继承Thread类创建线程,就无法再继承其他类。
  2. 实现Runnable接口
    • 优点:实现Runnable接口可以避免单继承的限制,可以继续继承其他类。
    • 缺点:需要额外定义一个类来实现Runnable接口,并重写run()方法。
  3. 基于Lambda表达式
    • 优点:使用Lambda表达式可以更简洁地创建线程,不需要显式地创建一个新的类或实现接口。
    • 缺点:Lambda表达式可能会降低代码的可读性,特别是对于复杂的线程逻辑。
  4. 实现Callable接口
    • 优点:Callable接口可以返回线程执行的结果,可以通过Future对象获取线程的返回值。
    • 缺点:使用Callable接口创建线程相对复杂,需要使用ExecutorService来执行Callable任务,并获取返回值。

四.总结体会

继承Thread类和实现Runnable接口是最常见的线程创建方法,它们都可以实现多线程的功能。

使用Lambda表达式可以简化线程的创建过程,特别适合简单的线程逻辑。

实现Callable接口可以获取线程的返回值,适用于需要线程执行结果的场景。

在选择线程创建方法时,我们需要根据具体的需求和代码结构来选择合适的方法。

你可能感兴趣的:(java,开发语言,学习,后端)