一篇文章说清楚并发多线程

并发与多线程

并发就是一并发生,也就是同时的概念,相互不影响的一同发生的任务就是并发

多线程很好理解,就是多个线程一同工作,相较于单线程,多线程就可以理解为一个任务多个人一起干活。

但是咱们做项目的人都知道,有些工作并不是人越多干的就越快,比如盖一栋房子,四个人同时砌东西南北四面墙肯定比一个人快,但是如果要搭二楼,就必须要一楼先完成,只能当成下一个任务再去完成。

多线程用于提高程序的效率和响应速度,适当的使用多线程可以优化资源的利用,提高软件性能

多线程的应用场景

1.并行处理数据:大规模计算或者图像视频处理

2.提高响应能力:处理多个客户端请求

3.利用多核处理器优势:充分利用机器资源

创建线程

继承Thread类,extends Thread

scala 复制代码
public class MyThread extends Thread {
    @Override
    public void run() {
       业务逻辑
    }
}

实现Runnable接口

typescript 复制代码
public class MyRunnable implements Runnable {
    @Override
    public void run() {
       业务逻辑
    }
}

实现callable接口,重写call方法可以通过future task获取任务的返回值

typescript 复制代码
public class CallerTask implements Callable<String> {
    public String call() throws Exception {
        return "Hello,i am running!";
    }

    public static void main(String[] args) {
        //创建异步任务
        FutureTask<String> task=new FutureTask<String>(new CallerTask());
        //启动线程
        new Thread(task).start();
        try {
            //等待执行完成,并获取返回结果
            String result=task.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

三种方式,实现runable接口较好,java不支持多重继承,集成线程类就不能继承其他类了,callable的话和runnable类似,区别就可以返回一个结果

获取线程返回值

实现的线程任务一般是交给线程池执行的,线程池中的ExecutorService方法可以执行提交的任务

callable

ini 复制代码
// 创建一个包含5个线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);

// 创建一个Callable任务
Callable<String> task = new Callable<String>() {
    public String call() {
        业务逻辑
        return 返回值
    }
};

// 提交任务到ExecutorService执行,并获取Future对象
Future[] futures = new Future[10];
for (int i = 0; i < 10; i++) {
    futures[i] = executorService.submit(task);
}

// 通过Future对象获取任务的结果
for (int i = 0; i < 10; i++) {
    System.out.println(futures[i].get());
}

// 关闭ExecutorService,不再接受新的任务,等待所有已提交的任务完成
executorService.shutdown();

Runnable

scss 复制代码
// 创建一个包含5个线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);

// 创建一个Runnable任务
Runnable task = new Runnable() {
    public void run() {
       业务逻辑
    }
};

// 提交任务到ExecutorService执行
for (int i = 0; i < 10; i++) {
    executorService.submit(task);
}

// 关闭ExecutorService,不再接受新的任务,等待所有已提交的任务完成
executorService.shutdown();

future

future就是获取callable任务的执行结果的接口,其中有5个方法

cancel:取消任务,成功返回true,错误返回false

iscancelled: 是否被取消成功,完成前被取消就是true

isDone:任务是否完成,完成为true

get:获取执行结果,阻塞,会一直等到任务执行完成

get(超时时间,时间单位):用来获取执行结果,指定时间内没获取就null

FutureTask

csharp 复制代码
public class FutureTask<V> implements RunnableFuture<V>

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

runnableFuture 继承了runnable和future两个接口,futureTask又实现了runnableFuture接口,所以可以被当作任务执行,又能有返回值

多线程风险

线程安全

原子性:经典的收付款例子,a转账给b必须是一个原子操作,这个操作中a失去了i元,b增加了i元,如果这个操作不是一个原子操作,在多线程情况下,用两个线程完成这件事情,a扣了钱,b没加,就出问题了

可见性:当多个线程访问同一个变量时,一个线程更改了这个变量,其他的线程立即能看见,每个人线程都有自己的工作内存,工作内存和主内存之间需要交互,就需要使用volatile关键字

锁问题

死锁:线程a等待b线程释放资源才能继续执行,b线程又需要等待a线程释放资源才能执行,形成互相等待的死锁

活锁:没有发生阻塞,但是a线程依赖b线程结束,b线程依赖a线程结束的,都在运行但是只能重复自身操作

性能

多线程不一定比单线程快,多线程创建线程和线程的上下文切换的开销

多线程的内存问题

对于每一个线程来说,栈是自己的,堆是共有的,共有的变量也就是共享变量,内存的可见性针对的就是堆中的共享变量

上图可以知道线程a无法访问b的工作内存,线程间的通信必须经过主存,但是线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接在主存中读取,所以每次的操作是先去主存中找到共享变量,读取共享变量的值拷贝到工作内存中,再次从本地读取

Java 内存模型 (JMM) 主要针对的是多线程环境下,如何在主内存与工作内存之间安全地执行操作的规则,涉及到可见性和原子性和指令重排

volatile关键字

指令重排

重排序是为了优化性能,进行的编译重新排序,但是对于数据依赖关系的操作不会进行重排序,单线程的执行结果也不会被改变。

原理和作用

volatile关键字禁止指令重排,也就是volatile修饰的变量语句前后的操作不允许串行,当程序执行到volatile修饰的变量时,其前面的操作更改一定全部执行结束,后面的操作一定还未执行

synchronized 关键字

synchronized 可以保证同一时刻只有一个线程执行某个方法或者某个代码块,可以保证一个线程的变化能被其他线程所看到

synchronized 应用

java 复制代码
public class AccountingSync implements Runnable {
    //共享资源(临界资源)
    static int i = 0;
    
    // synchronized 同步方法
    public synchronized void increase() {
        i ++;
    }
    
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
}

如果不加锁,因为i++不具备原子性,所以最终结果会小于实际应得的值

synchronized 可重入锁概念

synchronized属于可重入锁,即同一个线程能够多次获取同一个synchronized修饰的方法,获取同一个锁不会引起阻塞,释放次数要和获取锁次数一直避免死锁

synchronized 使用

csharp 复制代码
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    synchronized (this) {
        // code
    }
}

后面就不对锁过多分析了,锁相关知识会另起一篇进行分享

ThreadLocal

线程安全的问题核心就是多个线程会对同一个临界区的共享资源进行访问,threadlocal利用的就是空间换时间的方式,每个线程都设置了自己的线程的本地变量。

ThreadLocal 就是线程的"本地变量",即每个线程都拥有该变量的一个副本,达到人手一份的目的,这样就可以避免共享资源的竞争

set源码

scss 复制代码
public void set(T value) {
	//1. 获取当前线程实例对象
    Thread t = Thread.currentThread();

	//2. 通过当前线程实例获取到ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);

    if (map != null)
	   //3. 如果Map不为null,则以当前ThreadLocal实例为key,值为value进行存入
       map.set(this, value);
    else
	  //4.map为null,则新建ThreadLocalMap并存入value
      createMap(t, value);
}

get源码

java 复制代码
public T get() {
  //1. 获取当前线程的实例对象
  Thread t = Thread.currentThread();

  //2. 获取当前线程的ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null) {
	//3. 获取map中当前ThreadLocal实例为key的值的entry
    ThreadLocalMap.Entry e = map.getEntry(this);

    if (e != null) {
      @SuppressWarnings("unchecked")
	  //4. 当前entitiy不为null的话,就返回相应的值value
      T result = (T)e.value;
      return result;
    }
  }
  //5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value
  return setInitialValue();
}

remove源码

csharp 复制代码
public void remove() {
	//1. 获取当前线程的ThreadLocalMap
	ThreadLocalMap m = getMap(Thread.currentThread());
 	if (m != null)
		//2. 从map中删除以当前ThreadLocal实例为key的键值对
		m.remove(this);
}

threadLocalMap是threadLocal类的静态内部类,是专门用于保存每个线程中的线程局部变量

线程池

线程池的作用主要就是用来复用资源的,java是通过threadPoolExecutor来创建线程池的,通过传参即可创建线程池

