文章目录

  • 一、Thread 类
  • 1.1 跨平台性
  • 二、Thread 类里的常用方法
  • 三、创建线程的方法
  • 1、自定义一个类,继承Thread类,重写run方法
  • 1.1、调用 start() 方法与调用 run() 方法来创建线程,有什么区别?
  • 1.2、sleep()方法
  • 2、自定义一个类,实现Runnable接口,重写run方法
  • 3、继承 Thread 类,重写 run 方法,基于匿名内部类
  • 4、实现 Runnable 接口,重写 run 方法,基于匿名内部类
  • 5、匿名内部类
  • 5.1、回调函数的使用场景
  • 6、使用 Callable 接口


一、Thread 类

线程本身就是OS(操作系统简称OS,以下统一使用OS表示操作系统)提供的概念,OS也提供了一些 API 供程序员使用,譬如说:Linux 提供 pthread ,而在Java中,就把OS提供的 API 进行了封装,统一使用 Thread 类,供程序员在Java代码中调用来创建/操作线程。

1.1 跨平台性

那可能有同学疑惑了,为啥Java要封装OS提供的操作线程的API,自己提供一个Thread类供Java程序员调用来操作线程呢??

这是因为Java语言的特性:跨平台。

只要是学习过Java语言的同学,肯定听说过:一次编译,终身运行 这句话吧。其实就是在描述Java语言的跨平台性。

那么Java语言如何实现其跨平台性呢??我简单描述一下吧,以便大家更深刻理解Java为啥要封装一个 Thread 类 供程序员调用,而不直接使用OS提供的API。

JVM 通过把不同OS提供的不同的API统一封装成相同风格的API给Java程序员使用,因此JVM就能够屏蔽不同的OS的差异。

此时Java程序员写程序代码,就不需要考虑当前写的这个程序是在哪个OS上运行,运行时是否适配此OS,因为这些问题已经由JVM解决了。

二、Thread 类里的常用方法

我们通过 Thread类 创建线程时,需要先了解 Thread 类中有哪些常用方法。
Java官方文档对Thread类里的方法介绍

三、创建线程的方法

1、自定义一个类,继承Thread类,重写run方法

第一种创建线程的方式就是:自定义一个类,并且使该类继承自Java标准库 Thread 类,此时自定义的类需要重写 run() 方法。

注意:重写的 run() 方法,要处理异常时,只能 try {} catch (),并不能 throws,这是因为 Thread 类中的 run() 方法并没有throws xxx这样的设定。

重写的 run() 方法里书写的逻辑代码就是我们创建出来的新线程,所要执行的任务。

自定义一个继承自 Thread 类的自定义类,并且在该类中重写 Thread 类里的 run()方法后,并没有真正创建出一个新线程,还需要调用 start() 方法,让它真正被创建出来并执行起来。

代码展示如下:

