02多线程基础知识

目录

[1. 线程与进程](#1. 线程与进程)

进程(Process)

线程(Thread)

[2. 并发和并行](#2. 并发和并行)

并发(Concurrency)

并行(Parallelism)

[3. CPU 调度](#3. CPU 调度)

定义

类型

调度算法

上下文切换

4.线程间的状态流转

线程的主要状态

注意:

5.创建线程的方法

6.线程安全问题

7.线程间的通信

8.Lock对象的介绍和基本使用

[Lock 接口的主要方法](#Lock 接口的主要方法)

[Lock 接口的实现类](#Lock 接口的实现类)

9.避免忙等待

[使用Condition 对象](#使用Condition 对象)

使用Timer

1. 线程与进程

进程(Process)
  • 定义:进程是操作系统进行资源分配和调度的基本单位。每个进程都有自己独立的虚拟地址空间、系统资源(如文件句柄、内存等)和一组系统状态信息。

  • 特点:

    • 独立性:每个进程都有自己的独立内存空间,不会直接影响其他进程。

    • 资源分配:进程拥有独立的系统资源,如文件描述符、内存等。

    • 生命周期:进程从创建到终止有一个完整的生命周期,包括创建、就绪、运行、阻塞和终止等状态。

线程(Thread)
  • 定义:线程是进程内的一个执行单元,是操作系统进行调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源,但每个线程有自己的栈和程序计数器。

  • 特点:

    • 轻量级:相比于进程,线程的创建和切换开销较小。

    • 资源共享:同一进程内的线程共享进程的内存和其他资源。

    • 并发执行:多个线程可以并发执行,提高程序的响应性和资源利用率。

2. 并发和并行

并发(Concurrency)
  • 定义:并发是指多个任务在同一时间段内交错执行,但不一定同时执行。操作系统通过时间片调度在多个任务之间快速切换,使这些任务看起来像是同时执行的。

  • 特点:

    • 时间片调度:操作系统为每个任务分配一个时间片,在这段时间内任务可以执行。

    • 上下文切换:任务之间通过上下文切换来交替执行。

    • 适用场景:适用于 I/O 密集型任务,如网络请求、文件读写等。

并行(Parallelism)
  • 定义:并行是指多个任务在同一时刻真正同时执行。这通常需要多核处理器的支持,每个核心可以同时执行一个任务。

  • 特点:

    • 多核处理器:并行计算依赖于多核处理器,每个核心可以独立执行任务。

    • 提高计算效率:并行计算可以显著提高计算密集型任务的执行效率。

    • 适用场景:适用于计算密集型任务,如矩阵运算、图像处理等。

3. CPU 调度

定义
  • CPU 调度:CPU 调度是操作系统的核心功能之一,负责在多个进程或线程之间分配 CPU 时间。调度器根据一定的算法选择下一个执行的进程或线程,确保系统的高效运行。
类型
  • 长期调度(Job Scheduler):负责决定哪些进程可以进入内存并准备执行。通常在进程创建时进行。

  • 中期调度(Swapper):负责在内存和磁盘之间交换进程,以释放内存空间。

  • 短期调度(CPU Scheduler):负责在就绪队列中的进程或线程之间分配 CPU 时间。是最常见和最重要的调度类型。

调度算法
  • 先来先服务(First-Come, First-Served, FCFS):按照进程到达的顺序进行调度。

  • 短作业优先(Shortest Job Next, SJN):优先调度预计运行时间最短的进程。

  • 优先级调度(Priority Scheduling):根据进程的优先级进行调度,优先级高的进程优先执行。

  • 轮转法(Round Robin, RR):每个进程或线程分配一个固定的时间片,时间片结束后切换到下一个进程或线程。

  • 多级反馈队列(Multilevel Feedback Queue):结合多种调度策略,根据进程的行为动态调整其优先级和时间片。

上下文切换
  • 定义:上下文切换是指从一个进程或线程切换到另一个进程或线程的过程。包括保存当前进程或线程的状态,加载下一个进程或线程的状态。

  • 开销:上下文切换会消耗 CPU 时间和内存资源,频繁的上下文切换会影响系统的整体性能。

4.线程间的状态流转

线程的主要状态
  • NEW:线程被创建但是未启动

  • RUNNABLE:线程正在运行或者准备运行

  • BLOCKED:线程被阻塞,等待锁进入同步块或方法

  • WAITING :线程无限等待,需要其他线程调用特定方法唤醒

  • TIMED_WAITING:线程有限等待,指定时间过后恢复或被其他线程唤醒

  • TERMINATED:线程终止,执行完毕或者异常停止

注意:
  • sleep(time)和wait(time)的区别:

    • sleep(time)线程睡眠,睡眠的过程不会释放锁。到时间后自动醒来继续执行。

    • wait(time)线程等待,等待的过程会释放锁,其他线程可以抢,等待过程中被唤醒或者到时间后会进入队列争抢锁。

  • wait()和notify():

    • wait()无限等待,会释放锁,需要其他线程调用notify()或则notifyAll()唤醒,被唤醒后进入队列争抢锁

    • notify()一次只能唤醒一条等待的线程,如果是多条线程等待中,随机 唤醒一条等待中的线程。

    • notifyAll()唤醒所有等待中的线程。

    • notify()和notifyAll()都不会影响sleep状态的线程

  • wait()和notify()的共同点

    • 都需要锁对象,所以在同步方法或者块中执行

    • 两个方法的调用必须是同一个锁对象调用:理解为同一个锁对象将多条线程分到了一组中,notify就知道唤醒的是本组(同一个同步方法或块)的等待线程

5.创建线程的方法

线程的创建方法总共可以分为5种。

  • 继承Thread类,通过重写run()方法创建线程

    复制代码
    public class ThreadOne extends Thread {
        public void run() {
            System.out.println("Thread One is running");
        }
        public static void main(String[] args) {
            ThreadOne threadOne = new ThreadOne();
            //调用start方法,开启线程,jvm自动调用run方法
            threadOne.start();
        }   
    }

    Thread类中的方法:

    • void start():开启线程,jvm自动调用run()方法

    • void run():设置线程任务。Thread重写Runnable中的run()方法

    • String getName():获取线程名字

    • void setName():给线程设置名字

    • static Thread currentThread():获取当前线程对象

    • static void sleep(long millis):线程睡眠,超时后自动醒来继续执行,参数是毫秒

    • void setpriority(int newPriority):设置线程优先级

    • void join() 插队

    • void yield() 礼让

  • 实现Runnable接口,实现run()方法创建线程

    复制代码
    public class ThreadTwo implements Runnable{
        @Override
        public void run() {
            System.out.println("Thread Two is running");
        }
    ​
        public static void main(String[] args) {
            ThreadTwo threadTwo = new ThreadTwo();
            /*
               Thread(Runnable target)
             */
            Thread thread = new Thread(threadTwo);
            thread.start();
        }
    }
  • 使用Lambda表达式匿名内部类,简化Runnale的创建

    复制代码
    public class ThreadThree {
        public static void main(String[] args) {
            new Thread(new Runnable(){
                @Override
                public void run() {
                    System.out.println("Thread Three is running");
                }
            },"threadThree").start();
        }
    }
  • 使用ExecutorService创建和管理线城池,使用线程池创建

    使用 ExecutorService 创建线程池时,newFixedThreadPoolnewCachedThreadPool 是两种常用的工厂方法。然而,这两种线程池在某些情况下可能会因为资源耗尽而导致 OutOfMemoryError(OOM)。

    注意:实际的开发中不要使用ExecutorService创建线程池,要使用new ThreadPoolExecutor的方式。

    复制代码
        public static void main(String[] args) {
            // 线程池参数
            int corePoolSize = 5; // 核心线程数
            int maximumPoolSize = 10; // 最大线程数
            long keepAliveTime = 60L; // 线程空闲时间
            TimeUnit unit = TimeUnit.SECONDS; // 时间单位
            BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); // 工作队列
            ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 线程工厂
            RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy(); // 拒绝策略
    ​
            // 创建线程池
            ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
            );
    ​
            // 提交大量任务省略...
    ​
            // 关闭线程池
            executor.shutdown();
        }
    • newFixedThreadPool使用的是无界队列,当队列中的任务增长速度远大于处理的速度,队列会不断增长,导致内存耗尽。应对思路:设置有限队列存放任务

    • newCachedThreadPool使用的是可缓存的线程池,当任务的提交速度大于处理速度,线程池不断创建新线程,导致内存耗尽。应对思路:设置拒绝策略。

  • 使用FutureTask和Callable创建

    FutureTaskCallable 是用于实现异步计算和返回结果的重要接口和类。

    复制代码
    public class ThreadFive {
        public static void main(String[] args) {
            FutureTask<Integer> futureTask = new FutureTask<>(() -> {
                int sum = 0;
                for (int i = 0; i < 100; i++) {
                    sum += i;
                }
                return sum;
            });
    ​
            new Thread(futureTask).start();
    ​
            try {
                System.out.println(futureTask.get());
            } catch (InterruptedException | ExecutionException e) {
                System.out.println("Error: " + e.getMessage());
            }
        }
    }

6.线程安全问题

模拟妈妈摊10个煎饼儿子吃10个煎饼的场景

复制代码
public static void main(String[] args) {
        // 妈妈摊10煎饼
        Thread mother = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("妈妈:摊了一个煎饼!");
                NUM_PANCAKES++;
                System.out.println("还剩:" + NUM_PANCAKES + "个煎饼!");
            }
        });
​
        // 儿子吃10煎饼
        Thread son = new Thread(() -> {
            for (int i = 10; i > 0; i--) {
                System.out.println("儿子:吃了一个煎饼!");
                NUM_PANCAKES--;
                System.out.println("还剩:" + NUM_PANCAKES + "个煎饼!");
            }
        });
​
        mother.start();
        son.start();
    }

结果:

多线程下对同一个共享资源的访问,会导致诸多线程安全问题:

  • 数据竞争

    多个线程访问同一内存位置,至少一个在写,没有合适的同步机制保护数据就会导致未定义的行为。

  • 竞态条件

    多个线程访问和操作共享顺序的操作非原子性(不可中断的操作),那么就会导致不同的顺序不同的结果。

  • 死锁

    线程间互相等待对方的资源。吃饭需要碗和勺子,一个拿碗一个拿勺。

  • 内存可见性

    线程更改了共享变量的值,其他线程没有及时同步更新,读取的还是自己的缓存。

  • 指令重排序

    编译器和处理器可能会重新安排指令的执行顺序,多线程下会影响程序的正确性

7.线程间的通信

在Java中,可以使用wait()notify()方法来实现线程间的同步通信。

用妈妈摊煎饼儿子吃煎饼,模拟线程间通信,同时确保煎饼只能摊一张吃一张的功能。

复制代码
public class PancakeScenario {
    // 共享资源:煎饼
    private static boolean pancakeReady = false;
    private static int NUM_PANCAKES_SUM = 0;
​
    private static int NUM_PANCAKES_REST = 0;
​
    public static void main(String[] args) {
        // 妈妈:摊煎饼
        Thread motherThread = new Thread(() -> {
            while(true) {
                synchronized(PancakeScenario.class) {
                    // 有煎饼,妈妈就等待
                    while (pancakeReady) {
                        try {
                            PancakeScenario.class.wait();
                        } catch (InterruptedException e) {
                            System.out.println("妈妈:等待失败...");
                        }
                    }
                    System.out.println("妈妈:烤煎饼中...");
                    // 煎饼摊好了,妈妈通知儿子
                    pancakeReady = true;
                    NUM_PANCAKES_REST++;
                    NUM_PANCAKES_SUM++;
                    System.out.println("还剩" + NUM_PANCAKES_REST + "个煎饼");
                    System.out.println("妈妈摊了" + NUM_PANCAKES_SUM + "个煎饼");
                    PancakeScenario.class.notify();
                }
            }
        }, "motherThread");
​
        // 儿子:吃煎饼
        Thread childThread = new Thread(() -> {
            while(true) {
                synchronized(PancakeScenario.class) {
                    // 没煎饼,儿子就等待
                    while (!pancakeReady) {
                        try {
                            PancakeScenario.class.wait();
                        } catch (InterruptedException e) {
                            System.out.println("儿子:等待失败...");
                        }
                    }
                    System.out.println("儿子:吃煎饼中...");
                    // 煎饼吃完了,儿子通知妈妈
                    pancakeReady = false;
                    NUM_PANCAKES_REST--;
                    System.out.println("还剩" + NUM_PANCAKES_REST + "个煎饼");
                    System.out.println("儿子吃了" + NUM_PANCAKES_SUM + "个煎饼");
                    PancakeScenario.class.notify();
                }
            }
        }, "childThread");
​
        motherThread.start();
        childThread.start();
    }
}

8.Lock对象的介绍和基本使用

在Java中,Lock 接口提供了比内置的 synchronized 关键字更灵活的锁定机制。Lock 接口及其相关类位于 java.util.concurrent.locks 包中,提供了一系列高级功能,如公平锁、非阻塞锁、可中断锁等。

Lock 接口的主要方法
  1. void lock():获取锁。如果锁不可用,当前线程将被阻塞,直到锁可用。

  2. void lockInterruptibly():获取锁,如果锁不可用,当前线程将被阻塞,直到锁可用或被中断。

  3. boolean tryLock() :尝试获取锁。如果锁可用,则立即返回 true;如果锁不可用,则立即返回 false

  4. boolean tryLock(long time, TimeUnit unit) :尝试获取锁,但在指定的等待时间内如果锁不可用,则返回 false

  5. void unlock():释放锁。

Lock 接口的实现类
  1. ReentrantLock :最常用的 Lock 实现,支持重入。这意味着同一个线程可以多次获取同一个锁,而不会导致死锁。

  2. ReentrantReadWriteLock:读写锁,允许多个读取者同时访问资源,但写入者独占资源。

  3. StampedLock:提供乐观读锁、写锁和读锁,适用于高性能读多写少的场景

用lock改造摊煎饼场景:

复制代码
public class PancakeScenarioUpgrade {
    // 共享资源:煎饼
    private static boolean pancakeReady = false;
​
    // 锁对象
    private static final Lock lock = new ReentrantLock();
​
    public static void main(String[] args) {
        // 妈妈线程:负责摊煎饼
        Thread motherThread = new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    while (pancakeReady) {
                        // 如果已经有煎饼了,妈妈就等待
                        try {
                            lock.unlock();
                            Thread.sleep(1000); // 模拟等待时间
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally {
                            lock.lock();
                        }
                    }
                    // 摊煎饼
                    pancakeReady = true;
                    System.out.println("妈妈摊了一个煎饼");
                } finally {
                    lock.unlock();
                }
​
                // 模拟摊煎饼的时间
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
​
        // 儿子线程:负责吃煎饼
        Thread sonThread = new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    while (!pancakeReady) {
                        // 如果没有煎饼,儿子就等待
                        try {
                            lock.unlock();
                            Thread.sleep(1000); // 模拟等待时间
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally {
                            lock.lock();
                        }
                    }
                    // 吃煎饼
                    pancakeReady = false;
                    System.out.println("儿子吃了煎饼");
                } finally {
                    lock.unlock();
                }
​
                // 模拟吃煎饼的时间
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
​
        // 启动线程
        motherThread.start();
        sonThread.start();
    }
}

9.避免忙等待

在改造的场景中,idea有一个提示信息:

复制代码
// 在循环中使用Thread.sleep()可能会忙等待
Call to 'Thread.sleep()' in a loop, probably busy-waiting

忙等待是指在一个循环中不断检查某个条件,直到该条件满足为止,而在此过程中线程不会放弃 CPU 时间片,也不会进入休眠状态。这种方式通常用于短时间的等待,或者在高实时性要求的场景中。它会导致CPU持续占用和性能损耗。

使用Condition 对象

可以使用Condition 对象,提供了更精确的等待和通知机制,避免 Thread.sleep() 的精度问题。

复制代码
public class PancakeScenario {
​
    // 共享资源:煎饼
    private static boolean pancakeReady = false;
​
    // 锁对象
    private static final Lock lock = new ReentrantLock();
    // 条件对象
    private static final Condition pancakeReadyCondition = lock.newCondition();
​
    public static void main(String[] args) {
        // 妈妈线程:负责摊煎饼
        Thread motherThread = new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    while (pancakeReady) {
                        // 如果已经有煎饼了,妈妈就等待
                        pancakeReadyCondition.await();
                    }
                    // 摊煎饼
                    System.out.println("妈妈摊了一个煎饼");
                    pancakeReady = true;
                    // 通知儿子可以吃煎饼了
                    pancakeReadyCondition.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
​
                // 模拟摊煎饼的时间
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
​
        // 儿子线程:负责吃煎饼
        Thread sonThread = new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    while (!pancakeReady) {
                        // 如果没有煎饼,儿子就等待
                        pancakeReadyCondition.await();
                    }
                    // 吃煎饼
                    System.out.println("儿子吃了煎饼");
                    pancakeReady = false;
                    // 通知妈妈可以摊新的煎饼了
                    pancakeReadyCondition.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
​
                // 模拟吃煎饼的时间
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
​
        // 启动线程
        motherThread.start();
        sonThread.start();
    }
}
使用Timer

还是摊煎饼吧,儿子每隔10秒就跑去看煎饼是否摊好。

复制代码
public static void main(String[] args) {
        Thread son = new Thread(() -> {
            while (true) {
                try {
                    //看煎饼好了没
                    checkPancakeStatus();
                    Thread.sleep(1000L * 10);
                } catch (Exception e) {
                    //print the error log
                    e.printStackTrace();
                }
            }
        });
        son.start();
    }

如果checkPancakeStatus方法抛出了异常就会跳过sleep(),那就没办法休眠,循环持续执行,在这个基础上,如果捕获异常打印了日志,还会导致日志撑爆磁盘。

还有一个要注意的坑,线程在sleep的过程中并不会释放所持有的锁,这会导致严重的并发问题,甚至是死锁。

推荐可以使用使用jdk自带的java.util.Timer解决:

复制代码
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                try {
                    //看煎饼好了没
                    checkPancakeStatus();
                    Thread.sleep(1000L * 10);
                } catch (Exception e) {
                    //print the error log
                    e.printStackTrace();
                }
            }
        }, 1000L * 10, 1000L * 10);
    }
相关推荐
木易 士心8 分钟前
Spring AI 核心架构解析:构建企业级 AI 应用的 Java 新范式
java·spring
61900833618 分钟前
linux 安装jdk
java·linux·运维
懂得节能嘛.21 分钟前
【动态配置中心】Java+Redis构建动态配置中心
java·开发语言·redis
专注于大数据技术栈22 分钟前
Java中JDK、JRE、JVM概念
java·开发语言·jvm
YuanlongWang25 分钟前
C# 基础——值类型与引用类型的本质区别
java·jvm·c#
Kay_Liang1 小时前
大语言模型如何精准调用函数—— Function Calling 系统笔记
java·大数据·spring boot·笔记·ai·langchain·tools
自由的疯1 小时前
Java 如何学习Docker
java·后端·架构
自由的疯1 小时前
Java Docker本地部署
java·后端·架构
007php0071 小时前
猿辅导Java面试真实经历与深度总结(二)
java·开发语言·python·计算机网络·面试·职场和发展·golang
摇滚侠1 小时前
Spring Boot 3零基础教程,WEB 开发 内容协商机制 笔记34
java·spring boot·笔记·缓存