细说Java多线程系列(1.1)线程初探

it2026-02-21  7

细说Java多线程系列(1.1)线程初探

前言程序、进程、线程与协程(纤程)创建线程的方式实现Runnable接口的方式继承Thread类的方式实现Callable的方式 后记

前言

最近开始写Java多线程方面的东西,大概包括四个部分:线程、锁、容器与线程池,每个部分分成好几篇来写,就不知道以我的速度什么时候能写完了。先订上一个小目标:过年前结束,那么第一篇就在1024当天开始吧。

今天我们就先来了解一下与线程本身有关的东西。

程序、进程、线程与协程(纤程)

这几个概念应该是每个人都遇见过的问题,至于特别官方严谨的概念,网上有很多,说实话看了也记不太住。这里就给出我个人的理解:

首先,程序就是我们写好的代码,它本质上就是一堆二进制的指令和数据,躺在硬盘等着被执行。当我们要执行一个程序的时候,CPU会把它加载到内存中,分配各种资源,然后开始执行这些指令,这时候它就成为了一个进程。一个程序可以被执行很多次,每次对应的进程是不一样的,也可以说,进程是程序的一次执行过程。程序与进程,一个是静态的概念,一个是动态的概念。程序就是一堆死在那的代码,而进程是活的,它有自己的生命周期,各种状态,拥有独立的系统资源。

对于一个进程来说,它可能有不同的事情要做,这些事情交给谁来做呢? 线程才是真正去做事情的人。一个进程内,至少要有一个线程来干活,当然也可以有多个线程。每一个线程都等着CPU的调度,什么时候开始做,做多久,没做完怎么办,都得听CPU安排。所以说,线程是CPU调度的基本单位。

那么,为什么CPU不直接调度进程呢?因为进程是独立占据系统资源(存储空间、I/O资源等)的,切换一次进程开销很大;而同一进程内的多个线程是共享本进程全部资源的,它们自身只占有一点运行时必要的系统资源(程序计数器、一组寄存器和栈),切换线程要节省很多成本。因此对于CPU来说,它调度的是线程,也就意味着在一个进程中,线程是必不可少的。当然,由于线程需要共享进程的资源,其本身只有运行时必要的系统资源,因此线程也不能脱离进程而存在。

我们刚刚所说的线程,是由操作系统内核来实现、管理和调度的,内核可以感知到线程的存在,严格意义上来说,应该称其为内核态线程。而还有一种线程,对于它的管理是由用户来实现的,这种线程的创建、撤销、调度等工作都不依靠系统调用,因此内核是感知不到它的存在的,这种线程被称为用户态线程,也就是协程。协程比我们常说的线程来说,更轻量,因为创建协程的代价比创建线程还要小得多。当然,Java目前还没有支持协程,但是有类库可以使用。

至于纤程呢,可以认为是Windows中的协程。微软为了方便其他公司将代码移植到Windows,对协程这种用户态线程进行了实现,并且提供了一组api可以使用,取名为纤程。纤程与其他语言中的协程可能在实现上会有区别,但是本质上都是用户态线程。

一句话概述:

程序(Program):放在硬盘上的可执行文件

进程(Process):资源分配的基本单位,程序的一次执行过程

线程(Thread):CPU调度的基本单位,进程的组成部分

协程(Coroutine):用户态的轻量级线程

纤程(Fiber):Windows中的协程

参考资料:

基础面试题:程序, 进程,线程,纤程,管程,超线程详解

线程的三种实现方式

【操作系统】线程实现方式(内核级线程、用户级线程)

协程-用户态线程

协程和纤程的区别是什么?

创建线程的方式

说了那么多概念上的东西,那么到底如何创建一个线程呢?

直接进到Thread这个类中,可以看到Thread的构造方法:

事实上,这些方法都调用了同一个构造方法:

ThreadGroup g: 线程所在的线程组 Runnable target: 线程要执行的任务 String name: 线程名字 long stackSize: 栈空间的大小 AccessControlContext acc: 安全控制 boolean inheritThreadLocals: 子线程是否继承父线程的本地变量

对于线程而言,最重要的其实是Runnable这个参数,它代表着线程要执行的任务,毕竟线程就是要来干活的嘛!

Runnable呢,事实上是一个接口,点进源码中看,接口中只有一个run()方法。

也就是说,最重要的就是这个run()方法,我们想要创建一个线程,首先要给它一个任务,那么就得实现这个方法,然后交给线程执行。

实现Runnable接口的方式

要实现这个run()方法,首先可以自己写一个类声明实现Runnable接口,然后在类中重写run()方法。

public class MyRunnable implements Runnable { @Override public void run() { for (int i = 0;i < 10;i ++) { System.out.println("myRunnable:" + Thread.currentThread().getName()); } } }

