一、前言🧂
1、概述🥚
程序
:为完成特定任务,用某种编程语言编写的一组指令的结合(一段静态代码,静态对象)
进程
:是程序的一次执行的过程或是正在运行的一个程序,是一个动态的过:有他自身的产生、存在、消亡的过程(生命周期),进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
线程
:进程可以进一步细化为线程,是一个程序内部的一条执行路径。
知识小盲区🍰:
若一个进程同一时间并行多个线程,就是支持多线程的。
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(PC),线程切换的开销小。
一个进程的多个线程共享相同的内存单元、内存地址空间—>他们从同一堆中分配对象,可以访问相同的变量和对象。就使得线程间通信更加简便,高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(PC)。
本地方法栈— native
虚拟机栈和PC 每一个线程都有一份
方法区和堆 是一个进程一份
一个进程中有多个线程 多个线程共享方法区和堆
2、CPU🍮
单核CPU,是一种假的多线程
多核的话,能更好地发挥多线程的效率
3、并行和并发🥛
并行
:多个CPU同时执行多个任务 ,例如:多个人同时做不同的事儿
并发
: 一个CPU同时执行多个任务 ,例如:秒杀多个人同时做一件事儿
4、优点🧊
-
提高应用程序的响应。对图形化界面更有意义,可增强用户体验
-
提高计算机系统CPU的利用率
-
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
5、多线程时机🍿
-
程序需要同时执行两个或多个任务 -
程序需要实现一些需要风带的任务时,比如: 用户输入,文件读写操作,网络操作,搜索等 -
需要一些后台运行的程序时
二、线程的创建和使用🥢
方式一🍵
java语言的JVM允许程序运行多个线程通过继承 java.lang.Thread
Thread类的特性🍯
-
每个线程都是通过某个特性Thread对象的run()方法来完成操作,经常把run()方法的主体称为线程体 -
通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
多线程的创建方式一 继承Thread类
-
创建一个继承Thread类的子类 -
重写Thread类的run(); ——— 将此线程执行的操作声明在run方法中 -
创建Thread类的子类的对象 -
通过对象调用start();
//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();//通过t1调用run 是不可以的 啊
//以下方法仍然是在main线程中执行的
System.out.println("hello");
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i + "main");
}
}
t1.start();
}
}
为什么是通过对象调用start();方法?🏓
答:
-
启动当前线程 -
调用当前线程的run方法
通过t1调用run 是不建议的
Thread 类的相关方法🍾
void start()
:启动线程,并执行对象run方法
run()
:线程被调度时执行的操作
String getName()
:返回线程的名字
void setNAme (String name )
:设置该线程的名称
static Thread currentThread()
:返回当前线程。在Thread子类中就是this ,通常用于主线 程和Runnable实现类
static void yield()
:线程让步 暂停当前正在执行的线程,把执行的机会让给优先级相同的或者更高的线程
join()
:当某个成序执行流中调用其他线程的join();方法,调用线程将被阻塞,直到join方法加 入的join线程执行完为止
static void sleep(long millitime)
:令当前活动线程在指定时间段内放弃对CPU的控 制,是其他线程有机会被执行,时间到后重新排队
stop()
:强制线程生命期结束,不推荐使用
boolean isAlive()
:返回boolean,判断线程是否还活着
线程的调度🧉
-
调度策略
-
时间片 -
抢占式:高优线假的线程抢占CPU -
java调度方法
-
同优先级线程—先来先服务,使用时间片策略 -
对高优先级的,使用优先调度的抢占式策略
线程的优先级🍹
线程的优先级等级
-
MAX_PRIOPITY :10
-
MIN_PRIOPRITY:1
-
NPRM_PRIORITY:5
涉及的方法:
-
getPriority:返回县城优先值
-
setPriority(int newPriority):改变线程的优先级
说明:
-
线程创建时继承父线程的优先级
-
低优先级只是获得调度的概率低,并非一定是高优先级线程被调度后才会被调用
方式二🎱
实现Runnable接口
-
创建一个实现了Runnable接口的类
-
子类中重写Runnable接口中的run方法
-
创建实现类的对象/通过Thread类含参构造器创建线程对象
-
将Runnable接口的子类对象作为实现参数传递给Thread类的构造器中
-
通过Thread类的对象调用start();方法:开启线程,调用Runnable子类接口的run方法
//1.创建一个实现了Runnable接口的类
class MThread implements Runnable{
//2.实现类去实现Runnable中的抽象方法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 ThreadTest1 {
public static void main(String[] args) {
//3.创建实现类的对象
MThread mThread = new MThread();
// 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(mThread);
// 5.通过Thread类的对象调用start();方法
t1.setName("线程1");
t1.start();
//这是t1线程
//再启动一个线程
Thread t2 = new Thread(mThread);
t2.start();
}
}
通过Thread类的对象调用start()方法
两个功能 :
-
启动线程 -
调用当前线程的run方法 —-调用了Runnable类型的target的
比较线程的两种方式
-
一个对象三个线程 -
三个对象三个线程
开发当中优先选择实现Runnable接口的方式
原因
1:实现的方式没有类的单继承的局限性
2:实现的方式更适合来处理多个线程有共享数据的情况
联系
:public class Thread implements Runnable
相同点
:都是需要去重写run(),并且将线程要执行的逻辑声明在run()中
线程的分类:
java中,线程分两种:守护线程
和用户线程
-
他们俩几乎在每个方面都是相同的,唯一的区别是判断JVM何时离开。 -
守护线程是用来服务用户线程的,通过在stat();方法前调用 -
thread.setDaemon(true)可以吧一个用户线程变成一个守护线程 -
java垃圾回收就是一个经典的守护线程 -
若JVM中都是守护线程,JVM将退出
守护线程依赖用户线程
问题一
:
-
我们不能直接通过调用run方法的方式启动线程
-
我们要调用start
问题二
:
-
再启动一个线程,遍历100以内的偶数
-
应该再创建一个对象
-
IllegalThreadStartException
-
不可以让已经start的线程去再执行
-
要想创建多个线程就要创建多个对象
-
创建Thread类的匿名子类的方式
方式三🧇
通过Callable和Future创建线程
实现步骤:
①
创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。
②
创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值
③
使用FutureTask对象作为Thread对象启动新线程。
④
调用FutureTask对象的get()方法获取子线程执行结束后的返回值。
优劣对比🍦
继承Thread类和实现Runnable接口、实现Callable接口的区别
继承Thread
:线程代码存放在Thread子类run方法中。
-
优势:编写简单,可直接用this.getname()获取当前线程,不必使用Thread.currentThread()方法。
-
劣势:已经继承了Thread类,无法再继承其他类。
实现Runnable
:线程代码存放在接口的子类的run方法中。
-
优势:避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
-
劣势:比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。
实现Callable
:
-
优势:有返回值、避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
-
劣势:比较复杂、访问线程必须使用Thread.currentThread()方法
建议使用实现接口的方式创建多线程
三、线程的生命周期🫖
新建 NEW
:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪RUNNABLE
:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行BLOCKED
:当就绪的线程被调度并获得CPU资源时便进入运行状态,run()方法定义了线程的操作和功能
阻塞WAITING
:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
死亡
:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
四、线程的同步 🍚
同步方法
同步代码块
Lock
🍕小案例
创建三个窗口买票 总票数100张 使用实现Runnable接口的方式
存在线程安全问题 需要解决
-
问题: 买票过程中出现了重票错票 —>出现线程安全问题 -
问题出现原因:当某个线程操作车票中,尚未完成时,其他线程参与进来进行操作 -
解决:进来的时候关个门 当一个线程a在操作时 其它线程不能参与进来。直到线程a操作完 成,其他线程才可以操作ticket,这样 即使线程a出现了阻塞,也没关系 -
在java中我们通过同步机制来解决线程安全问题
方式一:同步代码块
synchronized(同步监视器){
大括号中是需要被同步的代码
}
说明 :
-
操作共享数据的代码,即为需要被同步的代码—->不能包含多,也不可包含少代码 -
共享数据:多个线程共同操作的变量,比如:ticket就是共享数据 -
同步监视器: 俗称 锁 任何一个类的对象都可以称为锁
要求: 多个线程必须共用一把锁
补充: 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中 , 我们不妨将此方法声明同步的
-
同步方法仍然涉及到同步监视器,只是不需要我们显式声明。 -
非静态的同步方法,同步监视器是:this 静态的同步方法,同步监视器是:当前类本身
同步的好处:解决了线程安全问题 操作同步代码时只能有一个线程参与其他线程等待 相当于单线程的过程 效率低
使用同步代码块的方式解决
class Window2 extends Thread {
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while (true) {
// synchronized(obj) {
synchronized(Window2.class) {
if (ticket > 0) {//还有余票
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":出售的票编号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
//这是由三个对象 不能用this
Window2 w1 = new Window2();
Window2 w2 = new Window2();
Window2 w3 = new Window2();
w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");
w1.start();
w2.start();
w3.start();
}
}
class Window1 implements Runnable{
private int ticket = 100;
// Object obj = new Object();
@Override
public void run() {
// Object obj = new Object();
while (true){
synchronized(this){//this唯一的window的对象
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":所售票的票号为:"+ ticket);
ticket--;
}else {
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
使用同步方法
class Window3 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private synchronized void show() {//同步监视器就是this
// synchronized(this){
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":所售票的票号为:" + ticket);
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w = new Window3();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window4 extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private static synchronized void show(){//方法改为静态的 同步监视器是唯一 的了 就是当前类Window4.class唯一的
// private synchronized void show(){//同步监视器 t1 t2 t3 此种解决方法是错误的
if (ticket > 0) {//还有余票
try {
Thread.sleep(
10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":出售的票编号为:" + ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 w1 = new Window4();
Window4 w2 = new Window4();
Window4 w3 = new Window4();
w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");
w1.start();
w2.start();
w3.start();
}
}
线程安全问题之懒汉式
package com.yer.java1;
/**
* 使用同步机制 将单例模式中的懒汉式 改写为 线程安全的
*
* @author Darling
* @create 2022-02-11-17:28
*/
public class BankTest {
}
class Bank {//单例的 创建一个当前类的实例
private static Bank instance = null;
private Bank() {
}//构造器私有化
public static Bank getInstance() {
// public static synchronized Bank getInstance() { //当前类本身
//方式一:效率稍差 第一个创建完,后面的就没必要等了 啊
// synchronized (Bank.class){
// if (instance == null)
// instance = new Bank();//就实例化一下子
// return instance;
// }
//方式二 :效率更高
if (instance == null) {
synchronized (Bank.class) {
if (instance == null)
instance = new Bank();
}
}
return instance;
}
}
线程的死锁问题
死锁:不同的几线程分别占用对方的资源不释放,都在等待对方释放自己需要的同步资源,就想成了线程的死锁
出现思索后,不会出现异常,么有提示,所有线程都处于阻塞状态
解决方法:
-
专门的算法,原则 -
尽量减少同步资源的定义 -
尽量避免嵌套同步
Lock锁🍯
java.util.concurrent.locks.Lock
package com.yer.java1;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
*
* 解决线程安全问题的方式三: Lock锁 ---- JDK5.0 新增的新特性
*
*
* 1.面试题synchronized 与lock的异同
* 解答: 相同点:都是解决线程安全问题
* 不同点:synchronized机制在执行完相应对的同步代码以后,自动释放同步监视器
* Lock手动启动同步(lock.lock()),同时结束同步也需要手动的实现 (lock.unlock();)
* @author Darling
* @create 2022-02-11-19:33
*/
class Window implements Runnable{
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();//true 公平 先进先出
@Override
public void run() {
while (true){
try{
//2.调用lock方法 获取了同步监视器
lock.lock();
//就相当于同步代码块中的一样
if (ticket > 0){
try {
Thread.sleep(99);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售出车票编号为:"+ticket);
ticket--;
}else {
break;
}
}finally {
//3.调用解锁的方法
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
面试题synchronized 与lock的异同
解答:
-
相同点:都是解决线程安全问题
-
不同点:synchronized机制在执行完相应对的同步代码以后,自动释放同步监视器
Lock手动启动同步(lock.lock()),同时结束同步也需要手动的实现(lock.unlock();)
synchronized与Lock的对比🫖
-
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),
synchronized是隐式锁,出了作用域自动释放
-
Lock只有代码块锁,synchronized有代码块锁和方法锁
-
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有 更好的扩展性(提供更多的子类)
优先使用顺序:
Lock→同步代码块(已经进入了方法体,分配了相应资源)→同步方法(在方法体之外)
五、线程的通信🎂
线程通信
wait();
notify();唤醒一个(优先级比较高的)
notifyAll(); 唤醒所有
这三个方法必须使用在在同步代码块 或者 同步方法
Lock不可以!有其他的
这三个方法的调用者,必须是同步代码块 同步方法的调用者 的否则报异常
这三个方法是Object类中的
面试题:关于sleep();wait();的相同点和不同点
相同点:一旦执行,都可以使得当前的线程进入阻塞状态
不同点:1)两个方法声明的位置不一样Thread类中声明sleep();
Object类中声明wait();
2)调用的要求不一样,sleep();可以在任何需要的场景下调用
wait();必须在同步代码块或同步方法中调用
3)关于是否释放同步监视器:如果两个方法都是用在同步代码块或同步方法中,
sleep不会释放锁,wait会释放同步 监视器
六、JDK5.0新增的线程创建方式🍮
实现Callable接口
package com.yer.java1;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 创建线程方式三 :实现Callable接口 ---JDK5.0新增
*
*
* 如何理解实现Callable接口 创建多线程,比实现Runnable接口强大呢?
* 1.call方法是可以有返回值的
* 2.call方法是可以抛出异常的 被外面的操作捕获,获取异常的信息
* 3.Callable支持泛型的
* @author Darling
* @create 2022-02-11-20:24
*/
//1.创建一个实现Callable 的实现类
class NumThread implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
//遍历100以内偶数,并且返回偶数的和
int sum = 0;
for (int i = 0; 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实现类重写的返回值
Object sum = futureTask.get();
System.out.println("总和:"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
实现Callable接口
相比run();call();有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FuntureTask类
Future fut = new FutureTask(numThread)
七、线程池🍸
背景
: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处
:
-
提高响应速度(减少了创建新线程的时间) -
降低资源消耗(重复利用线程池中线程,不需要每次都创建) -
便于线程管理 corePoolSize:核心池的大小 maximumPoolSize:最大线程数 keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程池相关API
JDK5.0起提供了线程池相关APl:
ExecutorService和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
-
voidexecute(Runnablecommand):执行任务/命令,没有返回值,一般用来执行 Runnable
-
< T>Future< T>submit(Callable< T>task):执行任务,有返回值,一般又来执行Callable
-
void shutdown():关闭连接池
Executors:工具类 、线程池的工厂类,用于创建并返回不同类型的线程池
-
ExecutorsnewCachedThreadPool():创建一个可根据需要创建新线程的线程池
-
ExecutorsnewFixedThreadPool(n);创建一个可重用固定线程数的线程池
-
ExecutorsnewSingleThreadExecutor():创建一个只有一个线程的线城池
-
ExecutorsnewScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
package com.yer.java1;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 创建线程的方法四:
* 使用线程池
* <p>
* 1.提高响应速度(减少了创建新线程的时间)
* 2.降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
* 3.便于线程管理
* <p>
* corePoolSize-------核心池的大小
* maximumPoolSize-----最大线程数
* keepAliveTime--线程没有任务时最多保持多长时间后会终止
*
* @author Darling
*/
class NumberThread implements Runnable {
@Override
public void run() {
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() {
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);
//创建一个可以重用的固定线程数的线程池
//在执行之前去设置线程池的属性 接口 在接口的实现类中--
System.out.println(service.getClass());//class java.util.concurrent.ThreadPoolExecutor
// ThreadPoolExecutor
// public class ThreadPoolExecutor extends AbstractExecutorService {
// public abstract class AbstractExecutorService implements ExecutorService {
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
service1.setCorePoolSize(20);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作,需要提供实现Runnable或Callable接口实现类的对象
//service.execute();//适合Runnable
//提交submit 执行execute
service.submit(new NumberThread());//适合Callable
service.submit(new NumberThread1());//适合Callable
//3.关闭连接池
service.shutdown();
//真正在开发使用线程池
}
}
//创建多线程有四种方式
static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//创建一个可以重用的固定线程数的线程池
//在执行之前去设置线程池的属性 接口 在接口的实现类中--
System.out.println(service.getClass());//class java.util.concurrent.ThreadPoolExecutor
// ThreadPoolExecutor
// public class ThreadPoolExecutor extends AbstractExecutorService {
// public abstract class AbstractExecutorService implements ExecutorService {
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
service1.setCorePoolSize(20);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作,需要提供实现Runnable或Callable接口实现类的对象
//service.execute();//适合Runnable
//提交submit 执行execute
service.submit(new NumberThread());//适合Callable
service.submit(new NumberThread1());//适合Callable
//3.关闭连接池
service.shutdown();
//真正在开发使用线程池
}
}
//创建多线程有四种方式
原文始发于微信公众号(步尔斯特):多线程初体验
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/35836.html