热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

多线程相关概念和线程池

多线程相关概念和线程池1.前言其实在平时的工作中用到多线程相关的问题时,总是遇到一个就解决一个,从来没有在宏观上去看它们,也就达不到所谓的看山还是山࿰

多线程相关概念和线程池

1.前言

其实在平时的工作中用到多线程相关的问题时,总是遇到一个就解决一个,从来没有在宏观上去看它们, 也就达不到所谓的"看山还是山,看水还是水"。在系统的总结多线程之前,先总结一些基本的概念

声明:部分观点仅由思考所得,欢迎讨论和指正.

2.多线程

条件:在一个进程下

2.1单cpu下的多线程称之为并发  

2.2多cpu下的多线程称之为并行

单cpu和多cpu的问题,后面还会再讨论,其实可以把多cpu看成单cpu来处理一些问题。

3.多线程的好处

3.1 以更简单的方式高效的使用系统资源(如cpu等)
因为一个程序运行的时候,并不是一直使用cpu,cpu的任务是处理和计算,当有其他操作并不使用cpu的时候(比如说进行读取磁盘文件的时候),这时改变操作顺序,可以减少cpu空闲的时间:
A: 一般情况下,处理多个文件:
5秒读取文件A
2秒处理文件A
5秒读取文件B
2秒处理文件B
---------------------
总共需要14秒
B:采用更好的算法,处理多个文件:
5秒读取文件A
5秒读取文件B + 2秒处理文件A
2秒处理文件B
---------------------
总共需要12秒
分析:在使cpu等资源的利用率高这个方面,明显B方法更好,但是这种算法或者说思想该如何实现呢?
我们看到这种思想的本质是保持cpu等系统资源的繁忙。
实现:那么我们可以采用单线程的方式,只要设置好相关的文件状态即可,达到B这种思想。但是在cpu的使用方面,这种思想有了更好的实现方式,那就是多线程,多线程会抢夺cpu等资源,完全符合了B这种"保持cpu等繁忙"的本质。所以说,实现系统高效的是B这种算法,而多线程让这种算法的代码实现变得简单。这才是多线程真正的好处之一

3.2 多线程的另一个好处是让大部分的用户感觉更快
这个都比较熟悉,对于多个用户来说,同一段时间内,是完成一个用户的全部任务,还是完成多个用户的一部分任务 对用户来说体验更好? 当然是后者了。
 注意:前面提到了单cpu和多cpu的问题,个人以为完全可以把多cpu当作单cpu来看待,多个cpu依然是一种资源,而且每一个cpu都高效的话,整个应用也会高效。 就像单兵,提高个人技巧,无疑在solo中更有优势,如果是一个部队,提高每个人的技巧,整个部队也一定会更优秀。

4.多线程的代价

4.1 有形的代价

A:空间 每开辟一个线程,就会占用一些内存空间
B:时间 每切换一次线程,就会消耗部分时间

4.2 无形的代价

A:多线程实现了前面说到的B算法,它能够保证cpu在一个时间点,只有一个线程在访问,但是对于其他的 资源,比如说全局变量,多个线程并不是按顺序访问的,可能会同时访问,所以要使用锁等办法;在访问两个及以上的资源时,也是不按顺序的,所以会有死锁。也就是说,多线程在cpu方面帮我们实现了算法B而在其他的资源方面,需要我们自己实现算法B,需要手动保证每个资源的访问顺序,而锁,同步,信号量等都是为了实现自己的算法B而产生的一些技术,也可以说是多线程在高效使用cpu时带来的一些副作用;

B:学习成本和使用成本;可能需要花很多时间去学习,更有可能花更多的时间去使用和调试它

注意:再深入思考一下,似乎发现多线程的本质。由单到多,带来的不就是顺序的问题么,多线程帮我们保证了访问cpu的顺序,那么访问其他资源的顺序怎么保证,当然是自己来保证; 所以,"解决多线程相关的问题,就是解决除cpu外的资源的使用顺序问题"。

5.多线程的创建方式和使用

5.1 创建方式

public static void main(String[] args) {/*Thread方式:一种是继承Thread类,一种是像下面这样使用匿名内部类*/Thread thread1 = new Thread(){@Overridepublic void run() {System.out.println("Multi.main(...).new Thread() {...}.run()");}};thread1.start();/*Runnable方式:一种是实现一个接口Runnable,一种是像下面这样使用匿名内部接口*/Runnable runnable = new Runnable() {public void run() {System.out.println("Multi.main(...).new Runnable() {...}.run()");}};Thread thread2 = new Thread(runnable);thread2.start();}/*main method*/

