Java:多线程:到底什么时候该用多线程

一、高并发

系统接受实现多用户多请求的高并发时,通过多线程来实现。

二、线程后台处理大任务

一个程序是线性执行的。如果程序执行到要花大量时间处理的任务时,那主程序就得等待其执行完才能继续执行下面的。那用户就不得不等待它执行完。

这时候可以开线程把花大量时间处理的任务放在线程处理,这样线程在后台处理时,主程序也可以继续执行下去,用户就不需要等待。线程执行完后执行回调函数。

三、大任务

大任务处理起来比较耗时,这时候可以起到多个线程并行加快处理(例如:分片上传)。

连续的操作,需要花费忍无可忍的过长时间才可能完成
并行计算
为了等待网络、文件系统、用户或其他I/O响应而耗费大量的执行时间
所以说,在动手之前,先保证自己的应用程序中是否出现了以上3种情形。

文章一:

我想大多数人在学习多线程时都会对此问题有所顾虑,尽管多线程的概念不难理解,那我们什么时候该用它呢?在大多数情况下,我们写了程序,发现有时必须使用多线程才能得到理想的运行结果,于是我们按照资料调用相关的线程类库或API改善程序,并使其正常运行;但是,到底存不存在一种判断依据,能够明确的指导我们正确地使用多线程机制来解决问题呢?笔者对此进行了一番思考,在此说说我的想法以供参考。

在开始之前,先引入几个问题,这些问题最终都会在这篇文章里找到答案。

问题情景[0]:设计一个简单的UI:包括一个文本标签和一个按钮,在点击按钮时文本显示由0~10的增长,每秒增长量为1。

问题情景[1]:某同学编写的坦克大战程序中,每一个坦克和子弹均使用一个独立的线程,这是否合理?(当然不合理。。。)如果是你,你会怎么编写这个程序?

笔者认为,多线程的使用离不开“阻塞”这个概念,不过,我想先对这个概念加以扩充,首先先来回想一下阻塞概念原本的意思,简单的说,就是程序运行到某些函数或过程后等待某些事件发生而暂时停止CPU占用的情况;也就是说,是一种CPU闲等状态,不过有时我们使用多线程并不一定是保持闲等时的程序响应,例如在追求高性能的程序中,某条线程在进行高强度的运算,此时若对运算性能不满意,我们也许会再启动若干条运算线程(当然,是在CPU有运算余力的情况下),此时,高强度运算应该归为一种“忙等”状态。

说到这,多线程归根究底是为了解决"等"的问题,那我们这样定义一个阻塞过程:程序运行该过程所消耗的时间有可能在运行上下文间产生明显的卡顿;这里使用“可能”是因为有些情况下,诸如Socket通信,如果数据源源不断的进入,那么阻塞的时间可能非常小,但我们还是使用了一条线程(nio另说)来处理它,因为我们无法保证数据到来的持续性和有效性;"卡顿"带有主观臆想,也就是说是使用者(人或一些自动化程序)不可接受的。

接下来,对什么时候使用多线程做一个回答:编写程序过程中需要使用某些阻塞过程时,我们才使用多线程,或者更进一步讲,使用多线程的目的是对阻塞过程中的实际阻塞的抽象提取。前半句话应该很好理解,而后面的一句虽然不太好懂,不过它对一个程序应具有的合理线程数量进行了阐释(这点接下来解释)。

好了,接下来我们回顾一些两个问题,并对它们做出解答:

问题情景[0]

为了方便表达,笔者在此采用伪Java代码来阐释解答过程。首先我们有一个Label textShower用于显示文本,Button textChanger作为点击的按钮

这个问题是笔者还是一名小菜时遇到的,当时笔者是这么写的:

public class MyFrame
{
    Label textShower;
    Button textChanger;
    public MyFrame//实例化等省略
    {
       textChanger.setOnClickListener(new OnClickListener(){
	public void onClick(MouseEvent e){
		for(int i = 1;i <= 10;i++){
			textShower.setText(i+"");//设置文字
			Thread.sleep(1000);//等待一秒
		}
	}
	});
    }
}

程序的执行结果是点击后10秒没有响应,然后数值被设定为10;现在知道了,是由于AWT消息线程同时负责着图像的绘制刷新操作,而Thread.sleep属于之前的阻塞过程,导致画面停止响应。

当时老师是这么教给我的:

public class MyFrame
{
    Label textShower;
    Button textChanger;
    public MyFrame//实例化等省略
    {
       textChanger.setOnClickListener(new OnClickListener(){
	public void onClick(MouseEvent e){
	new Thread(){
		public void run()
		{
			for(int i = 0;i < 10;i++){
				textShower.setText(i+"");//设置文字
				Thread.sleep(1000);//等待一秒
			}	
		}
	}.start();
	}
	});
    }
}

当然,这样确实能够满足题目要求,我也因此开心了一阵,不过不久我就有了新的问题:每按一次按钮产生一个线程是否合理呢?如果这样的文本组合再多几个,我也要创建更多的线程吗?要是使用者是个熊孩子来这里一通狂按这程序还受得了么…

后来,在面向对象思想深入人心后,稍微懂面向对象的人都会知道使用抽象来简化程序,只不过在上面的问题中,我们需要抽象的不是具体的实体,而是“实际阻塞”这种抽象概念。

在上面的代码中,笔者写的第一个onClick函数属于一个阻塞过程,其中sleep属于“实际阻塞”。

而改版的代码中,只是使用多线程将原本的阻塞过程变为了非阻塞过程,实际上是使用了一个独立的线程将整个阻塞过程包含在内,并没有做任何的抽象。

问题情景[1]:在这个问题中,将主要讨论实际阻塞的抽象和合理线程数量的问题。

这个情景是不久前一位网友问我的,他的毕业设计是编写一个坦克大战的游戏,在编的差不多的时候,突然想到每一辆坦克、每一发子弹都是用单独的线程不是很合理,问我如何改进。用这个例子说明实际阻塞的抽象再合适不过了,我们先看看他写的代码片段:

坦克类:

public class Tank extends Thread{
	float x;//这里以横向移动为例子,只写一个属性
	float speed = 1f;
	public void run()
	{
		drawtank();//清除上一次的绘制,根据横坐标x画一个坦克
		x+=speed;
		Thread.sleep(17);//约合一秒60次
	}
}

子弹类:

public class Bullet extends Thread{
	float x;//这里以横向移动为例子,只写一个属性
	float speed = 10f;
	public void run()
	{
		drawbullet();//清除上一次的绘制,根据横坐标x画一个子弹
		x+=speed;
		Thread.sleep(17);//约合一秒60次
	}
}

其实这样异步的绘制会使画面产生明显的抖动,而且用于同步的逻辑也十分复杂,并不是一个好的方案。

其实上面两个类中的run方法中,只有sleep属于实际阻塞,也就是说是可以被抽象出来的,我们只要一个线程,每过17毫秒执行一些列非阻塞过程即可。

上述过程中,绘制及坐标的运算属于非阻塞过程,我们将其抽象为一个接口:

public interface Drawable
{
	public void draw();
}

之后我们书写抽象实际阻塞的线程类:

public class BlockThread extends Thread 
{
	Collection<Drawable> c = new Collection<Drawable>();
	public void run()
	{
		for(Drawable d:c)
		{
			d.draw();
		}
		Thread.sleep(17);
	}
	//封装对成员c的同步CRUD不赘述
	public void addDrawable(Drawable d);
	public void removeDrawable(Drawable d);
	...
}

最后,坦克和子弹的改动:
坦克类:

public class Tank implements Drawable{
	float x;//这里以横向移动为例子,只写一个属性
	float speed = 1f;
	@Override
	public void draw()
	{
		drawtank();//清除上一次的绘制,根据横坐标x画一个坦克
		x+=speed;
	}
}

子弹类:

public class Bullet implements Drawable{
	float x;//这里以横向移动为例子,只写一个属性
	float speed = 10f;
	@Override
	public void draw()
	{
		drawbullet();//清除上一次的绘制,根据横坐标x画一个子弹
		x+=speed;
	}
}

我们可以发现:原有的实际阻塞过程已经被抽象到一个线程之中,而非阻塞过程,诸如绘制和坐标运算依然作为方法保留到对应类中,这样,无论有多少坦克和炮弹,只要非阻塞过程的运算压总和力不至于逼近阻塞的程度,使用一个线程即可完成所有工作。

而且,如果想要添加游戏元素,例如其他类型的子弹,只需要实现Drawable接口即可。

写到这,UI的问题也就解决了,诸如sleep这样纯粹延时的阻塞非常容易抽象,我们可以如法炮制,使用一个线程解决所有的数值延时自增的问题。但并不是所有实际阻塞都易于抽象,如socket.read(byte[] b);这样的方法显然没有抽象的余地,因此才引出后来的nio方案。

最后,我们对什么时候使用多线程,以及使用线程的数量做一个总结:在编写程序时,遇到了阻塞过程而不想使整个程序停止响应时,应使用多线程;一个程序的合理线程数量取决于对实际阻塞的抽象程度。

原文参考:https://blog.csdn.net/shuzhe66/article/details/25484195

文章二:

如果你的应用程序需要采取以下的操作,那么你尽可在编程的时候考虑多线程机制:

连续的操作,需要花费忍无可忍的过长时间才可能完成
并行计算
为了等待网络、文件系统、用户或其他I/O响应而耗费大量的执行时间
所以说,在动手之前,先保证自己的应用程序中是否出现了以上3种情形。

为什么需要多线程(解释何时考虑使用线程)
从用户的角度考虑,就是为了得到更好的系统服务;从程序自身的角度考虑,就是使目标任务能够尽可能快的完成,更有效的利用系统资源。综合考虑,一般以下场合需要使用多线程:
1、 程序包含复杂的计算任务时
主要是利用多线程获取更多的CPU时间(资源)。
2、 处理速度较慢的外围设备
比如:打印时。再比如网络程序,涉及数据包的收发,时间因素不定。使用独立的线程处理这些任务,可使程序无需专门等待结果。
3、 程序设计自身的需要
WINDOWS系统是基于消息循环的抢占式多任务系统,为使消息循环系统不至于阻塞,程序需要多个线程的来共同完成某些任务。

每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。

什么是多线程?

多线程是为了使得多个线程并行的工作以完成多项任务,以提高系统的效率。线程是在同一时间需要完成多项任务的时候被实现的。

使用线程的好处有以下几点:

·使用线程可以把占据长时间的程序中的任务放到后台去处理

·用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度

·程序的运行速度可能加快

·在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。

还有其他很多使用多线程的好处,这里就不一一说明了。

一些线程模型的背景

我们可以重点讨论一下在Win32环境中常用的一些模型。

·单线程模型

在这种线程模型中,一个进程中只能有一个线程,剩下的进程必须等待当前的线程执行完。这种模型的缺点在于系统完成一个很小的任务都必须占用很长的时间。

·块线程模型(单线程多块模型STA)

这种模型里,一个程序里可能会包含多个执行的线程。在这里,每个线程被分为进程里一个单独的块。每个进程可以含有多个块,可以共享多个块中的数据。程序规定了每个块中线程的执行时间。所有的请求通过Windows消息队列进行串行化,这样保证了每个时刻只能访问一个块,因而只有一个单独的进程可以在某一个时刻得到执行。这种模型比单线程模型的好处在于,可以响应同一时刻的多个用户请求的任务而不只是单个用户请求。但它的性能还不是很好,因为它使用了串行化的线程模型,任务是一个接一个得到执行的。

·多线程块模型(自由线程块模型)

多线程块模型(MTA)在每个进程里只有一个块而不是多个块。这单个块控制着多个线程而不是单个线程。这里不需要消息队列,因为所有的线程都是相同的块的一个部分,并且可以共享。这样的程序比单线程模型和STA的执行速度都要块,因为降低了系统的负载,因而可以优化来减少系统idle的时间。这些应用程序一般比较复杂,因为程序员必须提供线程同步以保证线程不会并发的请求相同的资源,因而导致竞争情况的发生。这里有必要提供一个锁机制。但是这样也许会导致系统死锁的发生。

原文参考:http://blog.sina.com.cn/s/blog_4ee24ced0101375f.html

文章三:

“IO操作的DMA(Direct Memory Access)模式”开始讲起。DMA即直接内存访问,是一种不经过CPU而直接进行内存数据存储的数据交换模式。通过DMA的数据交换几乎可以不损耗CPU的资源。在硬件中,硬盘、网卡、声卡、显卡等都有DMA功能。CLR所提供的异步编程模型就是让我们充分利用硬件的DMA功能来释放CPU的压力。

多线程使用的主要目的在于:

1、吞吐量:你做WEB,容器帮你做了多线程,但是他只能帮你做请求层面的。简单的说,可能就是一个请求一个线程。或多个请求一个线程。如果是单线程,那同时只能处理一个用户的请求。