这个run的任务是:循环十次输出当前线程的名称。

这样,我们得到了一个Runnable接口的实现类,如果需要执行任务,只需要创建一个线程,将这个类传给它即可。

//两个线程执行的是同一个run() MyRunnable myRunnable = new MyRunnable(); Thread thread1 = new Thread(myRunnable); Thread thread2 = new Thread(myRunnable); //两个线程执行的是不同的run() Thread thread3 = new Thread(new MyRunnable()); Thread thread4 = new Thread(new MyRunnable());

当然,因为Runnable接口中只有一个方法,而且很多时候我们给线程的任务也不是固定的,也就是说可能需要多个run()方法,为每一个方法都去新建一个类太麻烦,也不方便查看,因此有更简洁的写法,本质上是使用了匿名内部类。

Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0;i < 10;i ++){ System.out.println("newRun:"+Thread.currentThread().getName()); } } });

在JDK8以后,由于支持了lambda表达式,因此可以写得更加简略

Thread thread = new Thread(()->{ for(int i = 0;i < 10;i ++){ System.out.println("lambda:" + Thread.currentThread().getName()); } }); //等同于: Runnable r = ()->{ System.out.println("lambda:" + Thread.currentThread().getName()); }; Thread thread = new Thread(r);

继承Thread类的方式

可以发现,Thread类也实现了Runnable接口,这意味着我们可以通过继承Thread类的方式,重写它的run()方法。

Thread类中的run()方法:

public void run() { if (this.target != null) { this.target.run(); } }

继承并重写:

public class MyThread extends Thread { @Override public void run() { for (int i = 0;i < 10;i ++){ System.out.println("myThread:" + this.getName()); } } }

这样,我们得到的是一个Thread的子类,它能执行的任务就是我们指定的run()方法。(不需要传Runnable了。)

MyThread myThread = new MyThread();

实现Callable的方式

Callable是一个类似于Runnable的接口,里面也只有一个call方法:

可以发现,call()与run()很类似,不同的是它有返回值,也可以抛出异常。那么可以理解为,call()是一个增强版的run()方法。

为了接收call()方法的返回值,Java设计了一个Future接口来专门做接收。

现在,是不是就可以像使用run()一样,直接实现call()方法交给线程执行,然后使用Future来接收呢?很遗憾,并不行。因为线程它只认run(),不认call()啊。为了解决这个问题,JAVA设计了一个RunnableFuture的接口,同时继承了Runnable与Future,然后提供了一个RunnableFuture的实现类FutureTask。

由于FutureTask是实现了Runnable接口的,因此可以直接交给线程来执行;又由于它也实现了Future接口,因此可以直接接收到call()的返回值,两全其美。有人可能会问:为什么FutureTask没有实现Callable接口,而是将它作为一个成员变量呢?

回想一下我们使用Runnable的方式:

自己写run()方法交给Thread执行。

现在我们使用Callable就稍微麻烦一些,需要经过FutureTask转手:

自己写call()方法把call()方法交给一个FutureTask的实例把这个FutureTask实例交给一个Thread使用这个FutureTask实例获取返回的数据 //1.自己写call方法 Callable<Object> c = new Callable<Object>() { @Override public Object call() throws Exception { int i = 0; for(;i < 10;i++) { System.out.println("callable: " +Thread.currentThread().getName()); } return i; } }; //2.把call()方法交给一个FutureTask的实例 FutureTask<Object> task = new FutureTask<>(c); //3.把这个FutureTask实例交给一个Thread Thread thread5 = new Thread(task); thread5.start(); try { //4.使用这个FutureTask实例获取返回的数据 System.out.println(task.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); }

这样看就很明白了,call()方法和run()方法使用上也是差不多的,只是多了一道转交的手续。很容易想到,FutureTask的run()方法里肯定调用了call()方法。至于为什么不实现Callable接口,当然是因为call()要你自己根据任务去实现啦。

一般来说,大多使用实现Runnable的方式,因为Java中接口是可以实现多个接口的,而继承只能有单继承,实现了Runnable接口的类还可以继承其他类,就等于可以把继承留给最需要的情况。同时,多个线程可以使用同一个Runnable,适合多个线程处理同一份资源的情况。至于Callable,实现起来比Runnable复杂,更适合需要获得返回值的情况。

参考资料:

JAVA多线程的三种创建方式

Callable与Runnable的区别及其在JDK源码中的应用

后记

本来打算一篇把线程相关讲完的,考虑到篇幅的问题,只能先说到这了;剩下的线程状态与切换,以及线程的方法留到下一篇再说吧。

最新回复(0)