Java线程/线程池运行原理

Java中线程的五种创建方式

继承Thread类

步骤:

  • 创建一个继承于Thread类的子类
  • 重写Thread类的run()-> 将此线程执行的操作声明在run()方法中
  • 创建Thread类的子类的对象
  • 通过此对象调用start()执行线程

示例代码:

java 复制代码
package atguigu.java;

//1.创建一个继承于Thread类的子类
class MyThread extends Thread {
    //2.重写Thread类的run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}


public class ThreadTest {
    public static void main(String[] args) {
        //3.创建Thread类的子类的对象
        MyThread t1 = new MyThread();

        //4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
        t1.start();

        /*问题一:我们不能通过直接调用run()的方式启动线程,
        这种方式只是简单调用方法,并未新开线程*/
        //t1.run();

        /*问题二:再启动一个线程,遍历100以内的偶数。
        不可以还让已经start()的线程去执行。会报IllegalThreadStateException*/
        //t1.start();

        //重新创建一个线程的对象
        MyThread t2 = new MyThread();
        t2.start();

        //如下操作仍然是在main线程中执行的。
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i + "***********main()************");
            }
        }
    }
}

Java匿名线程Thread的三种方式

java 复制代码
/*
* 匿名Thread几种方法:
* 1) 重写run方法:通过new Thread(){}.start() 直接new一个实例后实现并start, 实现里面重写run方法
* 2) 调用runable接口:new Thread( new Runable(){} ).start()
* 3) 通过lambda传参给Thread: new Thread( () -> {}).start()
*
* Task:
* "yyyy-MM-dd HH:mm:ss"的格式打印当前系统时间,总循环10次,每1s调用一次
* */

import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.SimpleTimeZone;

public class useLib {
    // 1 通过lambda,最简洁
    public static void main(String[] args) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yy-MM-dd HH:mm:ss");
        new Thread(()->{
                    for(int i = 0; i < 10; i++) {
                        System.out.println("<M1" + dtf.format(LocalDateTime.now()));
                        try{
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
        }
        ).start();

        // 2 通过run方法,最直接
        System.out.println("Method 2: by run method");
        SimpleDateFormat d = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i ++) {
                    String str = d.format(new Date());
                    System.out.println(">M2" + str);
                    try{
                        Thread.sleep(100);
                    }catch(InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

       //3 通过runable接口,内部仍然需要覆盖run方法
        System.out.println("Method3: by runable interface");
        DateTimeFormatter dt = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
        new print().print(dt.format(LocalDateTime.now()));
        new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < 10; i++) {
                           new print().print("[M3 " + dt.format(LocalDateTime.now()));
                           try{
                               Thread.sleep(100);
                           }catch (InterruptedException e){
                               e.printStackTrace();
                           }
                        }
                    }
                }
        ).start();


    }
}

class print{
    public void print(String str) {
        System.out.println(str);
    }
}

Runnable 创建线程

java 复制代码
class Calculate implements Runnable {    //实现接口
    int sum = 0;
    int i = 1;

    @Override
    public void run() {
        Thread thread = Thread.currentThread(); //获取当前线程
        while (i <= 10) {
            sum = sum + i;
            if (i == 5) {
                Thread threadTwo = new Thread(this);
                threadTwo.setName("线程2");
                threadTwo.start();
                i++;    //因为没有机会执行下面的i++,所以要预先执行
                System.out.println(thread.getName()+"计算前半段的结果是:" + sum);
                return; //线程1进入死亡状态
            }
            i++;
        }
        System.out.println(thread.getName()+"接线程1计算到的结果是" + sum);
    }
}

public class Example15_4 {
    public static void main(String[] args) {
        Thread threadOne = new Thread(new Calculate());
        threadOne.setName("线程1");
        threadOne.start();
    }
}

从代码中可以看出来无论是Thread还是Runnable都需要重写run()方法,另外还有一点就是即便是实现了Runnable还是需要在new Thread才可以使用。

那么使用继承Thread和通过实现Runnable这两种方式创建线程有什么区别呢? 一般来说我们都会使用Runnable创建线程,因为Java中是单继承,多实现,如果我们在类中继承了Thread可能会对class的扩展带来限制,所以一般来说使用Runnable 来创建对象。

通过实现Callable接口创建线程

