Java多线程1--基本使用,全文12000余字,图文并茂

Java多线程

  • 一级目录
  • Java多线程
    • 1.多线程的引入
      • [1.1 引入的原因](#1.1 引入的原因)
      • [1.2 线程的概念](#1.2 线程的概念)
      • [1.3 线程与进程之间的区别](#1.3 线程与进程之间的区别)
    • 2.创建线程的5中方法
      • [2.1 方法一:继承Thread,重写run](#2.1 方法一:继承Thread,重写run)
      • [2.2 方法二:实现Runnable,重写run](#2.2 方法二:实现Runnable,重写run)
      • [2.3 方法三:方法一+匿名内部类](#2.3 方法三:方法一+匿名内部类)
      • [2.4 实现Runnable,重写run+匿名内部类](#2.4 实现Runnable,重写run+匿名内部类)
      • [2.5 针对方法3,4引入lambda表达式](#2.5 针对方法3,4引入lambda表达式)
    • [3. Thread类以及常见方法](#3. Thread类以及常见方法)
      • [3.1 Thread类的概念](#3.1 Thread类的概念)
      • [3.2 Thread常见的构造方法](#3.2 Thread常见的构造方法)
      • [3.3 Thread类中常见的几个属性](#3.3 Thread类中常见的几个属性)
        • [3.3.1. ID:](#3.3.1. ID:)
        • [3.3.2. isDaemon()](#3.3.2. isDaemon())
        • [3.3.3. isAlive():用来判断线程是否存活](#3.3.3. isAlive():用来判断线程是否存活)
        • [3.3.4. isInterrupted()判断线程是否被中断](#3.3.4. isInterrupted()判断线程是否被中断)
    • [4. 线程的启动,中断,等待](#4. 线程的启动,中断,等待)
      • [4.1 线程的启动](#4.1 线程的启动)
      • [4.2 线程的中断](#4.2 线程的中断)
      • [4.3 线程的等待](#4.3 线程的等待)
      • [4.4 线程的休眠](#4.4 线程的休眠)

一级目录

二级目录

三级目录

Java多线程

1.多线程的引入

1.1 引入的原因

  1. 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU,与多核cpu相配套的是并发编程,来提高cpu的利用率
  2. 虽然多进程也能实现 并发编程, 但是线程比进程更轻量 .
    • 创建线程比创建进程更快.
    • 销毁线程比销毁进程更快.
    • 调度线程比调度进程更快.
  3. 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池" 和 "协程"

1.2 线程的概念

一个线程就是一个执行流 . 每个进程都包含一个或者多个线程。每个线程之间都可以按照顺序执行自己的代码. 多个线程之间同时执行着多份代码.

1.3 线程与进程之间的区别

既然线程和进程都可以完成并发编程,那么,他们之间有什么区别吗?

  1. 进程包含线程,一个进程包含一个或者多个线程
  2. 进程是系统资源分配的基本(最小)单位,而线程是cpu调度的基本(最小)单位
    a.对于前半句的解释:进程和进程之间所涉及到的资源是各自独立的(cpu,内存,硬盘资源,网络带宽等等),这些进程的创建,销毁都需要申请资源,线程不需要吗?
    对于线程,只是第一个线程创建的时候需要申请资源,后续在创建线程不涉及资源申请操作,而且只有所有线程都销毁了才能真正释放资源。
    b.对于后半句的解释:若一个进程包含多个线程,此时,多个线程之间是各自去cpu上调度执行的,比如有线程1,2,3 很有可能线程1去cpu核心1上面去执行,2去核心2执行,3去核心3去执行,也就是并行执行,在这期间伴随着这几个线程在cpu核心上的来回切换(具体实现由操作系统中的调度器 完成,程序员无法干预)

  3. 一个进程挂了一般不会影响到其他进程. 但是一个线程挂了, 可能把同进程内的其他线程一起带走(整
    个进程崩溃).
  4. 进程和进程 之间不共享 内存空间. 同一个进程的线程 之间共享 同一个内存空间.

2.创建线程的5中方法

2.1 方法一:继承Thread,重写run

java 复制代码
class MyThread extends Thread{
    @Override
    public void run(){
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

}

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

运行结果:

我们借助第三方调试工具,可以看到:

注意:

  1. sleep是静态方法,目的是让线程暂时放弃cpu,休息一会儿,过了指定时间再去执行

  2. 在执行结果中,hello main与hello thread有先有后,因为:多个线程调度顺序是随机的(操作系统剥夺式调度线程),这两个线程谁先谁后执行都有可能,无法预测。

  3. t.start()与t.run()的区别:

    a. t.start()是在系统中真正创建出来一个线程

    b. t.run()这个操作没有创建线程,只是调用了刚才重写MyThread类中的run方法,整个系统中,只有main线程

  4. MyThread类中,重写的run方法:是线程的入口方法,新的线程启动了,就自动执行这里面的代码,run不需要我们手动调用,即新的线程创建好了之后,自动去执行。

    java 复制代码
    class MyThread extends Thread{
        @Override
        public void run(){
            while(true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    
    }
    
    public class Demo1 {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new MyThread();
    //        t.start();
            t.run();
            while(true){
                System.out.println("hello main");
                Thread.sleep(1000);
            }
        }
    }

    如上的代码所述,只是把t.start()替换为t.run()

    借助线程调试工具,前台进程中只能看到main进程

    在这里面没有看到我们之前的线程t,说明t.run()方法不能创建线程

    ,t.run()只能调用重写的run()函数,因此我们就看到了在控制台中仅仅打印hello thread,而且在调试工具中没有看懂线程Thread-0(我们刚才创建的线程t)的结果啦!

2.2 方法二:实现Runnable,重写run

java 复制代码
class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
        t.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

注意:

  1. 我们用个比喻来说明将runnable对象传参给Thread的构造方法的这一过程

    → 1.你写好了工作清单(MyRunnable重写run())

    → 2. 把清单打印出来(new MyRunnable()创建任务对象)

    → 3. 把清单交给工人(new Thread(runnable))

    → 4. 喊工人开始干活(t.start())

    → 5. 工人照着清单做事(执行run()里的循环打印)

    Runnable是任务载体,重写run()方法定义线程要执行的逻辑;

    new Thread(runnable)的核心是给线程绑定执行任务 ,让线程知道启动后该做什么;
    线程调用start()后 ,才会真正执行 Runnable中run()方法的代码,而非直接调用run()(直接调用 run () 只是普通方法执行,不会创建新线程)

  2. 为什么要把任务和线程分离?

    Java 这么设计,是为了解耦 ------ 这样的话,我们可以:

    同一个Runnable任务,传给多个Thread对象,让多个线程执行相同的任务;

    java 复制代码
    // 示例:多个线程执行同一个任务
    Runnable task = new MyRunnable();
    Thread t1 = new Thread(task); // 工人1执行这个任务
    Thread t2 = new Thread(task); // 工人2执行同一个任务
    t1.start();
    t2.start(); // 两个线程都会打印"hello thread"

    灵活替换任务比如后续想改线程执行的逻辑,只需要改Runnable的实现,不用动Thread的代码

  3. 异常分为受查异常和非受查异常,而InterruptedException属于受查异常,即编译时必须处理 (要么 catch 捕获,要么 throws 声明抛出)

    抛出的异常必须满足:子类重写方法抛出的受查异常 ≤ 父类方法声明的受查异常 (要么不抛,要么抛子类异常,不能抛父类没有声明的新受查异常)。

    这里的关键是:InterruptedException 是受查异常(编译时必须处理,要么 catch 要么 throws),而 RuntimeException 是非受查异常(编译时不用处理)。

    这里父类Runnablemei没有抛出受查异常,子类MyRunnable也不能抛出父类没有声明的新受查异常

    这里的解决方法就是:

    我们必须要在 run() 方法内部 catch 住 InterruptedException,然后把它包装成 RuntimeException 抛出

    (这是一个非受查异常父类没声明也能抛

2.3 方法三:方法一+匿名内部类

java 复制代码
public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run(){
                while(true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }

            }
        };
        t.start();

        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }

    }
}

运行结果为:

注意:

  1. Thread t = new Thread(){};做了三件事情
    1. 创建了一个Thread的子类,子类是匿名的
    2. {}里面就可以编写子类的定义代码。子类里面的方法,属性,重写的方法。。。
    3. 创建了这个匿名内部类的实例,并且把实例的引用赋值给t.
  2. 使用匿名内部类的好处?
    这样可以少定义一些类了,如果某些代码是一次性的,就可以使用匿名内部类

2.4 实现Runnable,重写run+匿名内部类

java 复制代码
public class Demo4 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        Thread t = new Thread(runnable);
        t.start();
        while(true){
            try {
                System.out.println("hello main");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

运行结果:

2.5 针对方法3,4引入lambda表达式

java 复制代码
public class Demo5 {
    public static void main(String[] args) {
        //lamda表达式
        Thread t = new Thread(()->{
            while(true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

        });
        t.start();
        while(true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

运行结果:

lambda表达式:本质就是一个匿名函数 ,最主要的用途是作为回调函数,格式为()->{},这样,创建了匿名的函数式接口的子类 ,并且创建出对应的实例,并且重写了里面的方法

3. Thread类以及常见方法

3.1 Thread类的概念

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联

3.2 Thread常见的构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用 Runnable 对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名
【了解】Thread(ThreadGroup group, Runnable target) 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可
java 复制代码
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

3.3 Thread类中常见的几个属性

这几个属性的使用方法

java 复制代码
public static void main(String[] args) throws InterruptedException {
        Thread myThread = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println("线程被中断:" + e.getMessage());
            }
            System.out.println("线程执行完毕");
        }, "我的测试线程"); // 给线程命名

        // 2. 设置线程为后台线程(可选,演示isDaemon())
        myThread.setDaemon(true);
        // 设置线程优先级(演示getPriority())
        myThread.setPriority(Thread.MAX_PRIORITY);

        // 3. 启动线程
        myThread.start();

        // 4. 主线程休眠一下,确保myThread进入运行状态
        Thread.sleep(500);

        // ========== 演示六个属性的获取方法 ==========
        System.out.println("1. 线程ID: " + myThread.getId());
        System.out.println("2. 线程名称: " + myThread.getName());
        System.out.println("3. 线程状态: " + myThread.getState());
        System.out.println("4. 线程优先级: " + myThread.getPriority());
        System.out.println("5. 是否后台线程: " + myThread.isDaemon());
        System.out.println("6. 是否存活: " + myThread.isAlive());
        System.out.println("7. 是否被中断: " + myThread.isInterrupted());

        // 演示中断线程,观察isInterrupted()的变化
        myThread.interrupt();
        System.out.println("中断线程后,是否被中断: " + myThread.isInterrupted());

        // 等待myThread执行结束
        myThread.join();
        System.out.println("线程结束后,是否存活: " + myThread.isAlive());
        System.out.println("线程结束后,状态: " + myThread.getState());
    }

运行结果:

上述的注意事项:

3.3.1. ID:

是java中给每个运行的线程分配id标识线程身份的效果,类似与PID

3.3.2. isDaemon()

这是判断是否为守护线程(后台线程)

为了理解这个概念,我们先看一下如下的代码

java 复制代码
public static void main(String[] args) {
	        Thread t1 = new Thread(()->{
	            while(true){
	                System.out.println("hello 1");
	                try {
	                    Thread.sleep(1000);
	                } catch (InterruptedException e) {
	                    throw new RuntimeException(e);
	                }
	            }
	        },"t1");
	        t1.start();
	        Thread t2 = new Thread(()->{
	            while(true){
	                System.out.println("hello 2");
	                try {
	                    Thread.sleep(1000);
	                } catch (InterruptedException e) {
	                    throw new RuntimeException(e);
	                }
	            }
	        },"t2");
	        t2.start();
	
	        Thread t3 = new Thread(()->{
	            while(true){
	                System.out.println("hello 3");
	                try {
	                    Thread.sleep(1000);
	                } catch (InterruptedException e) {
	                    throw new RuntimeException(e);
	                }
	            }
	        },"t3");
	        t3.start();
	
	        while(true){
	            System.out.println("hello main");
	            try {
	                Thread.sleep(1000);
	            } catch (InterruptedException e) {
	                throw new RuntimeException(e);
	            }
	        }
	    }

运行结果:

在这个代码中虽然main线程结束了,但是线程t1,t2,t3仍然存在,所以进程仍然存在,用下面的调试工具也可以验证上述结果

像t1,t2,t3这样的线程的存在,就能够影响到进程继续存在 ,这样的线程,就称为前台线程 ,上图所示中,除了t1,t2,t3之外,他们的存在不影响线程结束 ,这样的线程称为后台线程

|--------------------------------------------------------|
| 总结一下:咱们自己代码创建的线程,包括main主线程默认都是前台线程,可以通过setDaemon方法来修改! |

3.3.3. isAlive():用来判断线程是否存活

我们先来看一下下面的代码

java 复制代码
	public static void main(String[] args) {
        Thread t = new Thread(()->{
            for (int i = 0; i < 3; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();

        while(true){
            System.out.println(t.isAlive());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

运行结果:

  1. 可能有的同学会有疑问,为什么有关主线程的for循环了三次,但是true被打印了四次--> 第4次打印和t的结束不一定是谁先谁后

  2. Thread对象的生命周期,和系统中的线程生命周期,是不同的(可能存在,Thread对象还存活,但是系统中的线程已经销毁的情况)-->如图下所示:main线程已经结束,但是t线程仍然存活

java 复制代码
public static void main(String[] args) {
		        Thread t = new Thread(()->{
		            while(true){
		                System.out.println("hello thread");
		                try {
		                    Thread.sleep(1000);
		                } catch (InterruptedException e) {
		                    throw new RuntimeException(e);
		                }
		            }
		        });
		        //设置t为后台线程
		        t.setDaemon(true);
		        t.start();
		
		        for (int i = 0; i < 3; i++) {
		            System.out.println("hello main");
		            try {
		                Thread.sleep(1000);
		            } catch (InterruptedException e) {
		                throw new RuntimeException(e);
		            }
		        }
		        System.out.println("main 结束");
		        System.out.println("t线程是否存活"+t.isAlive());
		    }
3.3.4. isInterrupted()判断线程是否被中断

我们先来看第一段代码

java 复制代码
public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            //当当前的线程没有被打断时
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt();
    }

如果我们运行上述代码,会有如下报错:

它的原因是:每次执行循环的时候,绝大部分时间都是在sleep,当主线程调用Interrupt极大的概率下,此时t线程都是在sleep中,这个Interrupt操作能够唤醒sleep, sleep会抛出异常InterruptedException

接着看第二段代码:

java 复制代码
public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            //当当前的线程没有被打断时
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {

                }
            }
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt();
    }

运行结果如下:

大家可能对这个结果感到奇怪:执行完t.interrupt();之后,while循环的条件!Thread.currentThread().isInterrupted()应该是false,因此会退出while循环。

其实,是sleep在搞鬼。由于上述代码,是吧sleep给唤醒了,在这种唤醒的前提下,sleep就会在唤醒之后,把isInterruptted标志位给设置会false,因此while循环的条件为true,循环会正常执行。

这样设置的目的:为了让程序员在catch语句中有更多的选择空间。程序员可以自行决定,这个程序是否结束,还是等会结束,还是不结束(忽略这个终止型号)

因此,针对上述问题,共有三种解决方法

java 复制代码
catch (InterruptedException e) {//sleep操作没按照约定正常执行
                    throw new RuntimeException(e);
                    //1.加上break就是立即终止
									//break;
                    //2.什么都不写,就是不终止
                    //3.catch中执行一些其他逻辑再break,就是稍后终止
                }
java 复制代码
public static void main(String[] args) {
        boolean isFinished = false;
        Thread t = new Thread(()->{
            while(!isFinished){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("thread结束");
        });
        t.start();

        try {
            Thread.sleep(3000);
            isFinished = true;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

上述代码有如下编译报错:

出现这个问题的原因:

lambda表达式其实是回调函数 ,它的执行时机,是在很久之后(操作系统真正创建出线程之后,才会执行的)很有可能,后续线程创建好了,当前的main这里的方法都执行完了,对应的isFinished就销毁了。。。

这个问题的解决方法:

Java的方法是,把捕获的变量给拷贝一份到lambda表达式里面来,外面的变量是否销毁,就不影响lambda里面的执行了,因此,拷贝,就意味着这样的变量就不适合进行修改了。我们就要对于这个isFinished变量进行修改!

java 复制代码
public class Demo10 {

    private static boolean isFinished = false;

    public static void main(String[] args) {
//        boolean isFinished = false;
        Thread t = new Thread(()->{
            while(!isFinished){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("thread结束");
        });
        t.start();

        try {
            Thread.sleep(3000);
            isFinished = true;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

运行结果:

这样为何就行了?
本质是把isFinished修改成成员变量,此时不再是变量捕获的语法了,而是切换到"内部类访问外部类成员"的语法 。lambda本质上是函数式接口,相当于一个内部类,isFinished变量本身就是外部类(Demo10)的成员。内部类本来就可以访问外部类的成员。成员变量的生命周期是让GC(garbage collection 垃圾回收)来管理的。在lambda表达式里面不担心变量生命周期失效的问题,也就不必拷贝。

4. 线程的启动,中断,等待

4.1 线程的启动

之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。

• 覆写 run 方法是提供给线程要做的事情的指令清单(要执行的任务

• 线程对象可以认为是把 李四、王五叫过来了

• 而调用 start() 方法,就是喊一声:"行动起来!",线程才真正独立去执行了

4.2 线程的中断

我们在3.3.4. isInterrupted()判断线程是否被中断这一节当中提到过相同的内容

4.3 线程的等待

等待一个线程 - join()

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。

由于多个线程之间是并发执行,随即调度的。然而,我们程序员不期望随机,使用join可以实现控制多个线程之间结束的先后顺序

java 复制代码
public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i < 4; i++) {
                System.out.println("thread t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();

        System.out.println("main线程结束");
    }

运行结果如下:

如果我们想要控制上述t线程和main线程的执行顺序,需要使用join,比如我们让main等待t线程执行完,再开始执行main线程

java 复制代码
public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i < 4; i++) {
                System.out.println("thread t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        t.join();
        System.out.println("main线程结束");
    }

运行结果如下,这就是我们期望的结果main结束在所有的t之后

注意:

  1. t.join()的效果,在main函数里面,让main线程等待t先结束
java 复制代码
t.join(3000);

join中可以添加参数,这样的意思,如果t线程运行时间在3000ms之内结束,此时,就会立即执行join之后的代码,如果t线程运行的时间超过3000,t还没有结束,此时join也继续往下走,就不用等了

4.4 线程的休眠

对于sleep的理解

方法 说明
public static void sleep(long millis) throws InterruptedException 休眠当前线程 millis 毫秒
public static void sleep(long millis, int nanos) throws InterruptedException 可以更高精度的休眠
相关推荐
ZHOUPUYU5 小时前
PHP 8.3网关优化:我用JIT将QPS提升300%的真实踩坑录
开发语言·php
寻寻觅觅☆9 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
YJlio9 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
l1t10 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
青云计划10 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿10 小时前
Jsoniter(java版本)使用介绍
java·开发语言
ceclar12311 小时前
C++使用format
开发语言·c++·算法
山塘小鱼儿11 小时前
本地Ollama+Agent+LangGraph+LangSmith运行
python·langchain·ollama·langgraph·langsimth
探路者继续奋斗11 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd