java多线程基础学习

  • 一、线程简介
    • 1.类比
    • 2.程序进程线程
    • 3.线程的核心概念
  • 二、线程的实现(重点)
    • 调用方法与调用多线程的区别
    • Thread 类
      • 1.thread使用方法
      • 2. 代码实现
      • 3.应用
    • Runnable 接口
      • 1.Runable使用方法
      • 2. 代码实现
      • 3.应用
    • Callable接口
      • 1.Callable使用方法
      • 2.代码实现
  • 三、线程的状态
    • 线程的5个状态
    • 线程方法
    • 观察线程的状态
    • 线程的优先级
    • 守护线程(daemon)
  • 四、线程同步
    • 同步方法与同步块
    • 死锁
    • Lock(锁)
  • 五、线程协作
    • 生产者消费者模式
    • 线程池

一、线程简介

所有的资料来源都是以【狂神说Java】多线程详解为基础
【狂神说Java】多线程详解
建议看视频,这篇博客是笔记
同时要结合目录看

1.类比

类比于现实中的的多任务,在同一个时间段,同时做几件事情

在程序中就是主线程与子线程同时进行任务

2.程序进程线程

在操作系统中的运行的程序就是进程,而进程通常采用多进程执行

  • 程序

    指令和数据的有序集合,本身没有任何运行的含义,是静态概念

  • 进程

    是执行程序的一次执行过程,是一个动态概念,是系统资源分配的单位

  • 线程

    一个进程通常包括多个线程,线程是CPU调度和执行的单位

3.线程的核心概念

二、线程的实现(重点)

调用方法与调用多线程的区别

run()方法是需要执行的方法体

start()方法是开启多线程的方法体

Thread 类

不建议使用:避免OOP单继承局限性

1.thread使用方法

  1. 自定义线程类继承Thread类
  2. 重写run方法,编写线程执行体
  3. 主线程中创建线程对象,使用start方法启动线程

2. 代码实现