class MyThread extends Thread {
//    重写 run() 方法
    @Override
    public void run() {
        while (true) {
            System.out.println("hello world!");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


public class testThread1 {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
//       真正创建一个新的线程
        thread.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }

}

上述代码中,main() 方法中有一个 while循环,新线程的 run() 方法中也有一个 while 循环,这两个循环都是死循环。主线程(main)和新创建出的线程都在分别执行自己的循环。这两个线程都能参与到cpu的调度中,这两个线程是在并发执行,那么此时的运行结果就比较复杂不确定了,每台机子的性能都不一样,多个线程并发执行时,到底执行哪个线程,不知道,要看操作系统的调度。

我电脑的运行结果:

java单开一个子线程 java创建子线程_java单开一个子线程

再来看看以下代码片段含有什么问题:

class MyThread extends Thread {
//    重写 run() 方法
    @Override
    public void run() {
        while (true) {
            System.out.println("hello world!");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


public class testThread1 {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
//       没有创建一个新的线程
        thread.run();
    }
}

运行结果:

java单开一个子线程 java创建子线程_回调方法_02

上述代码片段作了一些改动:可以发现当前代码并没有调用 start() 方法去真正创建一个新的线程出来,而是调用了 run() ,但是其运行结果居然与调用 start() 方法一致,这是为什么呢??此处就涉及到一个问题:调用 start() 方法与调用 run() 方法来创建线程,有什么区别?

1.1、调用 start() 方法与调用 run() 方法来创建线程,有什么区别?

首先我们要明确:什么叫做创建出了新的线程,什么叫做没有创建出新的线程?

创建出了新的线程就是:每个线程都能够独立的调度执行。

当我们调用 start() 方法创建线程时,是真正的创建出了新的线程。此时的OS就会在底层调用创建线程的API,同时会在系统内核中创建出对应的PCB结构,并且将此PCB加入到对应的链表中。此时这个新创建出来的线程就会参与到cpu的调度中,执行任务。

run() 方法只是上面的入口方法,并没有去调动系统的API,在系统内核中创建出一个对应的PCB结构,因此并没有创建出新的线程。

以往我们只有一个线程,那就是main主线程,代码都是从前往后、从上到下执行的,遇到函数调用就先进入函数内部执行代码,然后再退出函数回到原来的代码段继续往后执行。但是现在我们接触了多线程的并发编程,虽然从宏观上来看,线程是同时在执行的,但其实多线程的执行顺序是不确定的,操作系统调度哪个线程到cpu上执行,就轮到哪个线程执行。每个线程,都是一个独立的执行流,每个线程都可以执行一段代码,多个线程之间是并发的关系。

1.2、sleep()方法

我们可以看到在代码中出现了sleep()方法,来了解以下这个常用方法。

sleep() 方法是 Thread 类的静态方法,线程调用该方法表示进入休眠/阻塞状态,其参数是 休眠的时间,单位是ms。

线程调用sleep()方法后,进入阻塞状态,当阻塞时间到,系统就会唤醒线程,并且恢复对线程的调度。如果是多个线程阻塞后都被唤醒了,那么此时谁先被调度到,谁后被调度,可以视为是”随机“的(随机在日常生活中,我们一般理解为是“概率均等”的情况,但是在这里只是看起来随机,因为我们也不知道操作系统是怎么调度的,我们只能在代码的设定上表示为随机),这样 “随机” 调度的过程,称为 “抢占式执行”

2、自定义一个类,实现Runnable接口,重写run方法

class MyRunnable implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("hello world!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class TestThread2{
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
        t.start();
        while (true) {
            System.out.println("hello main!");
            Thread.sleep(1000);
        }
    }
}

这类创建线程的方法,把线程本身与线程要执行的任务(在 Runnable中)分开了,进一步的解耦合了。当我们还想要并发编程,但是不想使用线程的方式实现并发编程,想使用其他方式时:譬如说线程池、协程…就可以使用Runnable搭配他们来使用,进而实现并发编程。

第一种方法创建线程,是将线程本身与线程所要执行的任务放在了一起,耦合度较高。

3、继承 Thread 类,重写 run 方法,基于匿名内部类

/**
 * 继承自Thread,重写run()方法,基于匿名内部类
 */
public class testThread3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run() {
                while (true){
                    System.out.println("hello world!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

4、实现 Runnable 接口,重写 run 方法,基于匿名内部类

/**
 * 实现 runnable ,重写 run(),基于匿名内部类
 */
public class testThread4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable(){
            public void run(){
                while (true){
                    System.out.println("hello world!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        t.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

5、匿名内部类

上述的几种创建线程的方法,多多少少有些啰嗦、复杂。因此就有了第5种方法:使用匿名内部类。

匿名内部类是一个 lambda 表达式,这个 lambda 表达式里就表示了run()方法里的内容。

lambda 表达式,本质上是一个 匿名函数,这样的匿名函数,主要可以用来作为回调函数来使用。

回调函数:先写好,但不需要程序员手动调用,在合适的时机自动被调用。

5.1、回调函数的使用场景

1、服务器开发:服务器收到一个请求,就会触发一个对应的回调函数,使用回调函数对该请求做出具体的处理。
2、图形界面开发:针对用户的某个操作,触发一个对应的回调。

/**
 * 使用 匿名函数 创建线程
 */
public class testThread5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (true){
                System.out.println("hello world!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();

        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

java单开一个子线程 java创建子线程_线程_03

那么在我们上述的代码中,此回调函数什么时候执行呢??即当线程被真正创建出来时会自动执行。

创建线程的方法还有许多许多,主要是介绍常用的写法,还有的写法后续会继续补充!

6、使用 Callable 接口

Runnable 里的 run() 能表示一个任务,Callable 里的 call() 也能表示一个任务,但是两者不同的是,run() 返回的是 void,call() 返回的是一个具体的值,具体返回什么类型,可以通过Object指定。

在进行多线程操作时,如果是关心多线程的执行过程,使用Runnable即可,因为Runnable无返回值;如果是关心多线程的计算结果,使用Callable更合适。

package thread;

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

public class testCallable{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            //    使用该 call() 方法计算出 1加到10的和。
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i <= 10; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        System.out.println(futureTask.get());
    }
}

在上述代码例子中,使用 Callable 创建线程t来计算1到10的和,在call()方法中进行计算,并且返回计算结果,同时Callable无法直接作为Thread的类,需要new 出 FutureTask实例,将Callable作为FutureTask的实例,同时将FutureTask作为Thread的实例,通过使用FutureTask里的get()方法获取call()中返回的计算结果,get()和join()一样,如果call()没执行完成,get()会阻塞等待至call()方法执行完成。