  1. corePoolSize:核心线程数
  2. maximumPoolSize:最大线程数
  3. keepAliveTime:线程最大的存活时间
  4. unit:时间单位
  5. workQueue:任务阻塞队列
  6. threadFactory:线程池内部创建线程的工厂
  7. handler:拒绝策略,当队列已满并且线程数量达到最大线程数量时,会调用方法处理任务

线程池示例

当有线程通过execute方法提交一个任务,首先会判断当前线程池的线程数是否小于核心线程数,小于就直接创建一个 如果线程池里的线程数已经满足核心线程数就进入阻塞队列 队列满了,这时线程池里的线程数小于最大线程数,就会创建非核心线程执行提交的任务 注意:就算队列中有任务,新创建的线程还是会优先处理提交的任务,而不是从队列中获取已有的任务执行

当线程数达到最大线程数,再进入任务时,就会执行拒绝策略 默认的拒绝策略是AbortPolicy(丢弃任务,抛出异常)其余的策略分别是CallerRunsPolicy(提交的线程自己执行任务)、DiscardPolicy(丢弃任务,不抛出异常)、DiscardOlderPolicy(从队列中最先进入队列的任务,再次提交任务)

execute执行过程

scss 复制代码
public void execute(Runnable command) {
    // 首先检查提交的任务是否为null,是的话则抛出NullPointerException。
    if (command == null)
        throw new NullPointerException();

    // 获取线程池的当前状态(ctl是一个AtomicInteger,其中包含了线程池状态和工作线程数)
    int c = ctl.get();

    // 1. 检查当前运行的工作线程数是否少于核心线程数(corePoolSize)
    if (workerCountOf(c) < corePoolSize) {
        // 如果少于核心线程数,尝试添加一个新的工作线程来执行提交的任务
        // addWorker方法会检查线程池状态和工作线程数,并决定是否真的添加新线程
        if (addWorker(command, true))
            return;
        // 重新获取线程池的状态,因为在尝试添加线程的过程中线程池的状态可能已经发生变化
        c = ctl.get();
    }

    // 2. 尝试将任务添加到任务队列中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 双重检查线程池的状态
        if (! isRunning(recheck) && remove(command))  // 如果线程池已经停止,从队列中移除任务
            reject(command);
        // 如果线程池正在运行,但是工作线程数为0,尝试添加一个新的工作线程
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 3. 如果任务队列满了,尝试添加一个新的非核心工作线程来执行任务
    else if (!addWorker(command, false))
        // 如果无法添加新的工作线程(可能因为线程池已经停止或者达到最大线程数限制),则拒绝任务
        reject(command);
}

示意图

线程池的监控

