万字长文详解Java线程知识

激光SLAM总结——Fast LIO / Fast LIO2 / Faster LIO 激光SLAM总结——Fast LIO / Fast LIO2 / Faster LIO 在之前的工作中有接触过LOAM,最近在工作中又接触到Faster LIO相关的工作,于是想着对Fast LIO / Fast LIO2 / Faster LIO这一系列工作进行一个简单的总结,以加深自己对激光SLAM算法的理解,之前总结过的一些和激光SLAM算法相关的博客还有,感兴趣的同学可以一起学习讨论: 激光SLAM总结——VLOAM / LIMO算法解析 激光SLAM总结——LIO-Mapping / LIOM / 阅读详情

什么是进程、线程、协程?

进程

进程是计算机科学中的一个核心概念,它指的是在操作系统中正在执行的一个程序的实例。进程是操作系统中的一个独立执行单元,具有独立的内存空间和系统资源。每个进程都有自己独立的地址空间和文件描述符,不同进程之间的通信需要使用特定的机制,如管道、套接字等。

进程特征

独立性:每个进程都是独立的,拥有自己的地址空间、内存、文件描述符和其他系统资源。进程之间彼此隔离,互不干扰。
资源分配和调度:进程是操作系统进行资源分配和调度的基本单位。它可以申请和拥有系统资源,如CPU时间、内存空间等。
动态性:进程是动态的概念,它描述了程序在数据集上运行的过程,包括程序的执行、数据的处理和状态的改变等。

进程组成

进程控制块:保存进程运行期间相关的数据,是进程存在的唯一标志。
程序段:能被进程调度程序调度到 CPU 运行的程序的代码段。
数据段:存储程序运行期间的相关数据,可以是原始数据也可以是相关结果。

进程状态

进程在其生命周期中会经历不同的状态,如新建、就绪、运行、等待(阻塞)、终止等。
新建:进程被创建,但尚未开始执行。
就绪:进程已准备好执行,等待被调度器分配CPU。
运行:进程正在CPU上执行。
阻塞(等待):进程因等待某些事件(如I/O操作)而暂停执行。
终止:进程执行完毕或因错误而结束。
 


在 Linux 中可通过 top 和 ps 工具查看进程状态:S 列表示进程的状态,有 R、D、Z、S、I 和 T 、X
R :Running,表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行。D:Disk Sleep,不可中断状态睡眠,表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断。
Z:Zombie,表示僵尸进程,实际上进程已经结束了,但其父进程还没有回收它的资源。
S:Interruptible Sleep,可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。
I:Idle,空闲状态,用在不可中断睡眠的内核线程上。可能实际上没有任何负载。
T:Stopped,表示进程处于暂停或者跟踪状态。
X:Dead,表示进程已经消亡,不会在 top 和 ps 命令中看到。

进程模型

单进程、多进程
传统UNIX网络服务器采用模型有:
PPC(Process Per Connection):指每次有新的连接就创建一个进程去专门处理这个连接。
Prefock :提前创建进程,预先创建好进程才开始接受用户的请求(省去 fork 进程的操作)。

线程

线程是进程内的一个执行流,共享同一进程的内存空间和系统资源。多个线程可以同时执行不同的任务,每个线程有自己的栈和局部变量,但共享进程的全局变量和堆内存。

线程特征

轻量级:线程的创建、销毁和切换的开销比进程小得多。
独立调度:线程可以被操作系统独立调度,但通常线程的调度是由其所属的进程控制的。
共享资源:同一进程内的线程可以共享进程的资源,如内存空间、文件句柄、全局变量等。
并发执行:在多核处理器上,线程可以实现真正的并行执行。
上下文切换快:线程间的上下文切换比进程间的切换要快,因为线程共享了进程的大部分资源。

线程组成

线程ID:每个线程都有一个唯一的标识符,用于区分不同的线程。线程 ID 通常由操作系统分配。
程序计数器:程序计数器存储了线程下一条将要执行的指令的地址。每个线程都有自己的程序计数器,以确保线程可以独立地执行指令。
寄存器集合:线程执行时会使用到一组寄存器,包括数据寄存器、地址寄存器等。这些寄存器用于存储临时数据和指令。每个线程都有自己的寄存器集合,以保证线程执行的独立性。
栈(Stack):线程拥有自己的栈,用于存储局部变量、方法参数、返回地址等。栈是线程执行时的内存管理结构,支持线程的函数调用和返回。

线程状态

新建状态:线程被创建,但还没有开始执行。在这个阶段,线程可能正在初始化其运行所需的资源。
就绪状态:线程已经准备好运行,它已经获得了除CPU之外的所有必要资源。线程处于就绪状态时,它将被放入就绪队列中,等待调度器的调度。
运行状态:线程正在CPU上执行。在多线程环境中,由于CPU时间的分配,一个线程可能在运行状态和其他状态之间多次切换。
阻塞状态:线程因为等待某些资源(如输入/输出操作、文件读写、网络通信等)而暂停执行。在阻塞状态,线程不会消耗CPU资源,直到它等待的事件发生。
等待状态:线程通过调用如wait()、LockSupport.park()等方法进入等待状态,主动放弃CPU资源,直到其他线程调用notify()、notifyAll()或LockSupport.unpark(Thread) 来唤醒它。
超时等待状态:与等待状态类似,但线程在指定的超时时间后会自动唤醒。这通常通过wait(long timeout)、sleep(long millis)或LockSupport.parkNanos()、LockSupport.parkUntil()等方法实现。
终止状态:线程的执行已经完全结束,可能是因为它执行到了逻辑的终点,或者因为其他线程通过调用stop()方法(虽然不推荐使用,因为它是不安全的)强制终止了线程。

多线程模型

常见服务器高性能的多线程模式:
TPC:每次有新连接就创建新线程。
Prethread:提前创建好线程,例如线程池,每次有新连接就从线程池里拿取。

协程

协程(Coroutine)是一种程序组件,它允许挂起和恢复执行,与线程相比,协程是更轻量级的并发单元。协程通常在用户态管理,而不是由操作系统内核管理,这意味着它们的创建、切换和管理的开销远小于线程。协程主要用于提高程序的并发性能,尤其是在I/O密集型和高级别的结构化异步编程中。

协程特征

轻量级:协程的创建和切换开销小,因为它们通常不需要操作系统内核的介入。
非抢占式:协程的执行由程序逻辑控制,一个协程可以选择主动挂起,让出控制权给另一个协程。
用户态管理:协程的调度通常由用户程序控制,而不是由操作系统内核管理。
协作式多任务:协程之间是协作的,它们需要彼此协作来实现多任务并发。
适用于I/O密集型任务:协程非常适合处理I/O密集型任务,因为它们可以在等待I/O操作时挂起,让出CPU给其他协程使用。

协程组成

协程控制块:类似于线程的线程控制块(Thread Control Block, TCB),CCB 包含了协程的元数据,如协程的状态、寄存器状态、调用栈信息等。CCB 通常由协程库或运行时环境管理。
程序计数器:用于存储协程执行时下一条指令的地址。当协程被挂起时,程序计数器记录了协程的执行状态,以便之后能够恢复执行。
寄存器集合:协程执行时使用的寄存器,包括局部变量、函数参数等。协程挂起时,需要保存其寄存器状态,以便恢复时能够继续执行。
调用栈:协程的调用栈用于存储函数调用的上下文,包括局部变量、返回地址等。协程的调用栈通常比线程的调用栈小,因为协程通常用于处理较短的任务。