2、伸缩性:也就是说,你可以通过增加CPU核数来提升性能。如果是单线程,那程序执行到死也就利用了单核,肯定没办法通过增加CPU核数来提升性能。

鉴于你是做WEB的,第1点可能你几乎不涉及。那这里我就讲第二点吧。

–举个简单的例子:
假设有个请求,这个请求服务端的处理需要执行3个很缓慢的IO操作(比如数据库查询或文件查询),那么正常的顺序可能是(括号里面代表执行时间):
a、读取文件1(10ms)
b、处理1的数据(1ms)
c、读取文件2(10ms)
d、处理2的数据(1ms)
e、读取文件3(10ms)
f、处理3的数据(1ms)
g、整合1、2、3的数据结果(1ms)
单线程总共就需要34ms。
那如果你在这个请求内,把ab、cd、ef分别分给3个线程去做,就只需要12ms了。

所以多线程不是没怎么用,而是,你平常要善于发现一些可优化的点。然后评估方案是否应该使用。
假设还是上面那个相同的问题:但是每个步骤的执行时间不一样了。
a、读取文件1(1ms)
b、处理1的数据(1ms)
c、读取文件2(1ms)
d、处理2的数据(1ms)
e、读取文件3(28ms)
f、处理3的数据(1ms)
g、整合1、2、3的数据结果(1ms)
单线程总共就需要34ms。
如果还是按上面的划分方案(上面方案和木桶原理一样,耗时取决于最慢的那个线程的执行速度),在这个例子中是第三个线程,执行29ms。那么最后这个请求耗时是30ms。比起不用单线程,就节省了4ms。但是有可能线程调度切换也要花费个1、2ms。因此,这个方案显得优势就不明显了,还带来程序复杂度提升。不太值得。

那么现在优化的点,就不是第一个例子那样的任务分割多线程完成。而是优化文件3的读取速度。
可能是采用缓存和减少一些重复读取。
首先,假设有一种情况,所有用户都请求这个请求,那其实相当于所有用户都需要读取文件3。那你想想,100个人进行了这个请求,相当于你花在读取这个文件上的时间就是28×100=2800ms了。那么,如果你把文件缓存起来,那只要第一个用户的请求读取了,第二个用户不需要读取了,从内存取是很快速的,可能1ms都不到。

不使用线程池的缺点

有些开发者图省事,遇到需要多线程处理的地方,直接new Thread(…).start(),对于一般场景是没问题的,但如果是在并发请求很高的情况下,就会有些隐患:

  • 新建线程的开销。线程虽然比进程要轻量许多,但对于JVM来说,新建一个线程的代价还是挺大的,决不同于新建一个对象
  • 资源消耗量。没有一个池来限制线程的数量,会导致线程的数量直接取决于应用的并发量,这样有潜在的线程数据巨大的可能,那么资源消耗量将是巨大的
  • 稳定性。当线程数量超过系统资源所能承受的程度,稳定性就会成问题

原文参考:https://blog.csdn.net/wujizkm/article/details/50651808

文章四:

在不希望等待一个耗时任务的返回结果时,会涉及到多线程,比如下载三个文件,可以在当前线程开启三个新线程分别执行,而不影响当前线程继续运行
在spring的项目中,一个请求就是一个线程

为什么可以多线程以及多线程有什么意义?然后你就知道自己什么时候需要用到了。

CPU从以前的单CPU单核发展为多核、多CPU、重核等,这是多线程可以实现的基础

多线程即意味着多个任务(子任务)可以同时执行(当然,只是宏观上),若是单线程,只能一个接一个顺序执行。
使用多线程,最直接的目的就是希望任务完成的更快。

当然,多线程也会产生一些不希望看到的问题

对java的线程类Thread来进行说明:
1:Thread是针对是java其本身所具有的,但并不能说其没有调用操作系统,其最底层的时间片调度是按照操作系统来执行的。
Thread下可以创建Thread,
2个Thread在一定条件下也可以相互调用。
根据以上特点可以总结认为java中的线程能让高级程序员更好的对庞大和复杂的数据流进行拆分,重组从而减低各个环节性能需求,通过增加各项负荷达到系统资源分配的最优值

也就是为了更快

对于处理时间短的服务或者启动频率高的要用单线程,相反用多线程!

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值