  1. getCompletedTaskCount 已经完成的任务数量
  2. getLargestPoolSize 线程池里曾经创建过的线程数量,用来判断是否满过
  3. getActiveCount 获取正在执行的任务线程数
  4. getPoolSize:获取当前线程池中线程数量

全局线程池和局部线程池

线程池的配置示例

线程池异步任务执行

网络请求处理:

在Web服务器、API服务等场景中,线程池用于处理高并发的HTTP请求,确保每个请求都能得到及时响应,而不会因为频繁创建和销毁线程导致性能下降。

解决思路,首先将本系统的请求处理接口改为异步,接口不用再等待第三方处理完成就返回,而是直接返回,再将异步任务执行

解决方案:消息队列,将请求全部入库然后通过线程池执行后续任务

消息队列消费:

消费者应用使用线程池从消息队列中拉取消息并进行处理,实现消息的异步解耦和高效消费。

定时任务调度:

如心跳请求、定期数据同步、报表生成等,线程池可以周期性地执行这些任务,避免单个任务阻塞主线程。

线程池并发数据处理:

批处理:

对大量数据进行批量化处理,如数据库批量插入、文件批量读写、图片批量处理等,线程池可分配多个工作线程并行处理数据块,显著加快处理速度。

数据分析与计算:

在大数据处理、科学计算、机器学习等领域,线程池用于分发计算任务到多个线程,利用多核CPU加速计算过程。

线程池资源密集型操作:

I/O密集型任务:

如文件读写、网络通信等,线程池能够有效地利用线程等待I/O操作完成期间的空闲时间,调度其他任务执行,减少线程阻塞带来的资源浪费。

CPU密集型任务:

对于计算密集型操作,线程池可以限制并发度,防止过度消耗CPU资源导致系统响应变慢,同时通过合理设置线程数量,使CPU核心得到充分利用。

线程池服务端应用:

数据库连接池:

虽然不是直接的线程池,但数据库连接池的思想与线程池相似,都是为了复用资源,避免频繁创建和销毁连接,提高数据库操作的性能和稳定性。

分布式系统:

在分布式服务架构中,线程池常用于服务端接收和处理来自客户端或其他服务节点的请求,保证服务的高吞吐量和低延迟。

线程池实现邮件发送:

将邮件发送任务放入线程池,使其在后台异步执行,不影响主线程的正常流程,提高用户体验。

线程池实现缓存刷新:

定期或触发式刷新缓存数据的任务可以放入线程池,避免阻塞业务逻辑。

使用线程池进行性能优化:

避免频繁创建销毁线程的开销:线程的创建和销毁涉及到系统资源分配和回收,是非常耗时的操作。线程池通过池化技术,复用已创建的线程,降低这部分开销。

使用线程池控制并发级别:

通过配置线程池大小,可以限制系统同时运行的任务数量,防止因过度并发导致的资源竞争和系统过载。

相关推荐
指令集梦境1 小时前
Cursor + Spring Boot实战:从零写一个RESTful API
spring boot·后端·restful
码云之上1 小时前
聊聊如何设计一个高效、稳定的 Node.js 接入层
前端·后端·node.js
IT_陈寒2 小时前
Vite项目build后路由404了?你可能漏了这个小配置
前端·人工智能·后端
宸津-代码粉碎机3 小时前
Spring AI企业级实战|从RAG优化到Agent多工具调度
java·大数据·人工智能·后端·python·spring
吴佳浩3 小时前
AI Infra 的真相:Go 没输,rust也不是取代
后端·rust·go
喵个咪3 小时前
实时游戏网络协议深度对比:KCP vs WebRTC vs WebSocket
后端·websocket·webrtc
普通网友3 小时前
springboot之集成Elasticsearch
spring boot·后端·elasticsearch
QuZero3 小时前
Guava Cache Deep Dive
java·后端·算法·guava
leeyi4 小时前
SSE 实时推流 —— Token 怎么一个个蹦出来
后端·agent
leeyi4 小时前
ReAct 循环的 50 行 Go 实现,逐行拆解
后端·agent