协程状态

初始化(Initialized):协程已经被创建,但还没有开始执行。这个阶段通常涉及到协程控制块的设置和必要的初始化操作。
就绪(Ready):协程已经准备好执行,正在等待调度器分配执行的机会。在这个阶段,协程可能被放置在就绪队列中。
运行(Running):协程正在执行。在单线程的协程调度器中,一次只有一个协程处于运行状态;在多线程环境中,协程可能会被映射到线程上并真正地并行运行。
挂起(Suspended):协程在执行过程中主动或被动地暂停执行。挂起状态通常发生在协程等待某个事件(如I/O操作、锁的获取等)完成时。挂起是协程的一个关键特性,它允许协程在不执行时释放CPU资源。
恢复(Resumed):挂起的协程在等待的事件完成后被重新调度执行。在这个阶段,协程从挂起状态恢复到就绪或运行状态。
完成(Completed):协程已经完成了它的执行逻辑,不会再被调度。完成状态标志着协程生命周期的结束。
取消(Cancelled):协程在执行过程中被外部请求终止。这可能是由于错误处理、超时或其他逻辑要求。
错误(Error):协程在执行过程中遇到了错误,导致它不能继续执行。错误状态可能需要特别的处理逻辑,比如错误恢复或资源清理。
阻塞(Blocked):协程正在等待一个资源或事件,但它不能主动挂起自己。阻塞状态与挂起状态不同,因为协程在阻塞状态下通常不能被调度器挂起。

进程、线程、协程 区别?

比较项进程线程协程
资源分配与独立性进程是资源分配的基本单位,每个进程都有独立的内存空间和系统资源。进程间通信需要通过特定的机制,如管道、套接字等。线程是CPU调度的基本单位,它共享所属进程的内存空间和系统资源。线程间通信主要通过共享内存来实现,开销较小但可能面临数据一致性和同步问题。协程不是由操作系统内核管理的,而是由用户程序控制。协程拥有自己的寄存器上下文和栈,但切换开销非常小,因为它不涉及内核态的切换。
并发与并行进程之间的并发执行是通过操作系统的时间片轮转机制来实现的,多个进程可以在不同的时间片内交替执行。但由于进程切换的开销较大,因此进程级的并发效率相对较低。线程之间的并发执行是在同一进程内进行的,多个线程可以共享进程的内存空间和系统资源,因此线程级的并发效率较高。但由于线程间共享资源,因此需要处理好同步和互斥问题。协程的并发执行是在用户程序中实现的,通过协程的调度器来管理多个协程的执行。协程之间的切换开销非常小,因此可以实现高效的并发执行。但协程的并发执行仍然是基于单线程的,因此无法充分利用多核CPU的并行处理能力。
调度与切换进程的调度和切换是由操作系统内核完成的,包括进程的创建、销毁、状态转换和上下文切换等。进程切换的开销较大,因为它需要保存和恢复进程的上下文信息。线程的调度和切换也是由操作系统内核完成的,但线程切换的开销相对较小,因为线程共享进程的内存空间和部分系统资源。线程切换主要涉及内核栈和硬件上下文的保存和恢复。协程的调度和切换是由用户程序控制的,不涉及操作系统内核的切换。协程切换的开销非常小,因为它只需要保存和恢复协程的寄存器上下文和栈信息。
同步与互斥进程间通信需要通过特定的机制来实现,如管道、套接字等。进程间的同步和互斥问题需要通过操作系统提供的同步原语(如信号量、互斥锁等)来解决。线程间通信主要通过共享内存来实现,但这也带来了同步和互斥问题。线程间的同步和互斥可以通过互斥锁、条件变量等同步原语来解决。协程之间的同步和互斥问题相对简单,因为协程的调度和切换是由用户程序控制的。协程可以通过协程库提供的同步机制(如协程锁、协程事件等)来解决同步和互斥问题。

Java协程(JDK21虚拟线程)

当前 Java 中的多线程并发编程绝对是另我们都非常头疼的一部分,感觉就是学起来难啃,用起来难用。但是转头看看使用其他语言的朋友们,根本就没有这个烦恼嘛,比如 GoLang,感觉人家用起来就很丝滑呢。
JDK21 中就在这方面做了很大的改进,让Java并发编程变得更简单一点,更丝滑一点。确切的说,在 JDK19或JDK20中就有这些改进了。

虚拟线程(Virtual Threads)

虚拟线程是基于协程的线程,它们与其他语言中的协程具有相似之处,但也存在一些不同之处。
虚拟线程是依附于主线程的,如果主线程销毁了,那虚拟线程也不复存在。
相同之处:
虚拟线程和协程都是轻量级的线程,它们的创建和销毁的开销都比传统的操作系统线程要小。
虚拟线程和协程都可以通过暂停和恢复来实现线程之间的切换,从而避免了线程上下文切换的开销。
虚拟线程和协程都可以使用异步和非阻塞的方式来处理任务,提高应用程序的性能和响应速度。
不同之处:
虚拟线程是在 JVM 层面实现的,而协程则是在语言层面实现的。因此,虚拟线程的实现可以与任何支持 JVM 的语言一起使用,而协程的实现则需要特定的编程语言支持。
虚拟线程是一种基于线程的协程实现,因此它们可以使用线程相关的 API,如 ThreadLocal、Lock 和 Semaphore。而协程则不依赖于线程,通常需要使用特定的异步编程框架和 API。
虚拟线程的调度是由 JVM 管理的,而协程的调度是由编程语言或异步编程框架管理的。因此,虚拟线程可以更好地与其他线程进行协作,而协程则更适合处理异步任务。

虚拟线程入门

先声明一个线程类,implements 自 Runnable,并实现 run方法。

public class TestRunner implements Runnable{
    @Override    public void run() {        System.out.println("当前线程名称:" + Thread.currentThread().getName());        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            throw new RuntimeException(e);        }    }}

Java中的new Thread()获取到的即对应操作系统中的线程。不过在JDK21中,给了他更明确的概念,平台线程PlatformThread。虚拟线程则命名VirtualThread。

//线程,即平台线程。两种方式//Thread platformThread = new Thread(new TestRunner());Thread platformThread = Thread.ofPlatform().name("platformThread").start(new TestRunner());//虚拟线程。跟一下源码,可知他是依赖于池化的ForkJoinPool的Thread virtualThread = Thread.ofVirtual().name("virtualThread").start(new TestRunner());

性能比较

import java.text.SimpleDateFormat;import java.time.Duration;import java.util.Date;import java.util.concurrent.CompletableFuture;import java.util.concurrent.CountDownLatch;
public class Main {

