• Java多线程学习笔记



    1. 引言

    1.1 多线程的重要性

    多线程编程是现代软件开发中的重要技术,能够提高资源利用率、提升应用程序的响应性、简化并发问题的建模和实现、提升性能,并增强系统的健壮性和容错性。掌握多线程编程技术,对开发高效、可靠的现代应用至关重要。

    2. 什么是多线程

    2.1 线程的定义和基本概念

    在计算机科学中,是将进程划分为两个或多个线程(实例)或子进程,由单处理器(单线程)或多处理器(多线程)或多核处理系统并发执行。

    2.2 线程与进程的区别

    线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。
    进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是
    一个进程从创建,运行到消亡的过程。

    3. 创建线程的方式

    3.1 继承Thread类

    public class MyThread extends Thread {
    
        public void run() {
            System.out.println("Thread is running");
        }
    
        public static void main(String[] args) {
            MyThread thread = new MyThread();
            thread.start();
        }
    }
    

    3.2 实现Runnable接口,重写run方法

    class MyRunnable implements Runnable {
    
        public void run() {
            System.out.println("Thread is running");
        }
    
        public static void main(String[] args) {
            MyRunnable myRunnable = new MyRunnable();
            Thread thread = new Thread(myRunnable);
            thread.start();
        }
    
    }
    

    3.3 实现Runnable接口,重写call方法

    无参

    public class MyCallable implements Callable<String> {
    
        public String call() {
            return "Thread is running";
        }
    
        public static void main(String[] args) {
            MyCallable callableTask = new MyCallable();
            String call = callableTask.call();
            System.out.println(call);
        }
    
    }
    

    有参

    public class MyCallable implements Callable<String> {
    
        private String name;
    
        public MyCallable(String name) {
            this.name = name;
        }
    
        public String call() {
            return "Hello, " + name;
        }
    
        public static void main(String[] args) {
            MyCallable callableTask = new MyCallable("Thread is running");
            String call = callableTask.call();
            System.out.println(call);
        }
    
    }
    
    

    3.4 匿名内部类创建Thread子类对象

    public class Test {
    
        public static void main(String[] args) {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    System.out.println("Thread anonymous class is running");
                }
            };
            thread.start();
        }
    
    }
    

    3.5 使用匿名内部类实现 Runnable 接口

    public class Test {
    
        public static void main(String[] args) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Runnable anonymous class is running");
                }
            });
            thread.start();
        }
    
    }
    
    

    3.6 使用匿名内部类实现 Callable 接口

    public class Test {
    
        public static void main(String[] args) {
            // 创建一个线程池
            ExecutorService executor = Executors.newFixedThreadPool(1);
    
            // 提交一个Callable任务,使用匿名内部类实现Callable接口
            Future<String> future = executor.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    System.out.println("Callable anonymous class is running");
                    // 模拟一些长时间的任务
                    Thread.sleep(1000);
                    return "Callable task completed";
                }
            });
    
            try {
                // 获取Callable任务的执行结果
                String result = future.get();
                System.out.println("Result from Callable: " + result);
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            } finally {
                // 关闭线程池
                executor.shutdown();
            }
        }
    
    }
    

    3.7 实现runable和callable的区别

    Runnable:适用于任务不需要返回结果或处理异常的场景,如简单的后台任务、事件处理等。
    Callable:适用于任务需要返回结果或需要处理异常的场景,如计算任务、IO操作等。

    4. 线程的生命周期

    4.1 线程的状态

    1. 新建(New)
      (1)线程对象已经被创建,但还没有调用start()方法。
      (2)线程处于新建状态,还没有开始执行。

    2. 可运行(Runnable)
      (1)线程已经调用了start()方法,但还没有获得CPU时间片开始执行。
      (2)线程处于就绪状态,等待操作系统调度执行。
      (3)一旦获得CPU时间片,线程就会执行其run()方法。

    3. 运行中(Running)
      (1)线程当前正在执行代码。
      (2)严格来说,Java线程状态没有单独的“运行中”状态,它是Runnable状态的一部分。

    4. 阻塞(Blocked)
      (1)线程在等待监视器锁(monitor lock),以进入同步块或方法。
      (2)当线程试图进入一个被其他线程持有的同步块/方法时,会进入阻塞状态。

    5. 等待(Waiting)
      (1)线程等待另一线程显式地唤醒(通过Object.wait()、Thread.join()、LockSupport.park()等方法)。
      (2)等待状态下的线程不占用CPU时间片。

    6. 超时等待(Timed Waiting)
      (1)线程等待另一线程显式地唤醒,但有时间限制(通过Thread.sleep()、Object.wait(long timeout)、Thread.join(long millis)、LockSupport.parkNanos()等方法)。
      (2)超时等待状态下的线程不占用CPU时间片。

    7. 终止(Terminated)
      (1)线程已完成执行,或由于异常退出了run()方法。
      (2)线程生命周期结束,进入终止状态。

    New -> Runnable -> Blocked/Waiting/Timed Waiting -> Runnable -> Terminated
    

    4.2 线程状态转换图

    在这里插入图片描述

    5. 线程的基本控制方法

    5.1 线程状态管理

    5.1.1 start()

    启动线程,使其进入可执行状态(RUNNABLE状态)。
    系统会在后台为线程分配资源,并调用线程的run()方法执行任务。

    5.1.2 join()

    当前线程等待目标线程执行完成。
    调用该方法的线程将会阻塞,直到目标线程执行完毕或超时。

    5.1.3 sleep(long millis)

    让当前线程休眠指定的时间(毫秒),进入TIMED_WAITING状态。
    休眠结束或被中断后线程重新进入可运行状态。

    5.1.4 yield():

    暂停当前正在执行的线程对象,并执行其他线程。
    可以让同优先级的线程有机会执行。

    5.2 线程等待和唤醒

    wait()、notify()、notifyAll():
    这些方法是Object类中的方法,用于线程间的等待和唤醒机制,必须在同步代码块或同步方法中使用。

    5.2.1 wait()

    使当前线程等待,释放锁资源。

    5.2.2 notify()

    唤醒一个等待中的线程。

    5.2.3 notifyAll()

    唤醒所有等待中的线程。

    5.3 线程优先级设置

    5.3.1 setPriority(int priority)

    设置线程的优先级,优先级范围为1到10,默认为5。
    高优先级的线程具有抢占低优先级线程CPU时间的能力,但具体实现依赖于操作系统。

    5.4 线程中断

    5.4.1 interrupt():

    中断线程,设置线程的中断状态为true。
    被中断的线程可以通过检查自身的中断状态或捕获InterruptedException异常来处理中断请求。

    5.4.2 isInterrupted():

    判断线程是否被中断,返回true或false。

    5.4.3 interrupted():

    判断当前线程是否被中断,返回true或false,同时会清除中断状态。

    start和run的区别

    1. start() 方法
    作用:

    start()方法用于启动一个新的线程,并使线程进入可执行状态(RUNNABLE状态)。在调用start()方法后,系统会为该线程分配必要的资源,并执行线程的run()方法。start()方法只能被调用一次,重复调用会抛出IllegalThreadStateException异常。
    执行过程:
    当调用start()方法后,系统会在后台启动一个新的线程,并调用该线程的run()方法。start()方法返回后,当前线程(通常是调用start()的线程)会继续执行,而不会等待新线程的执行完毕。
    2. run() 方法
    作用:

    run()方法是Thread类的实例方法,用于定义线程的主体任务或逻辑。当线程处于可执行状态并获得CPU时间片时,系统会调用线程的run()方法执行具体的任务。
    执行过程:
    run()方法被直接调用时,它会在当前线程中执行,而不会创建新的线程。直接调用run()方法会使得线程任务在当前线程中串行执行,不会体现出多线程的并发特性。

    6. 线程的同步与互斥

    一个多线程的程序,两个或者多个线程可能需要访问同一个数据资源。这时就必须考虑数据安全的问题,需要线程互斥或者同步。

    6.1 synchronized关键字

    例如对 count 进行修改或访问

    同步方法:

    public class Counter {
        private int count = 0;
    
        public synchronized void increment() {
            count++;
        }
    
        public synchronized int getCount() {
            return count;
        }
    }
    

    同步代码块:

    public class Counter {
        private int count = 0;
        private final Object lock = new Object();
    
        public void increment() {
            synchronized (lock) {
                count++;
            }
        }
    
        public int getCount() {
            synchronized (lock) {
                return count;
            }
        }
    }
    

    6.2 ReentrantLock

    public class Counter {
        private int count = 0;
        private final ReentrantLock lock = new ReentrantLock();
    
        public void increment() {
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }
    
        public int getCount() {
            lock.lock();
            try {
                return count;
            } finally {
                lock.unlock();
            }
        }
    }
    

    7. 线程间通信

    7.1 BlockingQueue

    BlockingQueue 是 java.util.concurrent 包中的接口,它提供了线程安全的阻塞队列实现,如 ArrayBlockingQueue、LinkedBlockingQueue 等。它们简化了生产者-消费者模式的实现,自动处理线程间的等待和唤醒。

    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
    
        Thread producer = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    queue.put(i);
                    System.out.println("Produced: " + i);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
    
        Thread consumer = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    int value = queue.take();
                    System.out.println("Consumed: " + value);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
    
        producer.start();
        consumer.start();
    }
    

    这里我只列举了其中一种,其他实现方式可参考:https://blog.csdn.net/qq_42411214/article/details/107767326 或其他文章。

    8. 高级多线程工具

    8.1 线程池

    8.1.1 线程池继承关系

    在这里插入图片描述

    1、Executor 接口:Executor 是一个函数式接口,定义了一个执行任务的方法execute(Runnable command),用于将任务提交到线程池执行。它是所有执行器的基类,提供了最基本的任务执行方法。

    2、ExecutorService :ExecutorService 是 Executor 的扩展,提供了更高级的任务管理功能,包括任务的提交、返回结果、取消任务以及关闭线程池等。它是一个接口,定义了管理线程池的方法。其中提供了一个方法submit(Runnable task):提交一个任务,返回 Future 对象。

    3、AbstractExecutorService :AbstractExecutorService 是 ExecutorService 的抽象实现类,提供了一些默认实现,方便子类实现。它实现了 ExecutorService 接口,提供了 shutdown() 和 shutdownNow() 方法的默认实现。

    4、ThreadPoolExecutor 类:ThreadPoolExecutor 是 ExecutorService 的一个具体实现,提供了一个可配置的线程池。
    它具有高度的可配置性,可以设置线程池的核心线程数、最大线程数、线程存活时间、任务队列和拒绝策略等。

    8.1.2 ThreadPoolExecutor

    在这里插入图片描述

    ThreadPoolExecutor 的构造方法包含七个参数:

    1、核心线程数量——在线程池当中无论空闲多久都不会被删除的线程
    2、线程池当中最大的线程数量——线程池当中最大能创建的线程数量
    3、空闲时间(数值)——临时线程(线程池中出核心线程之外的线程)空闲了多久就会被淘汰的时间。
    4、空闲时间(单位)——临时线程空闲了多久就会被淘汰的时间单位,要用枚举类TimeUnit类作为参数
    5、阻塞队列——就是创建一个阻塞队列作为参数传入,就是当线程池当中线程数量已经达到了最大线程数量,允许多少个任务排队获取线程,其余的用参数七那个方案来处理。
    6、创建线程的方式——不是new一个线程,而是传入一个线程工厂(例如:Executors工具类中的defaultThreadFactory方法返回的就是一个线程工厂)
    7、要执行的任务过多时的解决方案——当等待队列中也排满时要怎么处理这些任务(任务拒绝策略)。

    ThreadPoolExecutor 支持多种任务队列:

    LinkedBlockingQueue:一个基于链表的阻塞队列,容量为 Integer.MAX_VALUE,适合大多数场景。
    ArrayBlockingQueue:一个有界的阻塞队列,必须指定容量。
    SynchronousQueue:一个不存储元素的队列,每个插入操作必须等待一个相应的移除操作。
    PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。

    拒绝策略

    当线程池和队列都满了时,可以使用拒绝策略来处理新提交的任务。ThreadPoolExecutor 提供了以下四种拒绝策略:

    AbortPolicy(默认):抛出 RejectedExecutionException 异常。
    CallerRunsPolicy:由调用线程执行该任务。
    DiscardPolicy:直接丢弃任务。
    DiscardOldestPolicy:丢弃队列中最旧的任务,然后重新提交被拒绝的任务。

    8.2.2.1 线程池的任务调度流程

    在这里插入图片描述

    1. 当线程池中线程数量小于 corePoolSize 则创建线程,并处理请求。
    2. 当线程池中线程数量大于等于 corePoolSize 时,则把请求放入 workQueue 中,随着线程池 中的核
      心线程们不断执行任务,只要线程池中有空闲的核心线程,线程池就从 workQueue 中取 任务并处
      理。
    3. 当 workQueue 已存满,放不下新任务时则新建非核心线程入池,并处理请求直到线程数目 达到
      maximumPoolSize(最大线程数量设置值)。
    4. 如果线程池中线程数大于 maximumPoolSize 则使用 RejectedExecutionHandler 来进行任 务拒绝
      处理。

    8.1.2 Executors创建线程的4种方法

    1、newFixedThreadPool(int nThreads)

    创建一个固定大小的线程池,线程数量为 nThreads。当所有线程都在忙时,新的任务会在队列中等待。

    2、newCachedThreadPool()

    创建一个缓存型线程池。如果线程池的规模超过了处理需求,会回收空闲线程;当需求增加时,会添加新的线程。

    3、newSingleThreadExecutor()

    创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,确保所有任务按顺序执行。

    4、newScheduledThreadPool(int corePoolSize)

    创建一个可以延迟或定期执行任务的线程池。

    8.3 并发集合(ConcurrentHashMap, CopyOnWriteArrayList等)

    线程安全的集合可参考:Java Collection集合介绍、fail-fast快速失败机制介绍、线程安全的容器介绍

    8.4 原子变量(AtomicInteger, AtomicBoolean等)

    AtomicInteger
    AtomicInteger 是一个提供对 int 类型进行原子操作的类。它提供了许多方法来对整数进行原子操作,避免了使用 synchronized 关键字的复杂性和开销。

    AtomicBoolean
    AtomicBoolean 是一个提供对 boolean 类型进行原子操作的类。它同样提供了一些方法来确保在多线程环境下对布尔值的操作是线程安全的。

    10. 性能优化和最佳实践

    10.1 多线程性能优化

    1、多线程性能优化合理使用线程池:
    使用 Executor 框架来管理线程,而不是直接创建和管理线程。合理配置线程池的核心线程数、最大线程数和任务队列可以有效提高性能。
    选择合适的线程池类型(如固定大小线程池、缓存线程池、单线程池、调度线程池)以适应具体需求。

    2、减少上下文切换:
    线程上下文切换开销很大,尽量减少不必要的线程切换。通过调整线程池大小来控制线程数量,避免过多的线程竞争CPU资源。
    合理划分任务,避免任务过于细粒度导致频繁切换。

    3、避免锁竞争:
    尽量减少锁的使用范围和时间,缩小临界区范围。
    使用 java.util.concurrent 包中的并发集合和工具类(如 ConcurrentHashMap、CopyOnWriteArrayList)来替代传统的同步集合。
    使用 ReentrantLock 代替 synchronized,并结合 tryLock 方法进行非阻塞锁定。

    4、无锁编程:
    使用原子类(如 AtomicInteger、AtomicBoolean)进行无锁操作,提高性能。
    尽量避免在高并发环境下使用锁,尝试使用无锁算法和数据结构。

    10.2 多线程最佳实践

    1、任务划分和线程数量:
    合理划分任务,根据任务的性质和计算复杂度来确定线程的数量。
    根据硬件资源和具体应用场景调优线程池的配置,一般来说,CPU密集型任务线程数应为 CPU 核心数+1,IO密集型任务线程数应大于CPU核心数。

    2、线程安全:
    确保共享资源的线程安全,使用适当的同步机制或并发集合。
    尽量使用不可变对象,避免共享状态。

    3、线程间通信:
    使用高效的线程间通信机制,如 BlockingQueue、CountDownLatch、CyclicBarrier 等。

    4、线程生命周期管理:
    避免频繁创建和销毁线程,使用线程池来管理线程的生命周期。
    在线程池使用完毕后,记得调用 shutdown() 方法来优雅地关闭线程池。

    5、异常处理:
    在线程内部进行适当的异常处理,避免因未处理的异常导致线程突然终止。
    使用 Thread.setUncaughtExceptionHandler 来处理未捕获的异常。

    6、性能监控和调优:
    使用性能监控工具(如 JVisualVM、JProfiler)来监控线程的状态、锁竞争和CPU利用率。
    定期进行性能调优,根据实际运行情况调整线程池配置和任务划分策略。

    示例

    public class Test {
        private static final int NUM_THREADS = 4;
        private static final int NUM_TASKS = 10;
        private static final ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
        private static final ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<>();
    
        public static void main(String[] args) {
            for (int i = 0; i < NUM_TASKS; i++) {
                executor.submit(new Task("task" + i));
            }
            executor.shutdown();
            try {
                if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                executor.shutdownNow();
            }
            map.forEach((key, value) -> System.out.println(key + ": " + value));
        }
    
        static class Task implements Runnable {
            private final String taskName;
    
            Task(String taskName) {
                this.taskName = taskName;
            }
    
            @Override
            public void run() {
                map.computeIfAbsent(taskName, k -> new AtomicInteger(0)).incrementAndGet();
                System.out.println(Thread.currentThread().getName() + " executed " + taskName);
            }
        }
    }
    

    11. 常见面试题

    1、如何理解内存泄漏问题?有哪些情况会导致内存泄露?如何解决?
    2、线程有哪些基本状态?
    3、为什么要创建线程池?创建线程池的方式?
    4、创建线程池有哪几个核心参数? 如何合理配置线程池的大小?
    5、说说阻塞队列的实现
    6、线程间的通讯方式
    7、说说线程安全问题,什么是线程安全,如何实现线程安全;
    8、synchronized 和 ReentrantLock 的区别
    9、线程安全的集合

  • 相关阅读:
    NR系统双连接和移动性增强技术
    QT之QCheckBox的用法
    跨境独立站语言unicode转希伯来语
    Openpyxl笔记
    C语言perror
    基于LSCF和LSFD算法在频域中识别快速实现的MIMO研究(Matlab代码实现)
    #21天学习挑战赛—深度学习实战100例#——动物识别
    智工教育:军队文职报考要注意限制性条件
    深度强化学习-DQN算法
    最新漏洞:Spring Framework远程代码执行漏洞
  • 原文地址:https://blog.csdn.net/weixin_49832841/article/details/139683449