5.2 认识一下两个名词

A.竞态条件:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件

B.临界区:导致竞态条件发生的代码区称作临界区;在临界区中使用适当的同步就可以避免竞态条件。

通过示例来看一下:

public class Counter {protected long count = 0;public void add(long value){this.count = this.count + value; }}


上面的add()方法就是临界区,它会产生竞态条件,当两个线程同时访问这个add方法就可能会导致count 的值不一致。

因为JVM的执行按照下面顺序:

--从内存获取 this.count 的值放到寄存器
--将寄存器中的值增加value
--将寄存器中的值写回内存
如果两个线程交错执行,会出现下面情况:
A:读取 this.count 到一个寄存器 (0)
B:读取 this.count 到一个寄存器 (0)
B:将寄存器的值加2
B:回写寄存器值(2)到内存. this.count 现在等于 2
A:将寄存器的值加3
A:回写寄存器值(3)到内存. this.count 现在等于 3
最终得到的是线程A得到的值,这样的话得到的count值,就不是期望的值。

5.3 线程安全,下面给出一个自己的定义:
一段资源(代码)是线程安全的条件:
A:如果其中仅存在一类资源(代码),这类资源(代码)不被多个线程共同执行写操作。
B:如果还存在指向资源(代码)的引用,且它所指向的资源(代码)是线程安全的
上面两个结合就是线程安全的定义,而且这个定义是递归的。

注意:
1)条件A指明只有写操作才会引起线程不安全
2)上面的条件B是有必要的,看下面的示例:
两个线程都创建了各自的数据库连接,每个连接自身是线程安全的,但它们所连接到的同一个数据库也许不是线程安全的。
比如,2个线程执行如下代码:
检查记录X是否存在,如果不存在,插入X
如果两个线程同时执行,而且碰巧检查的是同一个记录,那么两个线程最终可能都插入了记录:
线程1检查记录X是否存在。检查结果:不存在
线程2检查记录X是否存在。检查结果:不存在
线程1插入记录X
线程2插入记录X
同样的问题也会发生在文件或其他共享资源上。因此,区分某个线程控制的对象是资源本身,还是仅仅到某个资源的引用很重要。


6.线程池

6.1.线程池的概念

6.1.1 为什么要使用线程池(本质)
1)减少了每次都要创建线程的麻烦
2)线程池可以设置合理的线程数目,手动创建线程不方便了解系统已经存在的线程数目。
6.1.2 为什么要使用队列
在使用线程池的时候,可能会有请求无法及时处理,这个时候就需要队列,而且是阻塞队列。阻塞队列的特点是,在队列中没有数据的情况下,线程池中的所有线程都会被自动阻塞,直到有数据放入队列;在队列中填满数据的情况下,队列中的所有请求都会被自动阻塞,直到队列中有空的位置,线程被自动唤醒。 

6.2 线程池相关的类

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池, 而只是一个执行线程的工具。真正的线程池接口是ExecutorService

6.3 如何使用线程池

6.3.1 先看一下ThreadPoolExecutor的构造函数

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize - 池中所保存的线程数,包括空闲线程。
maximumPoolSize-池中允许的最大线程数。
keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit - keepAliveTime 参数的时间单位。
workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。
threadFactory - 执行程序创建新线程时使用的工厂。
handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
ThreadPoolExecutor是Executors类的底层实现。corePoolSize - 池中所保存的线程数,包括空闲线程。
maximumPoolSize-池中允许的最大线程数。
keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit - keepAliveTime 参数的时间单位。
workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。
threadFactory - 执行程序创建新线程时使用的工厂。
handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
ThreadPoolExecutor是Executors类的底层实现。
主要注意的corePoolSize,maximumPoolSize,workQueue这三个概念,后面会讲. 

6.3.2 线程池与队列搭配的使用策略。

了解了线程池和队列概念,下面就是如何使用它们的问题。先看一下所有线程池和队列在使用时的原则:

1)如果运行的线程少于 corePoolSize,则 Executor始终首选添加新的线程,而不进行排队。(如果当前运行的线程小于corePoolSize,则任务根本不会存放,添加到queue中,而是直接抄家伙(thread)开始运行)
2)如果运行的线程等于或多于 corePoolSize,则 Executor始终首选将请求加入队列,而不添加新的线程。
3)如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。

