一篇文章助你搞懂java中的线程概念!

本文章将带你初步了解java语言中的线程基础概念常见用法,帮助你快速入门和学习线程!

进程与线程

我们都知道前端中的javascript是一个单线程的语言,所有代码只能在上一个代码执行完才能继续执行。

简单来说,进程 好比一个工厂线程 是好比工厂 内的一条流水线, 想提高工厂 的效益,我们可以多增加几个流水线(线程)。


java比较强大,它是一个多线程的语言,因此代码效率可以很高。

我们先看一段简单的代码:

java 复制代码
public class Java_Thread {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());      // main
    }
}
  • 这段Java代码的主要作用是打印出当前正在执行的主线程的名称。
  • Thread.currentThread()是一个静态方法,它返回对当前正在执行的线程的引用,即主线程的引用。
  • getName()方法获取这个线程的名称

在这段java程序运行时,默认产生一个进程 ,这个进程 会有一个主线程 (如main),所有代码都在主线程中运行。

线程的创建

创建一个自己的线程非常容易,首先,我们需要定义一个继承Thread的类。然后,重写线程的run方法即可(我们在编译器内点击Ctrl + O ,可以快速重写线程内的run方法)。Thread.currentThread().getName()可以获取线程的名称。

接下来,我们声明自己的线程,并启动线程(执行其start() 方法即可启动)

java 复制代码
public class Java_Thread {
    public static void main(String[] args) {
        // 创建自定义线程对象
        MyThread thread = new MyThread();
        // 执行自定义线程
        thread.start();
        // 主线程
        System.out.println(Thread.currentThread().getName());
    }
}

//声明自定义线程类
class MyThread extends Thread{
    // 重写运行指令
    @Override
    public void run() {
        System.out.println("我的线程"+Thread.currentThread().getName());
    }
}

代码中,thread.start()的执行在System.out.println(Thread.currentThread().getName())代码的运行之前,但通过运行结果,我们会发现"main"先打印,"我的线程Thread-0"被后打印。

这说明了,不同线程之间执行是有先后顺序的,由于主线程从一开始就在,所以在这个示例中主线程最先执行

线程的生命周期

同vue、react框架一样,线程也有生命周期的概念。如图,Java中的线程生命周期主要有六个阶段,分别是:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、等待(Waiting)和终止(Terminated)。

我们以如下代码为例,简述其声明周期过程

java 复制代码
public class Java_Thread {
    public static void main(String[] args) {
        // 创建自定义线程对象
        MyThread thread = new MyThread();
        // 执行自定义线程
        thread.start();

        // 主线程
        System.out.println(Thread.currentThread().getName());
    }
}

//声明自定义线程类
class MyThread extends Thread{
    // 重写运行指令

    @Override
    public void run() {
        System.out.println("我的线程"+Thread.currentThread().getName());
    }
}
  1. 新建状态(New):创建了MyThread的实例,但还未调用start()方法,线程处于新建状态。
  2. 就绪状态(Runnable):调用了thread.start()方法后,线程进入就绪状态,等待CPU调度。
  3. 运行状态(Running):由于该代码示例中没有等待状态,因此一旦线程进入就绪状态并被调度后,线程将直接进入运行状态,执行MyThread的run()方法。
  4. 终止状态(Terminated):当MyThread的run()方法执行完毕后,线程将进入终止状态,表示线程已经执行完毕。

由于上述代码示例中没有显式地让线程进入等待状态,因此它不包含等待状态。如果要让线程进入等待状态,可以在MyThread的run()方法中使用Thread类的wait()方法。

线程的并行与串行

并行

正常情况下,子线程的运行是独立的,互不干扰,谁先抢到cpu资源,谁先执行。

java 复制代码
public class Java_Thread {
    public static void main(String[] args) {
        MyThread1 thread1 = new MyThread1();
        MyThread2 myThread2 = new MyThread2();

        thread1.start();
        myThread2.start();
        
        System.out.println("主线程执行完毕");
    }
}
class MyThread1 extends Thread{
    @Override
    public void run() {
        System.out.println("我的线程1:"+Thread.currentThread().getName());
    }
}
class MyThread2 extends Thread{
    @Override
    public void run() {
        System.out.println("我的线程2:"+Thread.currentThread().getName());
    }
}

