一篇文章助你搞懂java中的线程概念!纯干货,快收藏!

简介: 【8月更文挑战第11天】一篇文章助你搞懂java中的线程概念!纯干货,快收藏!

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

进程与线程

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

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

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()可以获取线程的名称。
GIF 2023-11-6 10-14-13.gif
接下来,我们声明自己的线程,并启动线程(执行其start()方法即可启动)

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());
    }
}
             ![image.png](https://cdn.nlark.com/yuque/0/2023/png/21865277/1699237333219-14f5acf0-05aa-462c-a17b-6b7dd38d7dbb.png#averageHue=%23302f2e&clientId=u820503c1-7a78-4&from=paste&height=108&id=u7cbfd118&originHeight=135&originWidth=667&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=12086&status=done&style=none&taskId=uaa936cdd-90b9-4ac5-b40b-9f8706ee3ec&title=&width=533.6)

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

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

线程的生命周期

同vue、react框架一样,线程也有生命周期的概念。如图,Java中的线程生命周期主要有六个阶段,分别是:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、等待(Waiting)和终止(Terminated)。
GIF 2023-11-6 14-01-37.gif
我们以如下代码为例,简述其声明周期过程

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资源,谁先执行。

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());
    }
}

GIF 2023-11-6 14-19-11.gif
也正因如此,“线程1”和“线程2”的执行顺序是不确定的。这种运行方式成为“并行”。那么,我们自然而然可以做到让不同线程之间按顺序执行,即“串行”。

串行

要实现串行非常简单,只需要给线程对象执行join()方法即可。
GIF 2023-11-6 14-34-49.gif

休眠机制

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

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

如上述代码实现了一个3秒的线程休眠,3s后才会打印“main线程执行完毕”。我们通过休眠实现一个时间打印器:
GIF 2023-11-6 14-43-56.gif

线程的其他创建写法

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

Lambda表达式

() -> { }

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

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()方法启动线程。
GIF 2023-11-7 17-12-10.gif

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线程处于空闲状态(绿色)。

image.png
随后,C提交了一个新任务,由于1,2线程未结束,任务必然由3线程处理
image.png
如果此时再来一个D任务,由于1,2,3线程均处于工作状态,此时,任务D只能处于等待状态。
假设线程2的任务比较简单,线程2先处理完任务,此时,D任务就会交给线程2处理。

image.png

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

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());
                }
            });
        }
    }
}

那么,代码的运行结果应该只会打印三种线程的名称
GIF 2023-11-8 18-11-07.gif
观察打印结果,和我们的图解过程是一致的。

动态创建的线程池

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

ExecutorService executorService = Executors.newCachedThreadPool();

GIF 2023-11-8 18-35-25.gif

单一线程池

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

ExecutorService executorService = Executors.newSingleThreadExecutor()

image.png

定时线程池

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

//  定时线程池(ScheduledThreadPool)
ExecutorService executorService = Executors.newScheduledThreadPool(3);

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

线程的同步与异步

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

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

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

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

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

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

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

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变量,因此可能会出现数据竞争的问题,我们来看一下代码运行结果
image.png
问题似乎不言而喻!

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

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

相关文章
|
4天前
|
安全 Java UED
Java中的多线程编程:从基础到实践
本文深入探讨了Java中的多线程编程,包括线程的创建、生命周期管理以及同步机制。通过实例展示了如何使用Thread类和Runnable接口来创建线程,讨论了线程安全问题及解决策略,如使用synchronized关键字和ReentrantLock类。文章还涵盖了线程间通信的方式,包括wait()、notify()和notifyAll()方法,以及如何避免死锁。此外,还介绍了高级并发工具如CountDownLatch和CyclicBarrier的使用方法。通过综合运用这些技术,可以有效提高多线程程序的性能和可靠性。
|
3天前
|
缓存 Java UED
Java中的多线程编程:从基础到实践
【10月更文挑战第13天】 Java作为一门跨平台的编程语言,其强大的多线程能力一直是其核心优势之一。本文将从最基础的概念讲起,逐步深入探讨Java多线程的实现方式及其应用场景,通过实例讲解帮助读者更好地理解和应用这一技术。
19 3
|
8天前
|
Java 调度 UED
深入理解Java中的多线程与并发机制
本文将详细探讨Java中多线程的概念、实现方式及并发机制,包括线程的生命周期、同步与锁机制以及高级并发工具。通过实例代码演示,帮助读者理解如何在Java中有效地处理多线程和并发问题,提高程序的性能和响应能力。
|
5天前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
13 2
|
6天前
|
存储 安全 Java
Java-如何保证线程安全?
【10月更文挑战第10天】
|
7天前
|
Java
|
8天前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
21 1
|
1天前
|
缓存 算法 Java
|
7天前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
19 0
|
10天前
|
Java 程序员 开发者
Java中的多线程基础与实用技巧
【10月更文挑战第7天】本文旨在通过浅显易懂的语言和生动的比喻,向读者展示Java中多线程编程的世界。我们将一起探索创建线程的不同方法,理解线程生命周期的奥秘,并通过一些实用的技巧来避免常见的多线程陷阱。无论你是初学者还是有一定经验的开发者,这篇文章都将为你揭开多线程编程的神秘面纱,让你在并发编程的道路上走得更稳、更远。