    /**     * 基于15个线程池实现的虚拟线程     * 执行一万个任务,每个任务耗时1000毫秒,总共耗费2637毫秒     */    public static void virtualThread(int count) throws Exception {        StopWatcher stopWatcher = new StopWatcher();        stopWatcher.start();        CountDownLatch countDownLatch = new CountDownLatch(count);        for (int i = 0; i < count; i++) {            Thread.ofVirtual().start(new TestRunner(countDownLatch));        }        countDownLatch.await();        stopWatcher.stop();        System.out.printf("本次执行耗时:%s毫秒", stopWatcher.getTimeInterval().toMillis());    }
    /**     * 基于15个池化线程     * 执行一万个任务,每个任务耗时1000毫秒,总共耗费11分钟     */    public static void platformThread(int count) throws Exception {        StopWatcher stopWatcher = new StopWatcher();        stopWatcher.start();        CountDownLatch countDownLatch = new CountDownLatch(count);        for (int i = 0; i < count; i++) {            CompletableFuture.runAsync(new TestRunner(countDownLatch));        }        countDownLatch.await();        stopWatcher.stop();
        System.out.printf("本次执行耗时:%s毫秒", stopWatcher.getTimeInterval().toMillis());    }
    public static void main(String[] args) throws Exception {        int count = 10000;        //virtualThread(count);        platformThread(count);
    }

    public static class TestRunner implements Runnable {
        private final CountDownLatch countDownLatch;
        public TestRunner(CountDownLatch countDownLatch) {            this.countDownLatch = countDownLatch;        }
        @Override        public void run() {            try {                System.out.println(Thread.currentThread() + " start " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));                Thread.sleep(1000);                System.out.println(Thread.currentThread() + " stop " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));                countDownLatch.countDown();            } catch (Exception e) {                System.out.println(e.getMessage());            }        }    }

    public static class StopWatcher {        private long start;
        private long stop;
        public StopWatcher() {        }
        public void start() {            this.start = System.currentTimeMillis();        }
        public void stop() {            this.stop = System.currentTimeMillis();        }
        public Duration getTimeInterval() {            return Duration.ofMillis(this.stop - this.start);        }

    }}

两种方式,分别模拟处理10000个阻塞任务,每个任务阻塞1秒。
PlatformThread: 15个池化线程
VirtualThread: 15个池化线程,但是采用了虚拟线程方式
我的硬件情况就不详细描述了,直接对比结果,就能清楚明了感受到差异。
执行结果

线程类型耗时对比CPU使用率对比
PlatformThread11分钟10%左右
VirtualThread2秒50%左右

分别有多少种实现线程的方法?

在并发编程的广阔领域中,线程的实现构成了其最坚实的基石。唯有掌握了多线程的构建,我们方能顺利踏上后续复杂并发操作的探索之旅。因此,本教程的起点聚焦于并发编程的核心——如何实现线程,旨在为你打下坚实的基础。尽管表面看来,线程实现可能显得直接且基础,但其背后却蕴含着丰富的细节与深刻的原理。关于线程的实现方式,通常的观点认为存在多种途径,如二种、三种乃至四种之多。

实现 Runnable 接口

第一种实现多线程的方式是通过实现Runnable接口。这种方法的核心在于创建一个类,该类实现Runnable接口,并在这个类中重写run()方法。run()方法包含了线程需要执行的代码。随后,你不需要直接继承Thread类来创建线程,而是可以创建一个Thread类的实例,并将实现了Runnable接口的类的实例作为构造函数的参数传递给这个Thread实例。这样,当Thread实例被启动时,它会调用传递给它的Runnable实现类的run()方法,从而实现了多线程的执行。
以下是一个简单的示例代码,展示了如何通过实现Runnable接口来创建并启动一个线程:

// 首先,定义一个实现了Runnable接口的类  class RunnableThread implements Runnable {      @Override      public void run() {          // 这里编写线程需要执行的代码          System.out.println("线程运行中...");      }  }    public class Main {      public static void main(String[] args) {          // 创建RunnableThread类的实例          RunnableThread runnableThread = new RunnableThread();          // 将Runnable实现类的实例作为参数传递给Thread类的构造函数          Thread thread = new Thread(runnableThread);          // 启动线程          thread.start();          // main线程继续执行其他任务          System.out.println("主线程继续执行...");      }  }

继承 Thread 类

第二种实现多线程的方式是通过继承Thread类。与第一种方式(实现Runnable接口)不同,这里不是通过实现一个接口来定义线程的行为,而是直接继承Thread类,并重写其run()方法。在run()方法中,我们编写线程需要执行的代码。之后,创建该继承类的实例,并调用其start()方法来启动线程,这会导致run()方法在新线程中执行。
以下是一个简单的示例代码,展示了如何通过继承Thread类来创建并启动一个线程:

// 继承Thread类,并重写run()方法  class MyThread extends Thread {      @Override      public void run() {          // 这里编写线程需要执行的代码          System.out.println("通过继承Thread类实现的线程运行中...");      }  }    public class Main {      public static void main(String[] args) {          // 创建MyThread类的实例          MyThread myThread = new MyThread();          // 启动线程          myThread.start();          // main线程继续执行其他任务          System.out.println("主线程继续执行...");      }  }

线程池创建线程

第3种实现多线程的方式是通过使用线程池(ThreadPool)。线程池是一种基于池化技术的多线程管理机制,它预先创建并管理一组线程,当需要执行新的任务时,不是直接创建新线程,而是将任务提交给线程池中的空闲线程执行。这种方式可以有效地减少线程创建和销毁的开销,提高系统的响应速度和吞吐量。

import java.util.concurrent.ExecutorService;  import java.util.concurrent.Executors;  import java.util.concurrent.TimeUnit;    // 一个简单的任务类,实现了Runnable接口  class MyTask implements Runnable {      private int taskId;        public MyTask(int taskId) {          this.taskId = taskId;      }        @Override      public void run() {          // 模拟任务执行          System.out.println("任务 " + taskId + " 正在执行...");          try {              // 假设任务执行需要一些时间              TimeUnit.SECONDS.sleep(1);          } catch (InterruptedException e) {              Thread.currentThread().interrupt(); // 保持中断状态              System.out.println("任务 " + taskId + " 被中断");          }          System.out.println("任务 " + taskId + " 执行完成");      }  }    public class ThreadPoolDemo {      public static void main(String[] args) {          // 创建一个固定大小的线程池,这里设置为3个线程          ExecutorService executorService = Executors.newFixedThreadPool(3);          // 提交任务给线程池执行          for (int i = 0; i < 10; i++) {              int taskId = i + 1;              executorService.submit(new MyTask(taskId));          }          // 关闭线程池,但尝试完成已提交的任务          // 注意:这里使用shutdown()而不是shutdownNow(),因为shutdownNow()会尝试停止正在执行的任务          executorService.shutdown();            // 等待所有任务完成,或者等待超时时间后继续执行          // 这里我们简单地使用awaitTermination来等待,没有设置超时时间          try {              if (!executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)) {                  // 如果等待超时,这里可以处理超时的情况,但在这个例子中我们不会进入这个分支                  System.out.println("线程池任务未能在指定时间内完成");              }          } catch (InterruptedException e) {              // 当前线程在等待过程中被中断              executorService.shutdownNow(); // 停止所有正在执行的任务              // 注意:这里应该处理中断异常,比如重新设置中断状态等              Thread.currentThread().interrupt();              System.out.println("线程池等待过程中被中断");          }          System.out.println("所有任务执行完成,线程池已关闭");      }  }

有返回值的 Callable 创建线程