步骤:

  1. 创建一个实现Callable的实现类
  2. 实现call方法,将此线程需要执行的操作声明在call中(注意,和Thread和Runnable是实现不同的方法的
  3. 创建Callable接口实现类的对象
  4. 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask对象
  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Treahd对象,并调用start()
  6. 获取Callable中call方法的返回值

实现Callable接口的方式创建线程的强大之处

call()可以有返回值

call()可以抛出异常,被外面的操作捕获,获取异常的信息

Callable是支持泛型的

java 复制代码
package com.atguigu.java2;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

//1.创建一个实现Callable的实现类
class NumThread implements Callable {
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        //把100以内的偶数相加
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

public class ThreadNew {
    public static void main(String[] args) {    
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();

        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);

        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();

        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}

那么为什么使用Callable创建的线程就可以获取到返回值呢?

原理

  1. FutureTask的类图如下
  1. Future接口中包含获取返回值结果的方法get如果线程没有执行完任务,调用该方法会阻塞当前线程,以及取消执行任务cancel,查看任务是否执行完毕isDone,以及任务是否取消isCancelled
macOS 复制代码
public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
  1. RunnableFuture接口相当于整合Future和Runnable接口,没有添加其他功能
java 复制代码
public interface RunnableFuture<V> extends Runnable, Future<V> {
   
    void run();
}

具体的原理是这样的,FutureTask中有两个很关键的属性,一个是callable接口,一个是存放call方法的返回值outcome属性。

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

    private Callable<V> callable;

    private Object outcome; // non-volatile, protected by state reads/writes
    
    ...
}

FutureTask中run方法代码如下:

java 复制代码
    public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

run方法执行了Callable的call方法,然后将返回值赋值给了成员变量。如果发生了异常,进入catch代码块,调用setException方法,exception方法如下:

java 复制代码
    protected void setException(Throwable t) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = t;
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }

这里将异常对象赋值给outcome,所以,如果成功执行call方法,那么outcome中保存的返回值,如果执行失败,那么outcome中保存的是异常对象

然后,通过get方法获取返回值,get方法具体逻辑如下:

java 复制代码
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }
    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }

get方法会判断此时callable任务的状态,如果没有完成,那么阻塞当前线程,等待完成,如果处于已经取消状态直接抛出异常,如果已经执行完毕,判断是否是异常,如果是异常对象,通过ExecutionException包装,然后抛出,如果是执行的结果,直接返回。

FutureTask接口实现的比较复杂,阅读源码理解起来相对困难,但是本质上,FutureTask接口是一个生产者消费者模式,如果生产者没有生产完,那么会阻塞消费者,将消费者放到一个阻塞队列中,生产者生产完后,会唤醒阻塞的消费者去消费结果,大概原理就是这样,下面是一个简易版的实现。

java 复制代码
class MyRunnableFuture<T> implements RunnableFuture<T> {

  private Callable<String> callable;

  private Object returnObj;

  private ReentrantLock lock = new ReentrantLock();

  private Condition getCondition = lock.newCondition();

  public MyRunnableFuture(Callable<String> callable) {
      this.callable = callable;
  }

  @SneakyThrows
  @Override
  public void run() {
      this.returnObj = this.callable.call();
      try {
          lock.lock();
          this.getCondition.signalAll();
      } finally {
          lock.unlock();
      }
  }

  @Override
  public boolean cancel(boolean mayInterruptIfRunning) {
      throw new NotImplementedException();
  }

  @Override
  public boolean isCancelled() {
      throw new NotImplementedException();
  }

  @Override
  public boolean isDone() {
      throw new NotImplementedException();
  }

  @Override
  public T get() throws InterruptedException, ExecutionException {
      if (returnObj == null) {
          try {
              lock.lock();
              this.getCondition.await();
          } finally {
              lock.unlock();
          }
      }
      return (T) returnObj;
  }

  @Override
  public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
      throw new NotImplementedException();
  }
}

使用线程池

线程池优点:

  1. 提高响应速度(减少了创建线程的时间)
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  3. 便于线程管理

核心参数:

corePoolSize: 核心线程数量

maximumPoolSize: 最大线程数

keepAliveTime: 非核心线程空闲最长时间

步骤:

1.以方式二或方式三创建好实现了Runnable接口的类或实现Callable的实现类

2.实现run或call方法

3.创建线程池

4.调用线程池的execute方法执行某个线程,参数是之前实现Runnable或Callable接口的对象

java 复制代码
package com.atguigu.java2;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

