多线程开发
Posted 聆听微风
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程开发相关的知识,希望对你有一定的参考价值。
多线程开发
异步:Async与Await简单用法
简单写法:
- 创建方法使用Async进行修饰,
- 在方法内部耗时操作前面使用Await修饰。
代码如下:
public Form2()
InitializeComponent();
private async void TaskTestFun()
var task = await Task.Run(Fun).ConfigureAwait(true);
Console.WriteLine(task.ToString());
var d = 1;
return;
public string Fun()
Thread.Sleep(5000);
return "Sleep 5000 has Finished";
private void button1_Click(object sender, EventArgs e)
TaskTestFun();
运行现象描述:
当主进程进入TaskTestFun方法后,执行到创建Task方法后,就空闲了(表现为可以被拖动),这个时候,程序的运行指针还在var task那行代码上。
Task方法执行的时候,自己创建了一个工作子线程,用来执行动作,当动作执行完了以后,通知主进程获取var task的结果,让主线程继续执行下去。
图解如下:
- 当前主线程进入TaskTestFun()
- 主线程执行Task.Run方法(创建一个新的工作线程)
-
当前主界面可以拖动()说明主线程空闲,没有被阻塞=非阻塞
-
当Task任务结束即将以后
-
执行task完Return以后,工作子线程被销毁
-
最后输出子线程返回的信息
多线程并发(所有线程全结束了,才能继续往下运行)
要点:
- 创建List
集合 - 使用WhenAll方法,等待所有的线程完成。
特点:这里跑的快的线程,先结束,然后要等待跑得慢的线程。大家一起结束才能继续往下面进行
public partial class Form2 : Form
public Form2()
InitializeComponent();
InitializeList();
private void InitializeList()
for (int i = 0; i < 9; i++)
Targetlist.Add($" 任务i ");
private List<string> Targetlist = new List<string>();
List<Task<string>> Tasks = new List<Task<string>>();
private async void TaskTestFun()
Targetlist.ForEach(ItemActivation => Tasks.Add(Task.Run(Fun))) ;
var task = await Task.WhenAll(Tasks).ConfigureAwait(false);
foreach (var item in task)
Console.WriteLine(task.ToString());
return;
public string Fun()
Thread.Sleep(1000);
return "This Task has Finished";
private void button1_Click(object sender, EventArgs e)
TaskTestFun();
异步回调:多线程多线程并发(快的线程,先结束,先往下跑)(主线程不等待所有线程都结束)
- 要点:
不再使用async 和await关键字
使用ContinueWith关键字
-
出现的问题:
UI线程不再等待工作主线程任务是否结束,自己私下跑了
public partial class Form2 : Form
public Form2()
InitializeComponent();
InitializeList();
private void InitializeList()
for (int i = 0; i < 9; i++)
Targetlist.Add($" 任务i ");
private List<string> Targetlist = new List<string>();
List<Task<string>> Tasks = new List<Task<string>>();
private void TaskTestFun()
int idx = 0;
foreach (string item in Targetlist)
Task.Run(Fun).ContinueWith(t => Console.WriteLine($"idx++ 当前任务结束"); );
Console.WriteLine("所有任务已经结束");
return;
public string Fun()
Thread.Sleep(1000);
return "This Task has Finished";
private void button1_Click(object sender, EventArgs e)
TaskTestFun();
运行结果:
异步回调:多线程多线程并发(快的线程,先结束,先往下跑)(主线程等待所有线程都结束)
要点:
- 使用 Async 和Await 关键字标记UI主线程等待执行
- 任务方法的内部,编写事件。在外层注册事件和绑定回调的方法
情景介绍:幼儿园,有很多孩子在吃饭,吃完饭后自己要洗碗。当最后一个孩子把碗洗碗以后。老师会安排孩子们午休
输出结果:
代码逻辑:
编写自定义事件EatEvent,然后让Eat完成以后调用EatEvent注册的WashDishes方法。
主线程收到await Task.WhenAll(Tasks)影响,所以暂时不会继续往下走。
本质:任务动作方法,有编写触发的事件
public partial class Form2 : Form
/// <summary>
/// 创建孩子集合
/// </summary>
List<Child> children = new List<Child>();
public Form2()
InitializeComponent();
InitializeList();
private void InitializeList()
/// 初始化6个孩子
for (int i = 0; i < 6; i++)
Child child = new Child();
///注册洗碗事件
child.EatFinish_Event += WashDishes;
children.Add(new Child());
/// <summary>
/// 执行洗碗动作
/// </summary>
private void WashDishes(object sender, EatEvent e)
Console.WriteLine("洗碗完成了");
private void button1_Click(object sender, EventArgs e)
TaskTestFun();
private async void TaskTestFun()
List<Task> Tasks = new List<Task>();
foreach (var child in children)
Tasks.Add(Task.Run(child.Eat));
await Task.WhenAll(Tasks);
Console.WriteLine("最后一个孩子吃完饭并且完成洗碗了");
Console.WriteLine("老师让孩子去午休了");
return;
/// <summary>
/// 自定义事件类
/// </summary>
public class EatEvent : EventArgs
/// <summary>
/// 孩子
/// </summary>
public class Child
/// <summary>
/// 吃饭用时
/// </summary>
public int Duration;
public void Eat()
Thread.Sleep(Duration);
Console.WriteLine("完成吃饭动作,将要洗碗");
EatFinish_Event?.Invoke(this,new EatEvent());
public Child()
Random rd = new Random();
int n = rd.Next(5,10);
Duration = n * 1000;
public Action<object, EatEvent> EatFinish_Event;
ConfigureAwait用法
使用场合:含有UI的程序。(不含有UI界面的程序是没有影响的)
代码:ConfigureAwait(false)
public partial class Form2 : Form
public Form2()
InitializeComponent();
Stopwatch stopwatch = new Stopwatch();
private void button1_Click(object sender, EventArgs e)
stopwatch.Start();
Console.WriteLine("main1 thread:" + Thread.CurrentThread.ManagedThreadId);
TestConfigureAwait();
Console.WriteLine("main2 thread:" + Thread.CurrentThread.ManagedThreadId);
public async Task TestConfigureAwait()
await Task.Run(() =>
Thread.Sleep(1000);
Console.WriteLine("Task thread :" + Thread.CurrentThread.ManagedThreadId);
).ConfigureAwait(false);
Thread.Sleep(3000);
Console.WriteLine("TestConfigureAwait thread : " + Thread.CurrentThread.ManagedThreadId);
stopwatch.Stop();
Console.WriteLine($"消耗时间stopwatch.Elapsed.TotalSeconds.ToString()");
输出结果:
代码:ConfigureAwait(true)
public partial class Form2 : Form
public Form2()
InitializeComponent();
Stopwatch stopwatch = new Stopwatch();
private void button1_Click(object sender, EventArgs e)
stopwatch.Start();
Console.WriteLine("main1 thread:" + Thread.CurrentThread.ManagedThreadId);
TestConfigureAwait();
Console.WriteLine("main2 thread:" + Thread.CurrentThread.ManagedThreadId);
public async Task TestConfigureAwait()
await Task.Run(() =>
Thread.Sleep(1000);
Console.WriteLine("Task thread :" + Thread.CurrentThread.ManagedThreadId);
).ConfigureAwait(true);
Thread.Sleep(3000);
Console.WriteLine("TestConfigureAwait thread : " + Thread.CurrentThread.ManagedThreadId);
stopwatch.Stop();
Console.WriteLine($"消耗时间stopwatch.Elapsed.TotalSeconds.ToString()");
简单说明一下:
使用ConfigureAwait(false)时,下一句会从线程池里面取出一个线程执行下面的语句。ConfigureAwait(true)时,下面的代码会等到主线程,然后由主线程继续运行下去。
总结:对于带有UI的程序有效,为了减少时间损耗,建议尽量使用ConfigureAwait(false)。
Task Wait/WaitAny/WaitAll
Wait 阻塞
让调用的线程阻塞在Wait处。
只有三种方法可以让调用线程结束等待:
- 线程执行完毕
- 线程被取消
- 线程当前引发异常
代码案例:
public partial class Form2 : Form
public Form2()
InitializeComponent();
InitializeList();
List<People> peoples = new List<People>();
int count = 10;
private void InitializeList()
for (int i = 0; i < count; i++)
peoples.Add(new People());
private void button1_Click(object sender, EventArgs e)
for (int i = 0; i < count; i++)
Task task = Task.Run(() =>
peoples[i].Sleep();
);
task.Wait();
Console.WriteLine("完成当前线程任务");
Console.WriteLine("已经完成所有任务");
public class People
public People()
public void Sleep()
Thread.Sleep(5000);
当前效果:
主要线程一直等待(阻塞),直到任务线程完成动作以后才能执行下面的语句。
WaitAny 阻塞部分线程
调用线程也会在线程阻塞。但是当有一个线程完成任务以后,调用线程就能被释放,继续下面的语句
代码:
public partial class Form2 : Form
Stopwatch Stopwatch = new Stopwatch();
public Form2()
InitializeComponent();
InitializeList();
List<People> peoples = new List<People>();
int count = 10;
private void InitializeList()
for (int i = 0; i < count; i++)
peoples.Add(new People());
private void button1_Click(object sender, EventArgs e)
Stopwatch.Start();
List<Task> Tasks = new List<Task>();
for (int i = 0; i < count-1; i++)
Tasks.Add(
Task.Run(() =>
peoples[i].Sleep();
));
Task.WaitAny(Tasks.ToArray());
Console.WriteLine("当前已经有一个任务已经完成");
public class People
public People()
public void Sleep()
Console.WriteLine("执行睡觉任务");
Thread.Sleep(3000);
当创建完所有的任务以后
此时先执行到Task.waitAny
主线程阻塞到Task.waitAny上
当有一个任务线程执行完成以后:主线程继续往下面执行,其他线程释放
WaitAll 阻塞全部的线程
当所有的工作线程都执行完成以后,主线程才能从阻塞的状态继续往下执行
task Result()
这个现在没学明白,后面再写
死锁
借用某个案例中的代码。
有可能的触发函数:ContinueWith(true) 切换主线程执行
原理:当主线程遇到task.result这类方法被阻塞线程时,子线程执行完成触发ContinueWith,调度主线程回来,但是主线程已经被阻塞。所以触发死锁
关键代码如下:
主线程函数:
private void BtnDeadlock_Click(object sender, EventArgs e)
#region 死锁演示
Stopwatch sw = Stopwatch.StartNew(); //计时器
resultTextBox.Clear();
AppendLine("Wait开始......");
for (int i = 0; i < Data.Books.Count; i++)
var book = Data.Books[i];
var idx = i + 1;
var task = book.SearchAsync();
AppendLine($"task.Result task.Id.task.Status");
sw.Stop();
AppendLine($"Wait完成:Convert.ToSingle(sw.ElapsedMilliseconds) / 1000秒");
#endregion
任务线程的执行函数:
public async Task<string> SearchAsync()
Stopwatch sw = Stopwatch.StartNew();
await Task.Delay(Duration * 1000).ConfigureAwait(true);
sw.Stop();
return Result(sw.ElapsedMilliseconds);
当主线程到达SearchAsync方法后,遇到await关键字,知道当前方法为异步方法。所以继续往下执行,然后遇到task.Result需要获取线程返回结果,就被阻塞住了。与此同时,任务线程完成Delay任务,通过ConfigureAwait(true)方法来让主线程切换回来,切换失败。所以导致了死锁。
如果修改ConfigureAwait(false),那么导致的结果将是主线程一直阻塞,表现为界面卡住。
分析如下:ConfigureAwait(false),任务完成以后,当前系统从线程池里面抽取一个子线程来完成后续任务。但是当主线程只是在task.Result上等到需要的返回值以后,才能继续往下执行。所以表现为主线程能够往下执行,流程正确,但是主线程阻塞,界面卡住。
解决方法:
第一步:ConfigureAwait(false)
第二部:因为主线程需要接收到子线程的返回值来执行方法。
那么把子线程的执行方法封装为异步回调ContinueWith(),让主线程不要去接触这些内容,就能让主线程避免死锁
private void BtnDeadlock_Click(object sender, EventArgs e)
#region 死锁演示
Stopwatch sw = Stopwatch.StartNew(); //计时器
resultTextBox.Clear();
AppendLine("Wait开始......");
for (int i = 0; i < Data.Books.Count; i++)
var book = Data.Books[i];
var idx = i + 1;
var task = book.SearchAsync().ContinueWith(t => InvworkAppendLine($"idx.t.Result"));
sw.Stop();
AppendLine($"Wait完成:Convert.ToSingle(sw.ElapsedMilliseconds) / 1000秒");
#endregion
线程的暂停,继续和取消
ManualResetEvent
ManualResetEvent是一个通过信号机制,实现线程间状态同步的类。常用的方法有以下三个:
WaitOne:阻止当前线程,直到收到信号
Reset:将事件状态设置为非终止状态,导致线程阻止
Set:将事件状态设置为终止状态,从而允许继续执行一个或多个等待线程
CancellationTokenSource
CTS是用来发出取消标记,线程和任务根据CTS发出的信号,在自行决定在合适的位置取消线程或任务。
CTS是发出取消标记方式有2种 自动和手动:
1、手动通过调用Cancled()发出取消标记。
2、自动方式通过定时器、指定时间后取消信号。
public partial class FormComm : Form
private CancellationTokenSource? TokenSource;
private ManualResetEvent? ManualReset;
public FormComm()
InitializeComponent();
private void Print(string text)
BeginInvoke(() =>
richTextBox1.AppendText(text);
richTextBox1.ScrollToCaret();
richTextBox1.Refresh();
);
private void BtnStart_Click(object sender, EventArgs e)
TokenSource = new();
ManualReset = new(true);
int i = 0;
Task.Run(() =>
while (!TokenSource.Token.IsCancellationRequested)
ManualReset.WaitOne(); //根据是否收到信号判断是否阻塞当前线程
Thread.Sleep(200);
Print($"线程【Environment.CurrentManagedThreadId】正在运行第++i次Environment.NewLine");
, TokenSource.Token);
private void BtnPause_Click(object sender, EventArgs e)
ManualReset?.Reset();
private void BtnContinue_Click(object sender, EventArgs e)
ManualReset?.Set();
private void BtnStop_Click(object sender, EventArgs e)
TokenSource?.Cancel();
扩展:取消令牌CancellationTokenSource的用法
第一种用法:注册事件
当执行取消动作以后,将会执行注册的事件
private void button1_Click(object sender, EventArgs e)
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(3000);
CancellationToken cancellationToken = cancellationTokenSource.Token;
cancellationToken.Register(() => System.Console.WriteLine("我被取消了."));
System.Console.WriteLine("先等五秒钟.");
System.Console.WriteLine("手动取消.");
cancellationTokenSource.Cancel();
第二种用法:定时取消
通过构造函数,传入时间值
//设置3000毫秒(即3秒)后取消
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(3000);
CancellationToken cancellationToken = cancellationTokenSource.Token;
cancellationToken.Register(() => System.Console.WriteLine("我被取消了."));
System.Console.WriteLine("先等五秒钟.");
System.Console.WriteLine("手动取消.")
cancellationTokenSource.Cancel();
CancelAfter方法,传入时间值
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Token.Register(() => System.Console.WriteLine("我被取消了."));
//五秒之后取消
cancellationTokenSource.CancelAfter(5000);
System.Console.WriteLine("不会阻塞,我会执行.");
第三种用法:关联取消
设置一组关联的CancellationTokenSource,当其中的一个被取消了,那么其他的都被取消
private void button1_Click(object sender, EventArgs e)
//声明几个CancellationTokenSource
CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationTokenSource tokenSource2 = new CancellationTokenSource();
CancellationTokenSource tokenSource3 = new CancellationTokenSource();
tokenSource2.Token.Register(() => System.Console.WriteLine("tokenSource2被取消了"));
//创建一个关联的CancellationTokenSource
CancellationTokenSource tokenSourceNew = CancellationTokenSource.CreateLinkedTokenSource(tokenSource.Token, tokenSource2.Token, tokenSource3.Token);
tokenSourceNew.Token.Register(() => System.Console.WriteLine("tokenSourceNew被取消了"));
//取消tokenSource2
tokenSource2.Cancel();
第四种用法 判断取消(一般作为多线程令牌)
CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = tokenSource.Token;
//打印被取消
cancellationToken.Register(() => System.Console.WriteLine("被取消了."));
//模拟传递的场景
Task.Run(async ()=>
while (!cancellationToken.IsCancellationRequested)
System.Console.WriteLine("一直在执行...");
await Task.Delay(1000);
);
//5s之后取消
tokenSource.CancelAfter(5000);
多线程开发,先学会线程池吧
思维导图
前言
在实际开发场景中,我们经常要使用多线程开发应用,比如实现异步操作,或者为了提高程序的效率等等。但是以前我见过有实习生在使用的时候是直接new Runable(),然后start()。没有使用线程池,可能很多初学者对线程池在多线程开发中没有足够的认识,所以我写一篇文章讲讲线程池,希望对大家有所启发。
一、什么是线程池
线程池借鉴了"池化"技术的思想,线程池能够对线程的生命周期进行管理,对线程重复利用,并且能够以一种简单的方式将任务的提交与执行相解耦。
二、为什么使用线程池
一种技术的出现,肯定是要解决存在的问题。如果不用线程池,会怎么样呢?很简单,需要时创建线程,线程跑完销毁,如果频繁去做这两个动作,就会造成比较大的资源消耗。所以线程池主要就是解决这个问题。
因此在《java并发编程的艺术》书中就提到以下几点:
-
**降低资源消耗。**通过重复使用已创建的线程,降低线程创建和销毁造成的资源消耗。 -
**提高响应速度。**当有任务到达时,任务可以不需要的等到线程创建就能立即执行。 -
**提高线程的可管理性。**使用线程池可以进行统一的分配,调优和监控。
三、Executor
创建线程池主要使用ThreadPoolExecutor这个类,所以我们先看一张类图。
一般来说,遵守面向接口编程的思想,我们都喜欢使用ExecutorService接口接收线程池实例。如下:
public static void main(String[] args) throws Exception {
//创建线程池
ExecutorService executor = new ThreadPoolExecutor(10, 10, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>(10));
}
这里可以看到创建线程池是使用ThreadPoolExecutor构造器来创建。构造器的参数有什么意义呢,继续往下看。
3.1 七个关键参数
/**
* corePoolSize 核心线程数
* maximumPoolSize 最大线程数
* keepAliveTime 线程存活时间
* unit keepAliveTime的时间单位,有日,小时,分钟,秒等等
* workQueue 工作队列
* threadFactory 线程工厂,用于创建线程
* handler 饱和策略
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//省略...
}
那么这7个参数,在线程池工作时,起到什么作用呢?直接看一张图就明白了。
这里有两个参数需要讲解一下,工作队列workQueue和饱和策略handler。
工作队列的类是BlockingQueue,是一个接口,我们先看看类图,看一下有哪些子类可以使用。
可以看到有很多实现的子类,功能也各有不同。下面讲几个有代表性的。
DelayQueue是无界的队列,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。
LinkedBlockingDeque是基于双向链表实现的双向并发阻塞队列,该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(添加或删除);并且该阻塞队列是支持线程安全。可以指定队列的容量,如果不指定默认容量大小是Integer.MAX_VALUE
。
ArrayBlockingQueue是基于数组实现的有界阻塞队列,此队列按先进先出的原则对元素进行排序。新元素插入到队列的尾部,获取元素的操作则从队列的头部进行。
PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高或者最低的元素(规则可以通过实现Comparable接口自己制定),内部是使用平衡二叉树实现的,遍历不保证有序。
饱和策略只要看RejectedExecutionHandler接口,以及其实现子类。
饱和策略主要有四种,如果要自定义饱和策略也很简单,实现RejectedExecutionHandler接口,重写rejectedExecution()方法即可。下面介绍JDK里的四种饱和策略。
-
AbortPolicy,直接抛出异常,简单粗暴。 -
CallerRunsPolicy,在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务。 -
DiscardPolicy,什么都不做,既不抛出异常,也不会执行。 -
DiscardOldestPolicy,当任务被拒绝添加时,会抛弃任务队列中最旧的任务(也就是最先加入队列的任务),再把这个新任务添加进去。
3.2 Executors
Executors类提供了四种线程池,根据使用不同的参数去new ThreadPoolExecutor实现。简单介绍一下。
第一种是newFixedThreadPool,这是创建固定大小的线程池,核心线程数和最大线程数都设置相同的值,使用LinkedBlockingQueue作为工作队列,当corePoolSize满了之后就加入到LinkedBlockingQueue队列中。LinkedBlockingQueue默认大小为Integer.MAX_VALUE,所以会有OOM的风险。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
第二种是newSingleThreadExecutor,创建线程数为1的线程池,并且使用了LinkedBlockingQueue,核心线程数和最大线程数都为1,满了就放入队列中,执行完了就从队列取一个。也就是创建了一个具有缓冲队列的单线程的线程池。跟上面的问题一样,队列的容量默认是Integer.MAX_VALUE,也会有OOM的风险。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
第三种是newCachedThreadPool,创建可缓冲的线程池,没有大小限制。核心线程数是0,最大线程数是Integer.MAX_VALUE,所以当有新任务时,任务会放入SynchronousQueue队列中,SynchronousQueue只能存放大小为1,所以会立刻新起线程。如果在工作线程在指定时间(60秒)空闲,则会自动终止。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
第四种是newScheduledThreadPool,支持定时及周期性任务执行的线程池。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
3.3 使用规范
在阿里java开发规范中,是强制不允许使用Executors创建线程池,我们不妨看看。
假如有人头铁不信,那我们写一段代码模拟一下。
public class ThreadTest {
private static AtomicInteger num = new AtomicInteger();
public static void main(String[] args) throws Exception {
//创建线程池
ExecutorService executor = Executors.newCachedThreadPool();
while (true) {
executor.execute(() -> {
try {
System.out.println("线程数:" + num.incrementAndGet());
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}
然后设置JVM的参数-Xms5M -Xmx5M
,运行一小段时间,就会看到报错了。
第二个问题是线程数的设置,设置多少线程数比较合适呢?
如果是cpu密集型的应用,cpu密集的意思是执行的任务大部分时间是在做计算和逻辑判断,这种情况显然不能设置太多的线程数,否则花在线程之间的切换时间就变多,效率就会变得低下。所以一般这种情况设置线程数为cpu核数+1即可。
cpu核数可以通过Runtime
获取。
Runtime.getRuntime().availableProcessors()
如果是IO密集型的应用,IO密集的意思是执行的任务需要执行大量的IO操作,比如网络IO,磁盘IO,对CPU的使用率较低,因为在IO操作的特点需要等待,那么就可以把CPU切换到其他线程。所以可以设置线程数为CPU核数的两倍+1。
絮叨
经过学习之后,我们就要养成使用多线程不能直接new一个Thread,然后start(),要有使用线程池的意识。其次要理解线程池参数的意义,根据实际情况去设置。
并发编程往往是实际开发中比较容易出问题,希望看完这篇文章能减少一些不必要的错误。
觉得有用就点个赞吧,你的点赞是我创作的最大动力~
我是一个努力让大家记住的程序员。我们下期再见!!!
能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!
以上是关于多线程开发的主要内容,如果未能解决你的问题,请参考以下文章