第4种线程创建方式利用了Callable接口,与Runnable接口不同,Callable接口允许线程执行完毕后返回一个结果。当与ExecutorService的submit方法结合使用时,可以提交一个实现了Callable接口的任务,并立即获得一个Future对象,该对象代表了异步计算的结果。Future对象提供了方法来检查计算是否完成、等待计算完成以及检索计算结果。

为了具体实现这一方式,我们通常会定义一个类来实现Callable接口,并指定一个泛型作为返回值的类型,比如Integer。在实现的call方法中,我们编写线程的执行逻辑,并在执行完毕后返回一个结果。这个结果被封装在Future对象中,可以通过调用Future的get方法来获取,但需要注意的是,get方法是阻塞的,它会等待直到计算结果准备好。

以下是一个简化的示例代码,展示了如何使用Callable接口、Future以及ExecutorService来创建一个线程,该线程执行完毕后返回一个随机数:

import java.util.concurrent.Callable;  import java.util.concurrent.ExecutionException;  import java.util.concurrent.ExecutorService;  import java.util.concurrent.Executors;  import java.util.concurrent.Future;    // 实现Callable接口,指定返回类型为Integer  class RandomNumberCallable implements Callable<Integer> {      @Override      public Integer call() throws Exception {          // 模拟耗时操作,然后返回一个随机数          Thread.sleep(1000); // 休眠1秒模拟耗时操作          return (int) (Math.random() * 100); // 返回一个0到99之间的随机数      }  }  public class CallableThreadDemo {      public static void main(String[] args) {          // 创建一个ExecutorService          ExecutorService executorService = Executors.newSingleThreadExecutor();          // 提交Callable任务给ExecutorService,并立即获得Future对象          Future<Integer> future = executorService.submit(new RandomNumberCallable());          try {              // 等待任务完成,并获取结果              Integer result = future.get(); // 这会阻塞,直到任务完成              System.out.println("生成的随机数是: " + result);          } catch (InterruptedException | ExecutionException e) {              // 处理异常情况              e.printStackTrace();          }          // 关闭ExecutorService          executorService.shutdown();      }  }

为何说只有一种实现线程的方法?

在探讨Java中创建线程的方式时,确实存在两种基本方法,尽管从更广泛的角度来看,如线程池、定时器(Timer)等可以视为这些基本方法的封装或扩展。这里我们深入解析这两种基本方法,并阐述它们如何在本质上统一,同时探讨运行内容的来源。
本质上的统一性
在Java中,创建线程的基本方式可以归结为两种:
实现Runnable接口:通过实现Runnable接口的run方法,并将该实现类的实例传递给Thread类的构造函数,从而创建一个线程。这种方式允许你的类继承其他类(Java不支持多重继承),同时实现线程的执行逻辑。
继承Thread类:通过直接继承Thread类并重写其run方法,可以直接创建线程。这种方式简单直接,但限制了类的继承体系,因为Java不允许多重继承。
尽管这两种方式在表面上有所不同,但它们在本质上是统一的。这是因为无论是哪种方式,最终都是调用了Thread类的start方法来启动线程,而start方法最终都会调用到Thread类或其子类的run方法。Runnable接口的实现方式,其run方法的执行是通过将Runnable实例作为target传递给Thread类的构造函数,并在Thread类的run方法中调用target.run()来实现的。

实现 Runnable 接口比继承 Thread 类实现线程要好吗?

在探讨Java中创建线程的方式时,实现Runnable接口相较于直接继承Thread类,确实展现出了一些显著的优势。下面我们将详细对比这两种方式,并阐述为何实现Runnable接口通常被认为是更好的选择。

  1. 代码架构的清晰与解耦
    首先,从代码架构的角度来看,Runnable接口仅定义了一个run()方法,它专注于线程需要执行的任务内容。通过将任务逻辑与线程管理(如线程的启动、属性设置等)分离,实现了更好的解耦。这种分离使得代码更加清晰,易于理解和维护。相比之下,继承Thread类的方式将任务逻辑与线程管理紧密绑定在一起,降低了代码的模块性和可重用性。

  2. 性能优化与资源利用
    其次,在性能方面,实现Runnable接口的方式通常更加高效。当使用继承Thread类的方式时,每次需要执行新任务时,都需要创建新的Thread实例,这涉及到线程的创建和销毁过程,这些操作相对昂贵。如果任务执行时间很短,那么线程创建和销毁的开销可能会超过任务本身执行的时间,导致资源浪费。而通过实现Runnable接口,我们可以将任务提交给线程池(ExecutorService),由线程池中的固定线程来执行这些任务,从而避免了频繁的线程创建和销毁,提高了性能并优化了资源利用。

  3. 更好的扩展性和灵活性
    第三,Java语言不支持多重继承,这意味着如果一个类继承了Thread类,那么它就不能再继承其他类了。这限制了类的扩展性和灵活性。在实际开发中,我们可能需要让一个类继承自某个特定的基类(比如某个框架提供的类),同时又想让这个类具备执行线程任务的能力。在这种情况下,实现Runnable接口就成为了更好的选择,因为它不会限制类的继承体系。

  4. 易于与Java并发工具集成
    最后,实现Runnable接口的方式更容易与Java并发工具包(如java.util.concurrent)中的其他组件集成。例如,ExecutorService、Future、Callable等高级并发工具都是基于Runnable接口设计的。这使得我们可以更方便地利用这些工具来管理线程、处理并发任务,并享受它们带来的便利和性能提升。

线程状态是如何转换的?

线程6种状态

新建(New):线程对象已被创建,但还没有调用start()方法。
可运行(Runnable):线程已经调用了start()方法,它就进入了可运行状态。在这种状态下,线程可能正在运行,也可能正在等待CPU时间片,与其他线程共享CPU资源。
阻塞(Blocked):线程因为等待监视器锁(也就是等待进入同步块/方法)而进入阻塞状态。当线程试图进入一个已经被其他线程持有的对象的同步区域时,它将被阻塞。
等待(Waiting):线程通过调用wait()、join()或者LockSupport.park()方法进入等待状态。在这种状态下,线程不会被分配CPU执行时间,它们需要被其他线程唤醒。
超时等待(Timed Waiting):线程通过调用带有超时参数的sleep(long millis)、wait(long timeout)、join(long millis)、LockSupport.parkNanos() 或者LockSupport.parkUntil()方法进入超时等待状态。与等待状态类似,不过在指定的等待时间过后,线程会自动唤醒。
终止(Terminated):线程的运行结束。这可能是因为线程正常完成任务,或者因为某个未捕获的异常导致线程结束。

为什么wait和notify必须放在synchronized中?

在多线程编程的上下文中,wait方法被设计用来暂停当前线程的执行,直到另一个线程在同一对象上调用notify或notifyAll方法。这种机制是实现线程间通信和同步的关键手段之一。值得注意的是,wait、notify和notifyAll方法在Java中有着特定的使用规则,它们必须被包裹在同步代码块(synchronized block)或同步方法(synchronized method)中。

wait/notify的使用

wait/notify的使用demo