class NumberThread implements Runnable {
    @Override
    public void run() {
        //遍历100以内的偶数
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable {
    @Override
    public void run() {
        //遍历100以内的奇数
        for (int i = 0; i <= 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}


public class ThreadPool {

    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);

        //输出class java.util.concurrent.ThreadPoolExecutor
        System.out.println(service.getClass());

        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //自定义线程池的属性
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();

        //2. 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适用于Runnable
        service.execute(new NumberThread1());//适用于Runnable
//        service.submit(Callable callable);//适合使用于Callable

        //3. 关闭连接池
        service.shutdown();
    }

}

使用匿名内部类,上面的代码中已经展示了

线程类型

Java中线程分为: 主线程、子线程两种

主线程: 即main方法

子线程:非主线程皆为子线程

子线程又分为:守护线程、非守护线程

  • 守护线程: 主要指为主线程提供通用服务的线程,比如GC线程。主线程一旦结束或被销毁,守护线程也同步结束和销毁。
  • 非守护线程:即用户线程,通常异步处理一些业务逻辑。用户线程本质上是我们自己创建的线程,通过start()方法启动。通过线程的setDaemo(true)方法,可以设置非守护线程为守护线程。

线程状态

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为"运行"。 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。

线程的状态图

  1. 初始状态(NEW) 实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

2.1. 就绪状态(RUNNABLE之READY) 就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。 调用线程的start()方法,此线程进入就绪状态。 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。 锁池里的线程拿到对象锁后,进入就绪状态。 2.2. 运行中状态(RUNNABLE之RUNNING) 线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。

  1. 阻塞状态(BLOCKED) 阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

  2. 等待(WAITING) 处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

  3. 超时等待(TIMED_WAITING) 处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

  4. 终止状态(TERMINATED) 当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。 在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

线程池的创建

通过上面的线程池创建线程,了解到使用线程池进行线程管理,可以减少线程的创建与销毁的开销,那下面我们就来看看

线程池的创建分为两大类方法

  • 通过Executors自动创建
  • 通过ThreadPoolExecutor手动创建

Executors创建线程的方式 - 6种

  1. newFixedThreadPool:创建一个固定大小的线程池
csharp 复制代码
public class ThreadPool1 {
    public static void main(String[] args) {
        //1.创建一个大小为5的线程池
        ExecutorService threadPool= Executors.newFixedThreadPool(5);
        //2.使用线程池执行任务一
        for (int i=0;i<5;i++){
            //给线程池添加任务
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程名"+Thread.currentThread().getName()+"在执行任务1");
                }
            });
        }
        //2.使用线程池执行任务二
        for (int i=0;i<8;i++){
            //给线程池添加任务
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程名"+Thread.currentThread().getName()+"在执行任务2");
                }
            });
        }

    }
}

创建一个线程池,该线程池重用在共享的无界队列上运行的固定数量的线程。在任何时候,最多 nThreads 线程都是活动的处理任务。如果在所有线程都处于活动状态时提交了其他任务,则这些任务将在队列中等待,直到线程可用。如果任何线程在关闭前的执行过程中由于故障而终止,则如果需要执行后续任务,将有一个新线程来代替它。池中的线程将一直存在,直到显式关闭。

这个线程池中的队列是没有限制的,不安全

  1. newCachedThreadPool:带缓存的线程池,适用于短时间有大量任务的场景,但有可能会占用更多的资源;线程数量随任务量而定。
arduino 复制代码
public class ThreadPool3 {
    public static void main(String[] args) {
        //创建线程池
        ExecutorService service= Executors.newCachedThreadPool();
        //有50个任务
        for(int i=0;i<50;i++){
            int finalI = i;
            service.submit(()->{
                System.out.println(finalI +"线程名"+Thread.currentThread().getName());//线程名有多少个,CPU就创建了多少个线程
            });
        }
    }
}

创建一个线程池,该线程池根据需要创建新线程,但在以前构造的线程可用时将重用这些线程。这些池通常会提高执行许多短期异步任务的程序的性能。要执行的调用将重用以前构造的线程(如果可用)。如果没有可用的现有线程,则将创建一个新线程并将其添加到池中。60 秒内未使用的线程将被终止并从缓存中删除。因此,保持空闲足够长时间的池不会消耗任何资源。请注意,可以使用 ThreadPoolExecutor 构造函数创建具有相似属性但详细信息不同的池(例如,超时参数)。

这里面允许的最多线程是无线的(int最大值),使用起来肯定是有风险的

  1. newSingleThreadExecuto:创建单个线程的线程池

创建单个线程的线程池?为啥不直接创个线程?

  • 线程池的优点:
  • 1.复用线程:不必频繁创建销毁线程
  • 2.也提供了任务队列和拒绝策略