也正因如此,"线程1"和"线程2"的执行顺序是不确定的。这种运行方式成为 "并行" 。那么,我们自然而然可以做到让不同线程之间按顺序执行,即 "串行"。

串行

要实现串行非常简单,只需要给线程对象执行join()方法即可。

休眠机制

线程在执行过程中可以进行休眠,等待一定时机后继续执行的能力(可以与其他线程进行数据交互)

arduino 复制代码
public class Java_Thread {
    public static void main(String[] args) throws InterruptedException {
       Thread.sleep(3000);
       System.out.println("main线程执行完毕");
    }
}

如上述代码实现了一个3秒的线程休眠,3s后才会打印"main线程执行完毕"。我们通过休眠实现一个时间打印器:

线程的其他创建写法

之前的demo中,我们定义一个线程都采用了继承Thread类的方法。这种方法写起来较为繁琐,我们看看另外两种方便的创建方法。

Lambda表达式

rust 复制代码
() -> { } 

在这个Lambda表达式中,箭头符号"->"表示Lambda表达式的开始,箭头左侧是要传递的参数列表,箭头右侧是Lambda表达式的主体。

java 复制代码
public class Thread_01 {
    public static void main(String[] args) {
        // 构建线程对象时,可以把逻辑传递给这个对象
        // 传递逻辑 () -> {}
        Thread  T = new Thread(() -> {
            System.out.println("线程T");
        });
        T.start();
        System.out.println("main线程执行完毕");
    }
}

实现Runnable接口

可以将实现了Runnable接口的对象作为参数传递给Thread类的构造函数,然后调用start()方法启动线程。

java 复制代码
public class Thread_01 {
    public static void main(String[] args) {
        // 构建线程对象时,可以传递实现了Runnable接口得类的对象,一般使用匿名类
        Thread  t5 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程");
            }
        });
    }
}

线程池

线程是进程中的实际运作单位,而线程池是使用池化技术管理和使用线程的机制。它的存在价值就是降低资源消耗:

通过重复利用已创建的线程,避免线程的创建和销毁所造成的资源消耗。


javjava中有四种常见得线程池

  • 固定大小的线程池
  • 动态创建的线程池
  • 单一线程池
  • 定时线程池

固定大小的线程池

当有新任务到达时,如果线程池中有空闲线程,则立即执行任务。如果线程池已满,则任务会被放在队列中等待。这种线程池适用于处理大量短期且并发量不大的任务。

我们来看一个图解,如下图,在线程池对象中有三个线程1、2、3。A,B分别提交了一个任务给线程池对象。由于线程1,2,3空闲,因此,线程池对象对象把任务先分给1,2线程处理;此时,1,2线程处于工作状态(红色),3线程处于空闲状态(绿色)。

随后,C提交了一个新任务,由于1,2线程未结束,任务必然由3线程处理

如果此时再来一个D任务,由于1,2,3线程均处于工作状态,此时,任务D只能处于等待状态。

假设线程2的任务比较简单,线程2先处理完任务,此时,D任务就会交给线程2处理。

我们用代码展示上述的过程

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Thread_Pool {
    public static void main(String[] args) {
        //  1.创建固定数量的线程对象
        //  ExecutorService 是线程服务对象
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i= 0 ;i<5;i++){
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
    }
}

那么,代码的运行结果应该只会打印三种线程的名称

观察打印结果,和我们的图解过程是一致的。

动态创建的线程池

这是一个可以缓存空闲线程的线程池,当有新任务到达时,如果线程池中有空闲线程,则立即执行任务。如果线程池中没有空闲线程,则创建新的线程执行任务。这种线程池适用于处理大量短期且并发量较大的任务。

ini 复制代码
ExecutorService executorService = Executors.newCachedThreadPool();

