开发过程中的多线程思维方式_多线程程序开发

yumo66613小时前技术文章2

作为一个多项目管理者,我每天经常有大量并行的工作内容,尤其是以前在工作里同时协调着十几个经销商、技术外包、设计团队和工厂、还同时进行着公司内部的开发和管理工作的时候,曾经一度崩溃。

以我的经验来看,最有效的办法是让自己养成一种“多线程工作思维”

我们的时间都去哪了?

确实能感觉到,这些年大家工作普遍越来越忙,同时要处理的工作任务变多也变得越来越常见。

可每当这些时候,相信你和我都有一个感受,那就是:如果每个工作任务的工作量是1,那么一起来5个并行工作任务的工作时间,往往是大于5的,甚至远大于5。

那时间到底消耗在了哪些地方?


其实这种情况对于机器来言也是一样的,如何最大化利用一切可以利用的时间!

之前对接某部的数据平台时需要在规定时间内进行增量数据的访问文件、压缩、切片、传输等操作,然后频繁执行上链操作,经常遇到线程阻塞超时等现象,所以采用了多线程的方式来处理该业务逻辑;并且在与银行等机构进行数据交互时通常也会用到多线程操作。

创建线程对象不像其他对象一样在JVM分配内存即可,还要调用操作系统内核的API,然后操作系统为线程分配一系列的资源,这个成本就很高了。所以线程是一个重量级对象,如同数据库连接一样,应该避免频繁创建和销毁等操作,这个操作其实完全可以和类似C3P0数据库连接池等同对待。

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

1、吞吐量:做WEB开发的话,使用的中间件容器帮我们做了多线程,tomcat内部采用的就是多线程,但是也只能帮我们做到请求层面。换句话说就是一个请求一个线程或者多个请求一个线程。如果是单线程,那就同时只能处理一个用户的请求。如果不采用多线程机制,上百个人同时访问一个web应用的时候,tomcat就得排队串行处理了,那样客户端根本是无法忍受那种访问速度的。

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

因为我们都是做web开发的,第1点可能我们几乎不会涉及,所以这里主要说下第2点。

假设有个请求,这个请求服务端的处理需要执行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都不到。

public class MyService {

private static Map<String, String> fileName2Data = new HashMap<String, String>();

private void processFile(String fileName) {
  String data = fileName2Data.get(fileName);
  if (data == null) {
    data = readFromFile(fileName);// 耗时28ms
    fileName2Data.put(fileName, data);
  }
  // TODO:process with date
}

private String readFromFile(String fileName) {
  return "";
}

}

看起来好像还不错,建立一个文件名和文件数据的映射。如果读取一个map中已经存在的数据,那么就不用读取文件了。

可是问题在于,服务请求是并发操作,上面会导致一个很严重的问题:死循环。因为,HashMap在并发修改的时候,可能是导致循环链表的构成。(具体可以自行阅读HashMap源码)。

那就用ConcurrentHashMap吧,Concurrent意思是并存的、同时发生的;他是一个线程安全的HashMap,这样就能轻松解决问题。

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class MyService {

private static ConcurrentMap<String, String> fileName2Data = new ConcurrentHashMap<String, String>();

private void processFile(String fileName) {
  String data = fileName2Data.get(fileName);
  if (data == null) {
    data = readFromFile(fileName);// 耗时28ms
    fileName2Data.put(fileName, data);
  }
  // TODO:process with date
}

private String readFromFile(String fileName) {
  return "";
}

}

这样虽然只要有用户访问过文件a,那另一个用户想访问文件a,也会从fileName2Data中拿数据,然后也不会引起死循环。可是,你会发现一个问题,1000个用户首次访问同一个文件的时候,居然读取了1000次文件(这是最极端的,可能只有几百)。

因为Servlet是多线程的,那么下面注释的“偶然事件”,这是完全有可能的,因此,这样做还是有问题。

private void processFile(String fileName) {
	String data = fileName2Data.get(fileName);
	// "偶然事件"--1000个线程同时执行到这里并且同时发现为null
	if (data == null) {
		data = readFromFile(fileName);// 耗时28ms
		fileName2Data.put(fileName, data);
	}
	// TODO:process with date
}

因此,可以自己简单的封装一个任务来处理。接口
util.concurrent.ExecutorService 表述了异步执行的机制,并且可以让任务在后台执行。

import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;

public class MyService {

private static ConcurrentMap<String, FutureTask> fileName2Data = new ConcurrentHashMap<String, FutureTask>();

private static ExecutorService exec = Executors.newCachedThreadPool();

private void processFile(String fileName) {
	FutureTask data = fileName2Data.get(fileName);
	// "偶然事件"--1000个线程同时执行到这里并且同时发现为null
	if (data == null) {
		data = newFutureTask(fileName);
		FutureTask old = fileName2Data.putIfAbsent(fileName, data);
		if (old == null) {
			exec.execute(data);
		}
	}
	String str = data.get();
	// TODO:process with date
}

private FutureTask newFutureTask(final String fileName) {
	return new FutureTask(new Callable<String>() {
		public String call() {
			return readFromFile(fileName);
		}

		private String readFromFile(String fileName) {
			return "";
		}
	});
}

}

多线程最多的场景:web服务器本身;各种专用服务器(如游戏服务器);

多线程的常见应用场景:

1、后台任务,例如:定时向大量(100w以上)的用户发送邮件;

2、异步处理,例如:发微博、记录日志等;

3、分布式计算等

相关文章

Synchronized的实现原理详解(看这篇就够了)

谈到多线程就不得不谈到Synchronized,重要性不言而喻,今天主要谈谈Synchronized的实现原理。Synchronizedsynchronized关键字解决的是多个线程之间访问资源的同步...

探讨C语言系统编程中线程的原理以及实现

点击蓝字 关注我们线程的概念我们今天来聊一聊线程,之前有写过一篇关于进程的文章,今天我们聊的线程,和进程差不多,我们首先要知道的一件事情是一个进程里面可以包括多个线程,不能反过来,我们之前了解到的不同...

Java多线程问题大揭秘:从底层原理到解决方案

并发编程为什么会出问题?现代计算机为了提高计算机的整体能力,操作系统做出了以下努力:CPU增加了缓存CPU对于数据的计算速度远远高于从内存中存取数据的速度,为了缓和CPU与内存之间的速度差异,计算机的...

解析C#中的多线程编程机制:Thread、ThreadPool、Task和Parallel

Thread、ThreadPool、Task和Parallel是C#中用于多线程编程和并行处理的不同机制。每个机制都有自己的原理和使用方式。可以根据需求选择适当的机制来实现并发性和并行性,并结合示例进...

彻底了解线程池的原理——40行从零开始自己写线程池

前言在我们的日常的编程当中,并发是始终离不开的主题,而在并发多线程当中,线程池又是一个不可规避的问题。多线程可以提高我们并发程序的效率,可以让我们不去频繁的申请和释放线程,这是一个很大的花销,而在线程...

Redis不是号称单线程效率也很高吗,为什么又采用多线程了?

Redis是目前广为人知的一个内存数据库,在各个场景中都有着非常丰富的应用,前段时间Redis推出了6.0的版本,在新版本中采用了多线程模型。因为我们公司使用的内存数据库是自研的,按理说我对Redis...