一篇文章助你搞懂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

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

相关文章
|
10天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
6天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9
|
9天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
6天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
9天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
24 3
|
8天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
9天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
20 1
|
9天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
9天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
37 1
|
13天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####