package cn.livorth;public class TestThread01  extends Thread{@Overridepublic void run() {//在run()方法中编写执行体for (int i = 0; i < 100; i++) {System.out.println("在子线程中执行--" + i);}}public static void main(String[] args) {//在主线程中创建线程对象,使用start方法启动线程TestThread01 testThread = new TestThread01();testThread.start();for (int i = 0; i < 100; i++) {System.out.println("在主线程中执行---" + i);}}
}

效果如下:

并行交替执行,轮流占用cpu

3.应用

样例来自《狂神说java-多线程详解》

代码(详解在注释):

package cn.livorth;import org.apache.commons.io.FileUtils;import java.io.File;
import java.io.IOException;
import java.net.URL;public class TestThread02 extends Thread{private String url;private String filename;//构造函数传入所需的url和filenamepublic TestThread02(String url, String filename) {this.url = url;this.filename = filename;}@Overridepublic void run() {//调用downLoader来获取文件webDownLoader webDownLoader = new webDownLoader();webDownLoader.downLoader(url, filename);System.out.println(filename + "下载完成");}public static void main(String[] args) {//传入三张图图片的urlTestThread02 t1 = new TestThread02("https://i0.hdslb.com/bfs/album/" +"481ba607ba31e9932b90e383f3698fec4c1d9577.jpg@518w_1e_1c.jpg", "pic01.jpg");TestThread02 t2 = new TestThread02("https://i0.hdslb.com/bfs/album/" +"98d71302c030aa86258eb17a5db084bfadf8ff39.jpg@518w_1e_1c.jpg", "pic02.jpg");TestThread02 t3 = new TestThread02("https://i0.hdslb.com/bfs/album/" +"70849f611883c3e7feffc730c4f1e7b7173c9695.jpg@518w_1e_1c.jpg", "pic03.jpg");//开始多线程下载t1.start();t2.start();t3.start();}
}class webDownLoader
{/*** 通过url获取文件内容,并保存* @param url* @param name*/public void downLoader(String url, String name){try {//调用Commons_io包里面的方法copyURLToFileFileUtils.copyURLToFile(new URL(url),  new File(name));} catch (IOException e) {e.printStackTrace();System.out.println("IO异常,downLoader出现问题");}}
}

结果:

这只是一个简单的对多线程的小实践,事实上通过这样获取图片还是有诸多问题

Runnable 接口

推荐使用:避免单继承的局限性,方便同一个对象被多个进程调用

1.Runable使用方法

  1. 自定义MyRunable类实现Runable接口
  2. 重写run方法,编写线程执行体
  3. 主线程中创建线程对象,使用start方法启动线程

2. 代码实现

package cn.livorth;public class TestThread03 implements Runnable{@Overridepublic void run() {//在run()方法中编写执行体for (int i = 0; i < 100; i++) {System.out.println("在子线程中执行--" + i);}}public static void main(String[] args) {//创建Runable接口的实现类对象TestThread03 testThread = new TestThread03();//创建线程对象,通过线程对象来开启线程,也就是代理Thread thread =  new Thread(testThread);thread.start();//也可以缩写为://new Thread(testThread).start();for (int i = 0; i < 100; i++) {System.out.println("在主线程中执行---" + i);}}
}

效果如下:

在实现的效果上是没有区别的:

3.应用

按照狂神说的那个样例来的话,会出现问题

对个线程操作同一个资源的时候,线程不安全,数据会紊乱
所以不写了

Callable接口

仅作了解即可

1.Callable使用方法

通过服务提交线程

  1. 实现Callable接口,需要返回值类型
  2. 重写call方法, 需要抛出异常
  3. 创建目标对象
  4. 创建执行服务: ExecutorService ser = Executors.newFixedThreadPool(1);
  5. 提交执行: Future result1 = ser.submit(t1);
  6. 获取结果: boolean r1 = result1.get()
  7. 关闭服务: ser.shutdownNow();

2.代码实现

改自Thread中的应用

《狂神说java-多线程详解》-P8

package cn.livorth;import java.util.concurrent.*;public class TestCallable01 implements Callable<Boolean> {private String url;private String filename;//构造函数传入所需的url和filenamepublic TestCallable01(String url, String filename) {this.url = url;this.filename = filename;}@Overridepublic Boolean call() {//调用downLoader来获取文件webDownLoader webDownLoader = new webDownLoader();webDownLoader.downLoader(url, filename);System.out.println(filename + "下载完成");return true;}public static void main(String[] args) {//传入三张图图片的urlTestCallable01 t1 = new TestCallable01("https://i0.hdslb.com/bfs/album/" +"481ba607ba31e9932b90e383f3698fec4c1d9577.jpg@518w_1e_1c.jpg", "pic01.jpg");TestCallable01 t2 = new TestCallable01("https://i0.hdslb.com/bfs/album/" +"98d71302c030aa86258eb17a5db084bfadf8ff39.jpg@518w_1e_1c.jpg", "pic02.jpg");TestCallable01 t3 = new TestCallable01("https://i0.hdslb.com/bfs/album/" +"70849f611883c3e7feffc730c4f1e7b7173c9695.jpg@518w_1e_1c.jpg", "pic03.jpg");//创建执行服务(参数为线程池的大小)ExecutorService service = Executors.newFixedThreadPool(3);//提交执行Future<Boolean> r1 = service.submit(t1);Future<Boolean> r2 = service.submit(t2);Future<Boolean> r3 = service.submit(t3);//获取结果(不写应该也是可以的,这里应该是作为一种检验参数)try {boolean rs1 = r1.get();boolean rs2 = r2.get();boolean rs3 = r3.get();System.out.println(rs1);System.out.println(rs2);System.out.println(rs3);} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}//关闭服务service.shutdown();}
}

结果:

三、线程的状态

线程的5个状态

  1. 创建状态:Thread t = new Thread();
  2. 就绪状态:调用start方法
  3. 运行状态:被cpu调度执行
  4. 阻塞状态:调用sleep、wait或同步锁定时,进入阻塞状态
  5. 死亡状态:线程中断或者结束,一旦死亡就不再启动

线程方法

方法 说明
setPriority(int newPriority) 更改线程的优先级
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠
void join() 等待该线程终止
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
void interrupt() 中断线程,别用这个方式
boolean isAlive() 测试线程是否处于活动状态
  1. 停止线程

    • 不推荐使用JDK提供的stop()、destroy()。

    • 推荐让线程自己停下来,但也不建议使用死循环

    • 建议使用一个标志位作为终止变量,当flag==false时,线程终止运行

    package cn.livorth.state;public class TestStop implements Runnable{private boolean flag = true;@Overridepublic void run() {int i = 0;while(flag){System.out.println("子进程执行中---" + i++);}}//设置个公开方法,利用标志位停止线程public void stop(){this.flag = false;}public static void main(String[] args) {TestStop testStop = new TestStop();new Thread(testStop).start();for (int i = 0; i < 100; i++) {System.out.println("主进程执行中---" + i);if(i == 90){testStop.stop();System.out.println("子进程结束运行");}}}
    }
    
  2. 线程休眠

    • sleep()方法的单位是毫秒

    • sleep()存在异常InterruptedException

    • sleep完了线程进入就绪状态,不会直接进入执行态

    • 可以用于模拟网络延时,倒计时等

    • 每个对象都有一个锁,sleep不释放锁

            try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
    

    模拟网络延时可以放大问题的发生性

  3. 线程礼让

    • 礼让不一定成功,yield方法是当前正在执行的进程进入就绪态,等待cpu的调度
    public void run() {System.out.println(Thread.currentThread().getName()+"线程开始执行");Thread.yield();//礼让System.out.println(Thread.currentThread().getName()+"线程停止执行");
    }
    
  4. Join

    join合并线程,只能是当前线程执行完之后才能执行其他线程,对其他线程造成阻塞

    package cn.livorth.state;public class TestJoin implements Runnable{@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println("子线程执行---" + i);}}public static void main(String[] args) {TestJoin testJoin = new TestJoin();Thread thread = new Thread(testJoin);thread.start();for (int i = 0; i < 500; i++) {if(i == 200){try {//阻塞主线程,使子线程优先执行完thread.join();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("主线程执行---" + i);}}
    }

观察线程的状态

代码样例:

package cn.livorth.state;public class TestState {public static void main(String[] args) {Thread thread  = new Thread(()->{for (int i = 0; i < 5; i++) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("******");}});//启动前Thread.State state = thread.getState();System.out.println(state);//NEW//启动后thread.start();state = thread.getState();System.out.println(state);//RUNABLE//阻塞时与结束时while(state != Thread.State.TERMINATED){try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}state = thread.getState();//TIMED_WAITING或者TERMINATEDSystem.out.println(state);}}
}

结果样例:

线程的优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。

  • 线程的优先级用数字表示,范围从1~10.

    • Thread.MIN_ PRIORITY= 1;
    • Thread.MAX PRIORITY = 10;
    • Thread.NORM_ PRIORITY= 5;
  • 使用以下方式获取或改变优先级

    • getPriority() ,setPriority(int xxx)

注意:

  1. 先设置优先级再启动

  2. main方法的默认优先级为5

  3. 理论上来说优先级越高的越先执行,哪怕他start更晚

守护线程(daemon)

  • 线程分为用户线程守护线程
  • 虚拟机必须确保用户线程执行完毕.
  • 虚拟机不用等待守护线程执行完毕
  • 如,后台记录操作日志,监控内存,垃圾回收等待…

代码示例:

package cn.livorth.state;public class TestDaemon {public static void main(String[] args) {God god = new God();You you = new You();Thread thread = new Thread(god);//将上帝设置为守护线程,在false的时候为用户线程thread.setDaemon(true);thread.start();//开启一般的用户线程new Thread(you).start();}
}class God implements Runnable{@Overridepublic void run() {//上帝永生,无限循环while (true)System.out.println("上帝在你身旁");}
}class You implements Runnable{@Overridepublic void run() {//人生有限,100年后撒手人寰for (int i = 0; i < 100; i++) {System.out.println("happy everyday");}System.out.println("goodbye world");}
}

结果:

上帝明明是个死循环,但是进程还是结束了。

因为这里将上帝线程设置为了守护线程,虚拟机不加以考虑

四、线程同步

并发:同一个对象被多个线程同时操作,也就是不同线程同时操作同一个资源地址,造成数据紊乱

同步:多个需要同时访问资源的线程进入对象的等待池,等待前面线程使用完毕

锁:每个对象都有把锁,当获取对象时,独占资源,其他线程必须等待,使用结束后才释放

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

同步方法与同步块

synchronized关键字

synchronized默认锁的是他自身的对象,要是跨对象,通常使用同步块,即锁共享资源所在的对象

  1. 同步方法

    public synchronized void method(int args){}
    

    因为每个对象对应一把锁,使用加了关键字之后,方法一旦执行就独占该锁

    缺陷:若将一个大的方法申明为synchronized将会影响效率

  2. 同步块

    synchronized(Obj){}
    
  • Obj称之为同步监视器

    • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this ,就是这个对象本身或者是class [反射中讲解]
  • 同步监视器的执行过程:
    1. 第一个线程访问,锁定同步监视器,执行其中代码.
    2. 第二个线程访问 ,发现同步监视器被锁定,无法访问
    3. 第一个线程访问完毕 ,解锁同步监视器.
    4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形. 某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题.

我的理解:

​ 张三占用着A资源,等着B资源,李四占用着B资源,等着A资源

​ 张三同时有AB资源才能完成任务,李四也一样,但是两个人谁都不会先放手

​ 然后就进入了死锁

产生死锁的四个必要条件:

  1. 互斥条件: 一个资源每次只能被一个进程使用
  2. 请求与保持条件: 一个进程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件: 进程已获得的资源,在末使用完之前,不能强行剥夺
  4. 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系

只要破除任意一个就能避免死锁

Lock(锁)

通过显示定义同步锁对象(Lock)来实现同步

ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

演示代码:

package cn.livorth.gaoji;import java.util.concurrent.locks.ReentrantLock;public class TestLock {public static void main(String[] args) {TestLock2 testLock = new TestLock2();new Thread(testLock, "A").start();new Thread(testLock, "B").start();new Thread(testLock, "C").start();}}class TestLock2 implements Runnable{int count = 1000;//定义lock锁private final ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}while(true){try {//进入加锁状态lock.lock();if(count > 0)System.out.println(Thread.currentThread().getName() + "---" +count--);elsebreak;}finally {//解锁lock.unlock();}}}
}

说明:

在不加锁的情况下,ABC可能会同时操作到count,导致数据紊乱

在加了锁之后,ABC排队操作

五、线程协作

生产者消费者模式

有一说一这个我们上个学期的操作系统讲的很详细了

生产者与消费者共享一个资源,同时生产者与消费者相互依赖互为条件

  • 生产者访问仓库,往里面放;消费者也要访问,但是只拿

主要是用以下方法达到通信的效果:

  1. wait():先释放资源,并开始等待
  2. wait(long timeout):释放资源等待timeout秒
  3. notify():唤醒一个处于等待状态的线程
  4. notifyAll():唤醒同一个对象上所有调用了wait()方法的线程,优先级高的优先调度

解决方式:

  1. 管程法

    设置一个缓冲区,用于暂存数据

    代码示例:

    package cn.livorth.gaoji;import java.util.LinkedList;
    import java.util.Queue;//管程法解决
    public class TestPC {public static void main(String[] args) {//创建缓冲区SynContainer container = new  SynContainer();//双线程new Producer(container).start();new Consumer(container).start();}
    }//生产者
    class Producer extends Thread{SynContainer container;public Producer(SynContainer container) {this.container = container;}@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("生产了第" + i + "件产品");container.push(new Products(i));}}
    }//消费者
    class Consumer extends Thread{SynContainer container;public Consumer(SynContainer container) {this.container = container;}@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("消费了第" + container.pop().id + "件产品");}}
    }//产品
    class Products{int id;public Products(int id) {this.id = id;}
    }//缓冲区
    class SynContainer{Queue<Products> queue = new LinkedList<Products>();int count = 0;int size = 10;//生产者放入产品public synchronized void push(Products product){//容器满,等待消费者消费if(count == size){try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//没满,我们则需要存入成品,并唤醒消费者count++;queue.offer(product);this.notifyAll();}//消费者消费产品public synchronized Products pop(){//容器为空,等待生产者生产if (count == 0) {try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//容器有剩,取出物品,并告诉生产者可以生产了count--;Products productPoll = queue.poll();this.notifyAll();return productPoll;}
    }
    
  2. 信号灯法

    设置一个标记位(类似于容量为1的管程法)

    package cn.livorth.gaoji;//信号灯法解决
    public class TestPC2 {public static void main(String[] args) {//创建缓冲区TheProduct theProduct = new  TheProduct();//双线程new Producers(theProduct).start();new Consumers(theProduct).start();}
    }//生产者
    class Producers extends Thread{TheProduct theProduct;public Producers(TheProduct theProduct) {this.theProduct = theProduct;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {theProduct.push("产品" + i);}}
    }//消费者
    class Consumers extends Thread{TheProduct theProduct;public Consumers(TheProduct container) {this.theProduct = container;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {theProduct.pop();}}
    }//产品
    class TheProduct{boolean flag = false;String product;//生产者生产,消费者等待public synchronized void push(String product){if(flag){try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("生产者生产了" + product);this.product = product;this.flag = !this.flag;this.notifyAll();}//消费者消费,生产者等待public synchronized void pop(){if(!flag){try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("消费者消费了" + product);this.flag = !this.flag;this.notifyAll();}
    }
    

线程池

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
  • 好处:
    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理
      • corePoolSize:核心池的大小
      • maximumPoolSize: 最大线程数
      • keepAliveTime: 线程没有任务时最多保持多长时间后会终止

简单的使用参照Callable的使用

并发进阶以后再学

java多线程基础学习[狂神说java-多线程笔记]相关推荐

  1. Java零基础学习难吗

    java编程是入行互联网的小伙伴们大多数的选择,那么对于零基础的小伙伴来说Java零基础学习难吗?如果你是初学者,你可以很好的理解java编程语言.并不困难.如果你的学习能力比较高,那么你对Java的 ...

  2. 【J2ME 2D 游戏开发系列】◣HIMI游戏开发启蒙教程◢JAVA零基础学习J2ME游戏开发全过程!...

    本站文章均为 李华明Himi 原创,转载务必在明显处注明:  转载自[黑米GameDev街区] 原文链接: http://www.himigame.com/j2me-2/774.html Himi从写 ...

  3. Java零基础学习全套视频笔记

    Java零基础学习全套视频笔记 一.Java基础 1.注释 注释并不会被执行,是给我们写代码的人看的,防止项目结构代码太多忘记代码相关功能. 书写注释是一个非常好的习惯,平时写代码也一定要注意规范. ...

  4. 【Java】Java零基础学习笔记

    文章目录 前言 思维导图 前期准备 卸载JDK 安装JDK Hello,world 可能遇到情况 java程序运行机制 IDEA的安装 java基础部分 基础语法 运算符 包机制 javaDoc文档手 ...

  5. Java零基础学习Java编程语言基础知…

    很多Java编程初学者在刚接触Java语言程序的时候,不知道该学习掌握哪些必要的基础知识.下面就说说Java零基础学习Java编程语言基础知识的几个要点.希望能够对Java编程基础入门学习的新手有帮助 ...

  6. java入门基础学习(三)

    文章目录 (一)有返回值的方法 (二)方法重载 习题 (一)有返回值的方法 格式:public static 返回值数据类型 方法名(参数){方法体return 数据;} 注意:1.返回值数据类型非v ...

  7. java编程基础学习需要多久的时间

    Java是当前世界颇为流行的编程语言之一,很多想学习java的人都会听过一句话,先学好java基础,在考虑是自学还是培训学习,同时新的问题出现了,java基础包括什么,需要学习多久呢,对于小白来说,想 ...

  8. Java零基础好学吗?Java该怎么学?

    Java零基础好学吗?Java该怎么学?在IT行业中,Java开发工程师是一个很吃香的职业,薪资水平也是几乎过万,许多人想转行Java开发,但又担心零基础能不能学会,学起来有多难,如果是零基础自学Ja ...

  9. 尚硅谷Java零基础极速入门七天版笔记

    Java零基础极速入门 文章目录 Java零基础极速入门 1 Java快速入门 1.1计算机语言 1.2 Java语言 1.3 JVM 1.4 环境配置 2 基础语法 2.1 变量 2.2 标识符 2 ...

最新文章

  1. Packet Tracer 5.0配置cisco路由器详细说明
  2. c语言两个数组比较大小函数,输入两个数组,调用large函数比较,计数,输出统计结果...
  3. Java Web应用的生命周期
  4. c# 后台 添加datable 数据
  5. vue-codemirror基本用法:实现搜索功能、代码折叠功能、获取编辑器值及时验证
  6. 新概念英语(1-59)Is that all
  7. 深度学习工程师能力评估标准
  8. Win10彻底关闭Antimalware Service Executable占用内存过高问题
  9. C#一个FTP操作封装类FTPHelper
  10. SQL重复记录处理(查找,过滤,删除)
  11. 登陆模块防止恶意用户SQL注入攻击
  12. oracle数据库创建表空间和表临时空间
  13. php制作多媒体课件,网络自主学习型多媒体课件设计与制作PPT.ppt
  14. mysql手册05_存储过程和存储函数
  15. 合宙Air724UG二次开发(2):资料描述
  16. Laravel 下使用 FFmpeg 处理多媒体文件
  17. 软件相貌测试准确吗,测另一半的相貌超准软件 提前了解对象的外貌
  18. python等比例压缩图片_python使用pil进行图像处理(等比例压缩、裁剪)实例代码
  19. Git Re-Basin: Merging Models modulo Permutation Symmetries解读
  20. 食物也疯狂!KOOCAN盘点因为食物毁掉的中国电视剧

热门文章

  1. gamebryo游戏引擎概要
  2. 【全栈计划 —— 单片机】——Part_02 结合LED灯,拿捏底层输入输出
  3. STM32 USART理解
  4. 哄另一半开心的下雨特效
  5. LiberOJ - 2603. 「NOIP2012」国王游戏
  6. VASP 6.1.0 + VTST + intel新OneAPI 安装编译
  7. 研发项目经理需要具备的7大核心素质特征
  8. 字节跳动进军云计算市场,火山引擎补齐IaaS业务空白
  9. GAN在短视频中的AI特效实践(PPT演示)
  10. motan学习笔记 三 motan Demo 分析