6.3.3 了解原则后,再看一下在这种原则上,线程池和队列的三种常见搭配:

1)直接提交
工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。这里SynchronousQueue可看作它的长度为0
2)无界队列
使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性
3)有界队列
当使用有限的 maximumPoolSizes时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
针对上面三种策略,系统已经有预设置的搭配(设置好最合适的线程池大小和队列大小),能够处理常见的应用场景: 
直接提交-->newScheduledThreadPool 创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
无界队列-->newCachedThreadPool
有界队列-->newFixedThreadPool
除此之外,还有newSingleThreadExecutor,创建一个单线程的线程池,用来支持串行的任务。
这四种实现都是由Executors内的工厂方法产生的,而且JDK文档中强烈建议使用。

6.4.线程池的使用
直接拿来使用即可,关键是分析业务场景,从上面四种已有实现中选择最合适的

public class MultiThread {public static void main(String[] args) {/*创建一个可重用固定线程数的线程池*/ExecutorService executorService = Executors.newFixedThreadPool(2);/*创建实现了Runnable接口对象*/MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();MyThread t4 = new MyThread();MyThread t5 = new MyThread();/*将线程放入池中进行执行*/executorService.execute(t1);executorService.execute(t2);executorService.execute(t3);executorService.execute(t4);executorService.execute(t5);/*关闭线程池*/executorService.shutdown();}
}


7.总结

基本总结了多线程和线程池本质,它们的优点和带来的缺点;后面就开始讲述多线程在"除cpu外的资源的访问顺序"(文章中提到过,这是多线程带来优点的同时,需要解决的问题)多线程带来)方面的详细语法。


推荐阅读
  • Linux服务器密码过期策略、登录次数限制、私钥登录等配置方法
    本文介绍了在Linux服务器上进行密码过期策略、登录次数限制、私钥登录等配置的方法。通过修改配置文件中的参数,可以设置密码的有效期、最小间隔时间、最小长度,并在密码过期前进行提示。同时还介绍了如何进行公钥登录和修改默认账户用户名的操作。详细步骤和注意事项可参考本文内容。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 本文介绍了闭包的定义和运转机制,重点解释了闭包如何能够接触外部函数的作用域中的变量。通过词法作用域的查找规则,闭包可以访问外部函数的作用域。同时还提到了闭包的作用和影响。 ... [详细]
  • 本文介绍了解决Netty拆包粘包问题的一种方法——使用特殊结束符。在通讯过程中,客户端和服务器协商定义一个特殊的分隔符号,只要没有发送分隔符号,就代表一条数据没有结束。文章还提供了服务端的示例代码。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 基于事件驱动的并发编程及其消息通信机制的同步与异步、阻塞与非阻塞、IO模型的分类
    本文介绍了基于事件驱动的并发编程中的消息通信机制,包括同步和异步的概念及其区别,阻塞和非阻塞的状态,以及IO模型的分类。同步阻塞IO、同步非阻塞IO、异步阻塞IO和异步非阻塞IO等不同的IO模型被详细解释。这些概念和模型对于理解并发编程中的消息通信和IO操作具有重要意义。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 本文介绍了Windows操作系统的版本及其特点,包括Windows 7系统的6个版本:Starter、Home Basic、Home Premium、Professional、Enterprise、Ultimate。Windows操作系统是微软公司研发的一套操作系统,具有人机操作性优异、支持的应用软件较多、对硬件支持良好等优点。Windows 7 Starter是功能最少的版本,缺乏Aero特效功能,没有64位支持,最初设计不能同时运行三个以上应用程序。 ... [详细]
  • 关于CMS收集器的知识介绍和优缺点分析
    本文介绍了CMS收集器的概念、运行过程和优缺点,并解释了垃圾回收器的作用和实践。CMS收集器是一种基于标记-清除算法的垃圾回收器,适用于互联网站和B/S系统等对响应速度和停顿时间有较高要求的应用。同时,还提供了其他垃圾回收器的参考资料。 ... [详细]
  • 本文记录了在vue cli 3.x中移除console的一些采坑经验,通过使用uglifyjs-webpack-plugin插件,在vue.config.js中进行相关配置,包括设置minimizer、UglifyJsPlugin和compress等参数,最终成功移除了console。同时,还包括了一些可能出现的报错情况和解决方法。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
author-avatar
手机用户2502878261
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有