Object lock = new Object();new Thread(() -> {    synchronized (lock) {        try {            System.out.println("wait before");            // 调用 wait 方法            lock.wait();            System.out.println("wait after");        } catch (InterruptedException e) {            e.printStackTrace();        }    }}).start();
Thread.sleep(100);synchronized (lock) {    System.out.println("execute notify");    // 调用 notify 方法    lock.notify();}

为什么wait/notify/notifyAll必须在synchronized保护的同步代码中使用?

我们先不使用synchronized使用wait/notify。

private static void Test()throws InterruptedException {Object lock = new Object();new Thread(()->{    try {        System.out.println("wait before");        System.out.println("wait after");    }catch(InterruptedException e){        e.printStackTrace();                                                               }).start();Thread.sleep( millis:100);System.out.println("执行 notify");lock.notify();}

无论是wait还是notify,如果不配合synchronized一起使用,在程序运行时就会报IllegalMonitorStateException非法的监视器状态异常,而且notify也不能实现程序的唤醒功能了。
原因:JVM在运行时会强制检查wait和notify有没有在synchronized代码中,如果没有的话就会报非法监视器状态异常(IllegalMonitorStateException),但这也仅仅是运行时的程序表象,那为什么Java要这样设计呢?其实这样设计的原因就是为了防止多线程并发运行时,程序的执行混乱问题。

为什么wait/notify/notifyAll被定义在Object类中,sleep定义在Thread类中?

因为 Java 中每个对象都有一把称之为monitor监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll也都是锁级别的操作,它们的锁属于对象,所以把它们定义在Object类中是最合适,因为Object类是所有对象的父类。

因为如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。

wait/notify 和 sleep 方法的异同?

相同点:

  • 它们都可以让线程阻塞。

  • 它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。

不同点:

  • wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。

  • 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。

  • sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。

  • wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。

什么是线程安全?

线程安全是多线程编程中的一个重要概念,它关乎到多线程环境下对共享资源的访问和管理。具体来说,线程安全指的是在多线程环境中,当多个线程同时访问某个共享资源或数据时,不会出现数据污染、数据竞争、死锁或不可预期的结果,从而保证程序的正确性和稳定性。

线程安全的定义

线程安全的代码会通过同步机制或其他技术手段,确保多个线程在访问共享资源时能够正确地协调执行,不会因为线程的并发执行而导致数据的不一致或程序的错误行为。如果一个类或方法在多线程环境下,其执行结果不受线程调度和交替执行的影响,那么这个类或方法就是线程安全的。

线程安全的重要性

在多线程编程中,线程安全是至关重要的。由于多个线程可能同时访问和修改同一个资源,如果没有适当的同步机制来保护这些资源,就可能导致数据不一致、死锁等问题,进而影响程序的正确性和稳定性。因此,在多线程环境下进行编程时,必须考虑线程安全的问题,并采取相应的措施来确保程序的线程安全。

线程安全的实现方式

线程安全的实现方式多种多样,常见的包括以下几种:

  1. 同步机制:使用synchronized关键字、Lock接口等同步机制来确保线程在访问共享资源时的互斥性,即同一时刻只有一个线程能够访问该资源。

  2. 使用线程安全的数据结构:如ConcurrentHashMap、Atomic类等,这些数据结构内部已经实现了必要的同步机制,可以直接在多线程环境下安全使用。

  3. 避免共享状态:尽量设计无状态的类或方法,减少线程间的依赖和共享资源的需求,从而降低线程安全问题的复杂性。

  4. 使用ThreadLocal:为每个线程提供独立的变量副本,从而避免线程间的数据共享和冲突。

线程安全的判定

判断一个类或方法是否线程安全,通常需要分析其在多线程环境下的行为。如果一个类或方法在多线程环境中能够保持正确的状态和行为,且不会出现数据竞争、死锁等问题,那么它就是线程安全的。需要注意的是,线程安全并不是绝对的,它依赖于具体的实现和使用方式。因此,在设计和使用多线程程序时,必须仔细考虑线程安全的问题,并采取相应的措施来确保程序的正确性和稳定性。

三种典型的线程安全问题?

并发执行导致程序的行为不符合预期?

由于并发执行导致程序的行为不符合预期。这类问题往往与线程之间的交互有关,尤其是在处理共享资源时。以下是几种可能导致“运行结果错误”的典型线程安全问题:

  1. 并发修改异常(Concurrent Modification Exception):
    当一个线程正在遍历集合(如ArrayList),而另一个线程正在修改该集合时,可能会抛出ConcurrentModificationException。为了解决这个问题,可以使用Iterator的remove()方法安全地删除元素,或者使用CopyOnWriteArrayList这样的线程安全集合。

  2. 竞态条件(Race Condition):
    竞态条件发生在多个线程对同一共享变量进行读写操作时,如果没有正确的同步措施,可能会导致程序行为不符合预期。例如,两个线程同时对一个计数器进行递增操作,如果没有同步控制,最终的结果可能比预期的小。

  3. 虚假唤醒(Spurious Wakeup):
    Java的wait()和notify()方法用于线程间通信时,可能会遇到虚假唤醒的问题。虚假唤醒指的是某个线程被唤醒,但实际上它不应该被唤醒,因为相应的条件还没有满足。这种情况虽然不会直接导致运行错误,但会导致程序逻辑错误,例如线程提前退出等待状态,而实际上资源还未准备好。
    解决这些问题的方法:

  4. 使用同步机制:通过synchronized关键字或ReentrantLock类来确保只有一个线程能执行某个代码段。

  5. 使用原子变量:如AtomicInteger,这些类提供了原子操作,可以保证操作的不可分割性。

  6. 使用线程局部变量:通过ThreadLocal类来确保每个线程都有自己的变量副本,从而避免共享数据。

  7. 使用不可变对象:不可变对象自然就是线程安全的,因为它们的内部状态在创建后不能被改变。

  8. 使用线程安全的集合类:如ConcurrentHashMap,这些集合类内部实现了线程安全的机制。

发布和初始化导致线程安全问题?

在Java中,由于对象的发布(publication)和初始化(initialization)不当,可能会导致线程安全问题。这些问题主要出现在对象的创建过程以及多线程环境下的对象访问过程中。以下是几种典型的由发布和初始化导致的线程安全问题:

  1. 对象创建过程中的未完全初始化问题(Partially Initialized Objects):
    在Java中,当一个对象实例化后,它的构造函数执行完毕之前,对象的状态可能是部分初始化的。如果在这个过程中,其他线程能够访问到这个部分初始化的对象,那么就可能出现未定义的行为。这是因为构造函数内的代码可能还没有完成必要的初始化工作,而其他线程就已经开始使用这个对象。解决方案:确保对象完全初始化之后再对外发布。可以使用构造函数内部的synchronized代码块或者构造函数本身的同步来防止其他线程访问尚未完全初始化的对象。另外,可以使用final字段来确保字段在构造函数中一旦被初始化就不能再改变,从而增强对象的不变性。

  2. 对象引用的过早发布(Premature Publication of Object Reference :
    如果一个对象在构造期间其引用被暴露给了其他线程,那么在构造完成后,其他线程可能仍然持有指向旧状态的引用。这意味着其他线程可能会看到对象的一个不一致状态。解决方案:确保对象的引用只在其构造完成后才被发布给其他线程。可以通过构造函数私有化和提供工厂方法或构建者模式来控制对象的创建过程,确保对象完全初始化后再发布。

  3. 对象的引用传递导致的线程安全问题:
    当对象作为参数传递给其他方法时,如果该方法不在一个同步的上下文中执行,并且该对象包含可变状态,那么可能会引发线程安全问题。这是因为对象的引用可以在不同的线程之间共享,导致多个线程同时修改对象的状态。解决方案:对于需要在线程间共享的对象,应该确保它们是不可变的(immutable),或者在访问这些对象时使用同步机制。如果对象必须是可变的,那么在多线程环境中访问这些对象时应该使用synchronized关键字或其他同步工具类来保护对象的状态。
    为了安全地发布对象,可以采取以下措施:

  4. 使用同步机制:确保对象在发布之前已经完全初始化。例如,可以使用synchronized块或方法来保护对象的初始化代码。

  5. 使用volatile关键字:确保对象引用的写入对其他线程立即可见。volatile可以防止指令重排,确保在构造函数执行完毕后,对象的引用不会在初始化之前被其他线程看到。

  6. 使用final关键字:如果对象的字段被声明为final,那么在构造函数中对这些字段的赋值将会在构造函数结束后对所有线程可见。

  7. 延迟发布:直到对象完全初始化后再发布给其他线程。例如,可以使用工厂方法来创建并初始化对象,然后返回一个已经初始化好的对象引用。
    在Java多线程编程中,死锁、活锁和饥饿是常见的线程安全问题。下面分别解释这三个概念及其解决办法:

死锁、活锁和饥饿问题

死锁(Deadlock)

死锁是指两个或多个线程相互等待对方持有的锁,从而造成所有线程都处于无限等待的状态。最常见的死锁形式是一个线程持有一个锁,然后试图获取第二个锁,而另一个线程已经持有第二个锁并且试图获取第一个锁。
假设有两个线程 A 和 B,两个锁 X 和 Y。线程 A 持有锁 X 并尝试获得锁 Y,而线程 B 持有锁 Y 并尝试获得锁 X。这时就会发生死锁。

解决方法:
  • 按顺序加锁:确保所有线程按照相同的顺序获取锁,可以避免循环等待。

  • 超时机制:使用带超时的锁获取方法(如 tryLock),并在超时后释放已持有的锁。

  • 检测死锁:实现死锁检测算法,但这种方法通常比较复杂且可能影响性能。

活锁(Livelock)

活锁是指两个或多个线程在不断重复相同的动作,而无法继续向前推进,尽管每个线程都在不断地做功。活锁不同于死锁的地方在于,活锁中的线程并没有处于等待状态,而是不停地重复执行某些操作。
两个线程 A 和 B 都在尝试进入一个资源,但是每次当 A 进入时,B 就会退出,然后 B 再次尝试进入,A 又退出。如此循环往复,没有任何一方能够成功地独占资源。

解决方法:
  • 随机延迟:增加随机延迟,使得线程不太可能在同一时刻尝试相同的操作。

  • 优先级机制:给线程分配优先级,高优先级的线程可以优先获取资源。

饥饿(Starvation)

饥饿是指某个或某些线程因为某种原因一直得不到执行的机会,从而无法完成其任务的现象。这通常是由于优先级调度、不公平的锁机制等原因导致的。
在一个使用不公平锁(unfair lock)的情况下,新来的线程总是优先于已经在等待队列中的线程获取锁,导致后者永远得不到锁。

解决方法:
  • 公平锁:使用公平锁(如 ReentrantLock(true)),确保按照请求的顺序分配锁。

  • 轮询调度:使用轮询的方式调度线程,确保每个线程都有机会被执行。

  • 优先级调整:动态调整线程的优先级,确保长期等待的线程有机会获得资源。

哪些场景需要特别注意线程安全问题?

访问共享变量或资源

在多线程编程中,当多个线程需要访问相同的资源时,就涉及到了共享变量或共享资源的访问。这些共享资源包括但不限于访问共享对象的属性(即对象内部的字段)、访问类的静态变量(由于它们属于类本身,被所有实例共享)、以及访问共享的缓存系统(缓存数据被多个线程共享以加速数据访问)。
由于这些共享资源或变量不仅可能被单个线程单独访问,而且经常面临多个线程同时访问的情况,因此,在没有适当同步控制的情况下,就容易出现并发读写的问题。这些问题可能导致数据不一致、数据竞争、甚至是死锁等线程安全问题。数据不一致表现为某个线程修改后的数据未被其他线程及时感知,数据竞争则可能发生在多个线程同时尝试修改同一数据时,而死锁则是因为线程之间在等待对方释放资源而造成的相互阻塞。
为了确保线程安全,避免上述问题,程序员需要采取一定的同步措施,如使用关键字、接口提供的锁机制、或者包中提供的线程安全集合和原子类等,来确保在并发环境下对共享资源的访问是安全的、有序的。这样不仅可以保护数据的完整性,还能保证程序的稳定性和可靠性。

依赖时序的操作

在多线程编程中,一个需要特别关注的场景是那些依赖于特定执行时序的操作。当操作的正确性完全或部分依赖于操作的执行顺序时,这些操作就被视为是时序敏感的。然而,在多线程环境下,由于操作系统的调度机制,线程的执行顺序并不能被程序直接控制或保证。因此,即使我们在设计程序时预想了某个特定的执行顺序,实际运行时也可能因为线程调度的不确定性而导致执行顺序与预期不符。
当这种情况发生时,就可能引发线程安全问题。因为时序敏感的操作可能依赖于某个先前操作的完成状态或结果,如果这些操作被不同的线程以不确定的顺序执行,就可能导致数据不一致、逻辑错误或程序崩溃等问题。
这样类似的场景都是同样的道理,“检查与执行”并非原子性操作,在中间可能被打断,而检查之后的结果也可能在执行时已经过期、无效,换句话说,获得正确结果取决于幸运的时序。这种情况下,我们就需要对它进行加锁等保护措施来保障操作的原子性。

不同数据之间存在绑定关系

线程安全场景是不同数据之间存在相互绑定关系的情况。有时候,我们的不同数据之间是成组出现的,存在着相互对应或绑定的关系,最典型的就是 IP 和端口号。有时候我们更换了 IP,往往需要同时更换端口号,如果没有把这两个操作绑定在一起,就有可能出现单独更换了 IP 或端口号的情况,而此时信息如果已经对外发布,信息获取方就有可能获取一个错误的 IP 与端口绑定情况,这时就发生了线程安全问题。在这种情况下,我们也同样需要保障操作的原子性。

对方没有声明自己是线程安全的

我们使用其他类时,如果对方没有声明自己是线程安全的,那么这种情况下对其他类进行多线程的并发操作,就有可能会发生线程安全问题。举个例子,比如说我们定义了 ArrayList,它本身并不是线程安全的,如果此时多个线程同时对 ArrayList 进行并发读/写,那么就有可能会产生线程安全问题,造成数据出错,而这个责任并不在 ArrayList,因为它本身并不是并发安全的。
如果我们把 ArrayList 用在了多线程的场景,需要在外部手动用 synchronized 等方式保证并发安全。
所以 ArrayList 默认不适合并发读写,是我们错误地使用了它,导致了线程安全问题。所以,我们在使用其他类时如果会涉及并发场景,那么一定要首先确认清楚,对方是否支持并发操作,以上就是四种需要我们额外注意线程安全问题的场景,分别是访问共享变量或资源,依赖时序的操作,不同数据之间存在绑定关系,以及对方没有声明自己是线程安全的。

为什么多线程会带来性能问题?

主要有两个方面,一方面是线程调度,另一个方面是线程协作。

调度开销

上下文切换:
在软件开发实践中,特别是在处理高并发场景时,尽管现代CPU拥有多个核心,但实际应用中创建的线程数量往往会远远超过CPU核心数。操作系统通过复杂的调度算法,为这些线程分配有限的时间片资源,确保它们都能得到执行机会。然而,这一过程中的上下文切换机制却带来了不可忽视的性能开销。特别是在执行时间极短的任务时,频繁的上下文切换可能会消耗大量CPU资源,导致系统整体性能下降。
因此,开发者在设计和实现多线程应用时,需要谨慎考虑线程的数量与任务的性质,以避免因过度创建线程而引发的性能瓶颈。通过合理设计任务粒度、优化线程管理策略(如使用线程池)等方式,可以在确保应用并发性能的同时,有效降低上下文切换带来的开销,从而实现更高效、更稳定的系统表现。
缓存失效:在程序执行过程中,性能问题不仅源于上下文切换,缓存失效同样是一个重要因素。为了提高数据访问速度,程序常利用缓存机制存储频繁访问的数据。然而,当线程调度发生时,CPU转向执行其他线程的代码,原先缓存的数据可能因不再被当前线程所需而变得无效,这时需重新加载新数据到缓存中,这一过程增加了额外的开销。
为了减少上下文切换的频率及其带来的性能损失,线程调度器通常会为被调度的线程设定一个最小执行时间片,确保线程在执行完该时间段内的任务前不会被打断,从而降低了上下文切换的频率。
另一方面,密集的上下文切换往往由特定情况触发,如程序中的锁竞争激烈或频繁的I/O阻塞。这些情况会导致线程频繁地等待资源或执行中断,迫使操作系统频繁地进行上下文切换,显著增加系统开销。因此,在设计程序时,应努力优化锁的使用策略,减少不必要的I/O操作,以避免造成密集的上下文切换,从而提高程序的整体性能和效率。

协作开销

除了线程调度之外,线程协作同样也有可能带来性能问题。因为线程之间如果有共享数据,为了避免数据错乱,为了保证线程安全,就有可能禁止编译器和 CPU 对其进行重排序等优化,也可能出于同步的目的,反复把线程工作内存的数据 flush 到主存中,然后再从主内存 refresh 到其他线程的工作内存中,等等。这些问题在单线程中并不存在,但在多线程中为了确保数据的正确性,就不得不采取上述方法,因为线程安全的优先级要比性能优先级更高,这也间接降低了我们的性能。

华为MetaERP Oracle EBS OM(订单管理)模块详解作为ERP财务解决方案架构师,理解OM模块的业务全景和数据模型至关重要。OM是EBS分销链路的核心引擎,贯穿“订单到收款“(Orde Oracle EBS OM模块是管理销售订单全生命周期的核心模块,涵盖客户管理、订单执行、发运开票、定价及信用控制等功能。其标准流程包括订单录入、预订、发放、发运确认和自动开票等环节,核心数据表涉及订单、客户、发运和定价等业务对象。OM模块与库存、应收、发运等模块深度集成,架构师需重点设计订单类型、信用控制策略和自动开票接口等关键环节,确保业务流程高效流转。 阅读详情

相关推荐

Ghidra逆向分析实战:从安装JDK到解析STM32的bin文件(附SVD插件配置)

本文详细介绍了使用Ghidra进行STM32固件逆向分析的完整流程,从JDK安装配置到SVD插件的应用。通过实战案例展示如何解析bin文件、恢复符号信息以及利用SVD插件识别外设寄存器,帮助开发者高效完成嵌入式设备的逆向工程任务。

weixin_29309475的博客 646

JAVA面试八股文,万字长文!

Java核心面试知识点摘要 多态与反射 多态:通过继承、重写实现,父类引用指向子类对象,提高代码扩展性 反射:运行时获取类信息和操作方法,用于动态加载、自定义注解等场景 JVM核心 内存模型:堆、栈、方法区等区域功能 垃圾回收:分代回收机制,多种GC回收器选择 类加载:加载-验证-准备-解析-初始化过程 多线程 线程创建方式:继承Thread、实现Runnable等 同步机制:synchronized、AQS锁、CAS等 线程池:submit与execute区别 MySQL优化 事务特性ACID 隔离级别:

2401_89221445的博客 1469

椭圆曲线点加的推导公式(一)

椭圆曲线点加推导过程

qq_32100199的博客 1518

2025最新JAVA面试八股文,万字长文!

反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,都能够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。I这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。应用场景有:要操作权限不够的类属性和方法时、实现自定义注解时、动态加载第三方jar包时、按需加载类,节省编译和初始化时间;获取class对象的方法有:class.forName(类路径),类.class(),对象的getClass()

qq_45591302的博客 2万+

药物对接仿真软件:HADDOCK_(16).实践项目:抗体-抗原对接案例

定义分子结构文件。

kkchenjj的博客 582

Unreal Engine开发:角色动画与状态机_动画混合:通过混合节点实现平滑动画过渡

动画混合是提升游戏动画质量的关键技术之一。Unreal Engine提供了多种混合节点,包括BlendSpace1D、Blend Two By Bool、Blend Two By Alpha和Sequencer,每种节点适用于不同的场景。通过合理使用这些节点,可以实现平滑、自然且高效的动画过渡效果。此外,遵循最佳实践和高级应用技巧,可以进一步优化动画质量和性能。希望本节内容对你在Unreal Engine中实现动画混合有所帮助,使你的游戏角色动作更加流畅和自然。

chenlz2007的博客 1247

万字长文详解Java线程池

而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。1、线程池不在RUNNING状态,线程池调用shutdown等方法关闭线程池后,即便线程池内部依然存在没有执行完的任务正在执行,但是线程池已经关闭,此时再向线程池内提交任务,就会遭到拒绝。线程可以从队列的两端出队任务执行,但通常从自己的那一端出队,而从其他线程的队列另一端窃取任务。

qq_35701988的博客 939

STM32F103/F407/H743不同GPIO速度配置(HAL库)对应的最高速度

详细介绍了STM32F103/F407/H743不同GPIO速度配置(HAL库)对应的最高速度。

kevin1499的博客 5594

四万字长文总结多线程,一篇就够了!

1.认识线程及线程的创建 <1>.线程的概念 线程和进程的区别: 进程是系统分配资源的最小单位,线程是系统调度的最小单位。 一个进程内的线程之间是可以共享资源的。 每个进程至少有一个线程存在,即主线程。 注: 每个进程至少有一个线程存在,即主线程(系统级别的,C语言的主线程) java级别的主线程(自己写的入口函数main方法(可以没有这个线程) 对java进程来说,至少有一个非守护线程还没终止,进程就不会结束 <2>.线程的特性 在后面线程的安全性会详细介绍 1.原子性:即一个操作

qq_45704528的博客 6262

汽车电子零部件(14):TMS热管理系统

TMS(thermal management system)热管理系统,这是新能源汽车诞生后随之而产生的一种新汽车零部件,一旦热管理失控会触发自燃,这种现象也是对EV来说是件头疼的事。汽车的热管理系统(TMS)是一个关键部件,有助于调节汽车电池组、车厢和其他车辆系统的温度。TMS的主要目的是在驾驶过程中保持最佳的电池性能和乘客舒适度。TMS使用各种加热和冷却系统来调节电池组和车厢的温度,并通过优化其使用,降低能耗,提高车辆的整体效率。

码农17年的博客 3166

华为OD技术面八股文_C++_01

汇总华为OD技术面C++方向八股文真题,并提供详细解答。

qq_45776114的博客 1891

【005】万字长文: 什么是线程池?在Java中如何使用线程池?

线程池是一种复用线程、降低资源开销的并发工具。Java中通过Executor框架实现,推荐使用ThreadPoolExecutor手动创建(可灵活配置核心线程数、队列和拒绝策略),而非Executors工具类(存在OOM风险)。线程池核心思想是复用线程,避免频繁创建销毁,提高响应速度并便于管理。使用时需提交Runnable/Callable任务,并注意合理关闭线程池。

编程技术圈 2351

结构光相移法-多频外差原理与实践

结构光相移法是一种常用的三维形貌测量技术,它通过对投射到目标物体上的结构光进行相位调制和解调,实现对物体表面的三维重建。其中,多频外差原理是结构光相移法的核心原理之一,通过使用不同频率的结构光来实现相位测量,并可以提高测量精度和抗干扰性能。结构光相移法的多频外差原理是一种常用的三维形貌测量技术,它通过使用多个不同频率的结构光进行相位调制和解调,实现对物体表面的三维重建。本文介绍了多频外差原理的原理和实践应用,并提供了相应的Python源代码,希望对读者理解和应用结构光相移法有所帮助。

CodeByte的博客 737

两万字长文-最全的线程池详解

int corePoolSize, // 核心线程数:线程池中始终保持存活的线程数,即使它们处于空闲状态。int maximumPoolSize, // 最大线程数:线程池中允许的最大线程数。当队列满了并且当前线程数小于最大线程数时,会创建新的线程来处理任务。long keepAliveTime, // 线程空闲时间:非核心线程的空闲时间超过这个时间,就会被回收。指定数值和时间单位一起使用。

图你怀中安稳的博客 1208

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

五万字长文:Java面试知识点总结

大家第一眼看过去可能会怀疑文章是不是标题党,这篇短短的文章应该是没有50000字的。但是本文实际上远远不止五万字,大家可以打开正文中任何一个链接看看,每一个链接都是一类知识点的总结,任何一个链接都是。正文中的每一个链接都是目录分类的形式知识点的总结,使得杂乱无章的知识点变得条理很清楚,也便于同学们复习。 另外,博主收集了几类全面的思维导图,思维导图可以帮助大家系统全面的建立起知识网络,方便大家理解...

我的博客 3838

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

99%的人答不对的并发题(从JVM底层理解线程安全,硬核万字长文)

众所周知,java 是一门可以轻松实现多线程的语言,再加之目前的社会环境和业务需求,对多线程的使用和高并发的场景也越来越多,与之带来的就是并发安全的问题。如何在多线程的环境下写出符合业务需求的代码,是程序员的基本功。而理解 JVM 中的线程特性,则是我们扎实根底的第一步。 学习忌浮躁。 首先大家要知道一点,JVM 帮我们屏蔽了不同的操作系统的不同特性,来实现一次编写到处运行的特点。所以有个很关键的...

小龙JWY的博客 1万+

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

【万字长文】Java面试八股文:深入剖析常见问题与解答

封装是将数据和方法进行封装,形成一个类,隐藏对象的内部细节,提供统一的接口。封装可以提高代码的安全性和可维护性。多态是面向对象的一个重要特性,分为编译时多态和运行时多态。在Java中,运行时多态通过继承和接口实现。@Override@Override对象关系映射:将Java对象映射到数据库表,避免了SQL和数据库结构的直接关联。自动建表:通过Java类自动生成数据库表。缓存机制:提高数据库访问性能。事务管理:保证数据的一致性和完整性。

努力是为了站在万人之中,成为别人的光 9901

万字长文详解java多并发知识点

CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。在JUC( java.util.concurrent )包下实现的很多类都用到了CAS操作AbstractQueuedSynchronizer(AQS框架)AtomicXXX类例子:我们还是基于刚才学习过的JMM内存模型进行说明线程1与线程2都从主内存中获取变量int a = 100,同时放到各个线程的工作内存中。

weixin_60800735的博客 197

2 万字长文详解 10 大多线程面试题|原力计划

作者 |ZZZhonngger责编 | 伍杏玲出品 | 优快云博客Volatile相关1.请谈谈你对 volatile 的理解答:volatile ...

优快云资讯 2140

2026最新JAVA面试八股文,万字长文!

Java核心面试知识点总结 本文系统整理了Java核心技术面试知识点,涵盖基础语法、JVM、多线程、MySQL、SpringBoot和Redis等方面。 基础篇: 面向对象三大特性:封装、继承、多态 集合框架:ArrayList与LinkedList区别,HashMap实现原理 异常处理:try-catch-finally执行流程 并发编程:线程创建方式,synchronized与volatile JVM篇: 内存模型:堆、栈、方法区作用 垃圾回收:GC算法与回收器 类加载机制:双亲委派模型 数据库篇: M

A1234543211232的博客 5177

3w 字长文爆肝 Java 基础面试题!太顶了!!!

hey guys ,这不是也到了面试季了么,cxuan 又打算重新写一下 Java 相关的面试题,先从基础的开始吧,这些面试题属于基础系列,不包含多线程相关面试题和 JVM 相关面试题,多线程和 JVM 的我放在后面了,下面不多说,搞起! Java 基础篇 Java 有哪些特点 并发性的: 你可以在其中执行许多语句,而不必一次执行它 面向对象的:基于类和面向对象的编程语言。 独立性的: 支持一次编写,到处运行的独立编程语言,即编译后的代码可以在支持 Java 的所有平台上运行。 Java 的特性 Ja

程序员cxuan的个人主页 2万+

万字长文!七个方面带你玩转Java线程池

点击上方 "程序员小乐"关注,星标或置顶一起成长后台回复“大礼包”有惊喜礼包!关注订阅号「程序员小乐」,收看更多精彩内容每日英文Remember every who t...

程序员小乐 378

万字长文:带你透彻理解“线程池”

什么是线程池为什么使用线程池线程池有哪些优势线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;网购商品秒杀云盘文件上传和下载12306网上购票系统等总之,只要有并发的地方、任务数量大或小、每个任务执行时间长或短的都可以使用线程池;只不过在使用线程池的时候,注意一下设置合理的线程池大小即可;

2401_83384536的博客 864

万字长文扫盲JUC基础

JUC基础篇,锁

ljz66254的博客 1221

面试干货 | 万字长文详解Java基础篇,面试热点题整理

史上最全最细致最完整的Java基础八股文,全篇文章图文并茂,每个知识点都有深入讲述,致力于让读者深刻理解Java基础,更好地应用面试!

qq_45832461的博客 5167
上一篇: 读取 SpringBoot 配置的6种方式
下一篇: 万字长文详解Java线程池
Java_2017_csdn
博客等级 码龄10年 329粉丝 49原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值