单一线程池

这是一个只有一个线程的线程池,适用于需要顺序执行且无并发需求的任务。这种线程池可以避免多线程竞争和死锁等问题,提高程序的稳定性和性能。

ini 复制代码
ExecutorService executorService = Executors.newSingleThreadExecutor()

定时线程池

这是一个可以定时执行任务的线程池,可以按照指定的时间间隔执行任务。这种线程池适用于需要定时执行的任务,如定时清理缓存、定时发送邮件等。

ini 复制代码
//  定时线程池(ScheduledThreadPool)
ExecutorService executorService = Executors.newScheduledThreadPool(3);

了解即可,我们不用做深入研究!

线程的同步与异步

线程的串行与并行和异步与同步是两个不同的概念。

串行和并行是描述线程的执行方式的:

  • 串行:在程序中,一个线程完成一项任务后,另一个线程才开始执行。这就像一条直线,一个任务完成后,下一个任务才开始。
  • 并行:在程序中,多个线程可以同时执行。这就像多条直线,它们并行前进,没有先后顺序。

而异步与同步是描述线程之间通信方式的:

  • 异步:当一个线程在等待资源时,它不会阻塞,而是继续执行其他任务。当资源可用时,它会通过回调函数获取结果。
  • 同步:当一个线程在等待资源时,它会阻塞,直到资源可用。

当然,作为初学者,我们不用理解的太深,我们简单看一个demo就行!

我们来实现一个火车站买票的程序。首先,我们需要一个售票的窗口的类Ticket,然后我们开三个线程(代表三个人去抢票)

java 复制代码
package thread;

public class Ticket implements Runnable {
    private int num = 10;
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (num <= 0) {
                break;
            }
            System.out.println(Thread.currentThread().getName() + "买到了第" + num + "票");
            num --;
        }
    }
}

class TicketTest{
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        Thread thread1 = new Thread(ticket,"Person1");
        Thread thread2 = new Thread(ticket,"Person2");
        Thread thread3 = new Thread(ticket,"Person3");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

这段代码的主要功能是模拟一个售票系统。在Ticket类中,有一个变量num,表示可售出的票的数量。当num小于或等于0时,售票活动结束。

在TicketTest类中,创建了三个Thread对象,这三个线程都使用同一个Ticket对象作为参数。

由于这三个线程共享同一个Ticket对象(也就是共享同一个num变量),因此在每个线程的run方法中,循环会一直执行直到num小于等于0为止。

这段代码的一个主要问题是,由于线程可能会同时修改num变量,因此可能会出现数据竞争的问题,我们来看一下代码运行结果

问题似乎不言而喻!

为了解决数据竞争的问题,我们可以使用synchronized关键字来确保在同一时刻只有一个线程可以访问共享资源(即num变量)。我们展示一种写法:

讲到这里,相信你对线程的同步异步问题已经有了初步的认识。

相关推荐
2401_854391088 分钟前
Spring Boot大学生就业招聘系统的开发与部署
java·spring boot·后端
虽千万人 吾往矣28 分钟前
golang gorm
开发语言·数据库·后端·tcp/ip·golang
这孩子叫逆1 小时前
Spring Boot项目的创建与使用
java·spring boot·后端
coderWangbuer2 小时前
基于springboot的高校招生系统(含源码+sql+视频导入教程+文档+PPT)
spring boot·后端·sql
攸攸太上2 小时前
JMeter学习
java·后端·学习·jmeter·微服务
Kenny.志2 小时前
2、Spring Boot 3.x 集成 Feign
java·spring boot·后端
sky丶Mamba3 小时前
Spring Boot中获取application.yml中属性的几种方式
java·spring boot·后端
千里码aicood4 小时前
【2025】springboot教学评价管理系统(源码+文档+调试+答疑)
java·spring boot·后端·教学管理系统
程序员-珍4 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
liuxin334455664 小时前
教育技术革新:SpringBoot在线教育系统开发
数据库·spring boot·后端