java 复制代码
public class ThreadPool4 {
    public static void main(String[] args) {
        ExecutorService service= Executors.newSingleThreadExecutor();
        for (int i=0;i<5;i++){
            int finalI = i;
            service.submit(()->{
                System.out.println(finalI +"线程名"+Thread.currentThread().getName());//CPU只创建了1个线程,名称始终一样
            });
        }
    }
}
  1. newSingleThreadScheduledExecutor:创建执行定时任务的单个线程的线程池
java 复制代码
public class ThreadPool5 {
    public static void main(String[] args) {
        ScheduledExecutorService service= Executors.newSingleThreadScheduledExecutor();
        System.out.println("添加任务:"+ LocalDateTime.now());
        service.schedule(new Runnable() {

                @Override
                public void run() {
                    System.out.println("执行任务:"+LocalDateTime.now());
                }
            },3,TimeUnit.SECONDS);//推迟3秒执行任务
    }
}

创建一个单线程执行程序,该执行程序可以计划命令在给定延迟后运行或定期执行。(但请注意,如果此单个线程在关闭前的执行过程中由于失败而终止,则如果需要执行后续任务,将有一个新线程来代替它。任务保证按顺序执行,并且在任何给定时间都不会超过一个任务处于活动状态。与其他等效 newScheduledThreadPool(1) 项不同,返回的执行器保证不会被重新配置为使用其他线程。

  1. newScheduledThreadPool:创建执行定时任务的线程池
java 复制代码
/**
 * 创建执行定时任务的线程池
 */
public class ThreadPool6 {
    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newScheduledThreadPool(5);//5个线程
        System.out.println("添加任务:" + LocalDateTime.now());
        //once(service);
        many(service);
    }
    /**
     * 执行一次的定时任务
     */
    public static void once(ScheduledExecutorService service) {
        service.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("执行任务:"+ LocalDateTime.now());
            }
        },3, TimeUnit.SECONDS);//推迟3秒执行
    }
}
  1. newWorkStealingPool:根据当前设备的配置自动生成线程池
java 复制代码
public class ThreadPool7 {
    public static void main(String[] args) {
        ExecutorService service= Executors.newWorkStealingPool();
        for(int i=0;i<50;i++){
            int finalI = i;
            service.submit(()->{
                System.out.println(finalI +"线程名"+Thread.currentThread().getName());//线程名有多少个,CPU就创建了多少个线程
            });
        }
        //创建的为守护线程,JVM不会等待守护线程结束
        while (!service.isTerminated()){

        }
    }
}

ThreadPoolExecutor------7.手动创建线程池

这也是我们在项目中创建线程池中使用的创建方式。

  • 核心(最少)线程数
  • 最大线程数
  • 闲置可存活时间
  • 描述(闲置可存活时间)的单位
  • 任务队列
  • 线程工厂
  • 拒绝策略有5种:
java 复制代码
         //1.提示异常,拒绝执行多余的任务
        // new ThreadPoolExecutor.AbortPolicy()
        
        //2.忽略堵塞队列中最旧的任务
         //new ThreadPoolExecutor.DiscardOldestPolicy()

         //3.忽略最新的任务
         //new ThreadPoolExecutor.DiscardPolicy()

         //4.使用调用该线程池的线程来执行任务
         //new ThreadPoolExecutor.CallerRunsPolicy()

         //5.自定义拒绝策略
         new RejectedExecutionHandler()
相关推荐
李老头探索1 小时前
Java面试之Java中实现多线程有几种方法
java·开发语言·面试
测试199813 小时前
2024软件测试面试热点问题
自动化测试·软件测试·python·测试工具·面试·职场和发展·压力测试
马剑威(威哥爱编程)14 小时前
MongoDB面试专题33道解析
数据库·mongodb·面试
独行soc16 小时前
#渗透测试#SRC漏洞挖掘#深入挖掘XSS漏洞02之测试流程
web安全·面试·渗透测试·xss·漏洞挖掘·1024程序员节
理想不理想v16 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
sszmvb123417 小时前
测试开发 | 电商业务性能测试: Jmeter 参数化功能实现注册登录的数据驱动
jmeter·面试·职场和发展
测试杂货铺17 小时前
外包干了2年,快要废了。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
王佑辉17 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
真忒修斯之船17 小时前
大模型分布式训练并行技术(三)流水线并行
面试·llm·aigc
ZL不懂前端18 小时前
Content Security Policy (CSP)
前端·javascript·面试