问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

并发编程如何入门?看看这篇文章

发布网友 发布时间:2024-09-28 13:16

我来回答

1个回答

热心网友 时间:2024-09-28 18:39

简介:

并发编程的目的是为了让程序运行的更快,但是,并不是启动更多的线程就能让程序最大限度的并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行的更快,会面临非常多的挑战,比如上下文切换的问题、死锁问题,以及受限于硬件和软件的资源限制问题,本篇文章介绍几种并发编程的挑战及解决方案,文章总结至《Java并发编程的艺术》

一、上下文切换

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程执行的时间,因为时间片非常短,所有CPU通过不停的切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一次任务的状态,以便于下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

这就像我们同事读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本英文技术书。这样的切换时会影响读书效率的,同样的道理上下文的切换也会影响多线程的执行速度。

1.1多线程一定快吗

下面的代码演示串行和并发执行并累加操作的时间,分析并发执行一定比串行执行快么?

packagecom.lizba.p1;/***<p>*测试并发执行和串行的速度*</p>**@Author:Liziba*@Date:2021/6/223:40*/publicclassConcurrencyTest{/**执行次数*/privatestaticfinallongcount=10000;publicstaticvoidmain(String[]args)throwsInterruptedException{concurrency();serial();}/***并发执行*@throwsInterruptedException*/privatestaticvoidconcurrency()throwsInterruptedException{longstart=System.currentTimeMillis();Threadthread=newThread(newRunnable(){publicvoidrun(){inta=0;for(longi=0;i<count;i++){a+=5;}}});thread.start();intb=0;for(longi=0;i<count;i++){b--;}thread.join();longtime=System.currentTimeMillis()-start;System.out.println("concurrency:"+time+"ms,b="+b);}/***串行执行*/privatestaticvoidserial(){longstart=System.currentTimeMillis();inta=0;for(longi=0;i<count;i++){a+=5;}intb=0;for(longi=0;i<count;i++){b--;}longtime=System.currentTimeMillis()-start;System.out.println("serial:"+time+"ms,b="+b);}}

时间统计

循环次数串行执行耗时/ms并发执行耗时/ms并发比串行快多少1万05慢10万23慢100万34差不多1000万87差不多1亿5454差不多10亿514508差不多

从上表可以看出,当并发执行累计操作低于百万次时,速度会比串行执行累加操作要慢。为什么在这种情况下并发执行比串行执行要慢呢?这是因为创建线程和上下文切换的时间开销要远远大于简单计算的时间开销。

1.2测试上下文切换次数和时长

测试工具:

使用Lmbench3可以测量上下文切换的时长

使用vmstat可以测量上下文切换的次数

vmstat参数的含义:

参数名含义r表示运行队列(就是说多少个进程真的分配到CPU)b表示阻塞的进程swpd虚拟内存已使用的大小,如果大于0,表示你的机器物理内存不足了,如果不是程序内存泄露的原因,那么你该升级内存了或者把耗内存的任务迁移到其他机器。free空闲的物理内存的大小buffLinux/Unix系统用来存储,目录里面有什么内容,权限等的缓存cache用来记忆我们打开的文件,给文件做缓冲si每秒从磁盘读入虚拟内存的大小,如果这个值大于0,表示物理内存不够用或者内存泄露了,要查找耗内存进程解决掉so每秒虚拟内存写入磁盘的大小,如果这个值大于0,同上bi块设备每秒接收的块数量,这里的块设备是指系统上所有的磁盘和其他块设备,默认块大小是1024bytebo块设备每秒发送的块数量,例如我们读取文件,bo就要大于0。bi和bo一般都要接近0,不然就是IO过于频繁,需要调整in每秒CPU的中断次数,包括时间中断cs每秒上下文切换次数us用户CPU时间sy系统CPU时间,如果太高,表示系统调用时间长,例如IO操作频繁wt等待IOCPU时间#每隔一秒采集数据,一直采集,直到程序终止vmstat1

CS(ContentSwitch)表示上下文切换的次数,从上面的可以看出上下文每秒钟切换1000多次。

1.3如何减少上下文切换

减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。

无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的id按照Hash算法取模分段,不同的线程处理不同段的数据。

CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程处于等待状态。

协程。在单线程里实现多任务调度,并在单线程里维持多个任务见的切换。

1.4减少上下文切换实战

这个例子简单说明如何来减少线程池中大量WAITING线程,来减少上下文切换次数。(本文在Windows环境dump测试)

写一个模拟出现WAITING状态的代码:

packagecom.lizba.p1;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;/***<p>*线程池Dump测试--代码只是示例*</p>**@Author:Liziba*@Date:2021/6/423:26*/publicclassThreadPoolDumpTest{publicstaticvoidmain(String[]args){//创建固定大小的线程池ExecutorServicefixedThreadPool=Executors.newFixedThreadPool(300);//初始化线程池中的线程for(inti=0;i<300;i++){fixedThreadPool.execute(getThread(i));}while(true){try{Thread.sleep(5000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("测试!");}}/***创建线程*@parami*@return*/privatestaticRunnablegetThread(finalinti){returnnewRunnable(){publicvoidrun(){try{Thread.sleep(50);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println(i);}};}}

用jstack命令dump线程信息,可以看当前运行的Java程序的pid,查看当前进程号里的线程在做什么。

#查看Java进程jps

结果:

1216

12176RemoteMavenServer36

18052ThreadPoolDumpTest

18084Launcher

15800Jps

统计所有线程分别处于什么状态,找出处于(onobjectmonitor)阻塞状态的线程。

#dump下快照jstack-l18052>d:\dump.txt

打开dump文件查看处于(onobjectmonitor)阻塞的线程在做什么。

发现有300个线程处于WAITING状态

"pool-1-thread-300"#311prio=5os_prio=0tid=0x000000002fe46800nid=0x4880waitingoncondition[0x0000000033cfe000]java.lang.Thread.State:WAITING(parking)atsun.misc.Unsafe.park(NativeMethod)-parkingtowaitfor<0x000000077b098178>(ajava.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)atjava.util.concurrent.locks.LockSupport.park(LockSupport.java:175)atjava.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)atjava.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)atjava.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)atjava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)atjava.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)atjava.lang.Thread.run(Thread.java:748)Lockedownablesynchronizers:-None

此时如果发现是我们在程序中定义的线程池中的线程,则我们应该适当考虑降低线程池的maxThreads的值。

此处示例中我们修改线程池的固定大小为10:

//创建固定大小的线程池ExecutorServicefixedThreadPool=Executors.newFixedThreadPool(10);

修改maxThread值之后我们可以重启项目。再次dump线程信息,然后重新统计(onobjectmonitor)阻塞的线程数。

再次dump快照分析线程运行情况,发现只有10个线程处于WAITING状态了:

"pool-1-thread-10"#21prio=5os_prio=0tid=0x000000001ecde000nid=0x312cwaitingoncondition[0x00000000212ef000]java.lang.Thread.State:WAITING(parking)

在上面的简单案例中WAITING线程减少了,系统上下文切换的次数就会减少,因为每一次从WAITING到RUNNABLE都会进行一次上下文的切换。在实际开发中,我们并不会做这么看似低级的操作,但是样例却能给我们代理线程池优化和程序线程优化各方面的解决问题的思路。

二、死锁

锁是一个非常有用的工具,运用的场景非常多,因为它使用起来非常简单,而且易于理解。但同时它会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可用。

2.1死锁示例

下面演示一段引起死锁的代码,使得线程t1和线程t2互相等待对方释放锁。

packagecom.lizba.p1;/***<p>*死锁示例代码*</p>**@Author:Liziba*@Date:2021/6/50:37*/publicclassDeadLockDemo{privatestaticfinalStringA="A";privatestaticfinalStringB="B";/***t1\t2互相持有锁*/privatevoiddeadLock(){Threadt1=newThread(newRunnable(){publicvoidrun(){//持有锁Asynchronized(A){try{Thread.currentThread().sleep(2000);}catch(InterruptedExceptione){e.printStackTrace();}//持有锁Bsynchronized(B){System.out.println("holdLockB");}}}});Threadt2=newThread(newRunnable(){publicvoidrun(){//持有锁Bsynchronized(B){try{Thread.currentThread().sleep(2000);}catch(InterruptedExceptione){e.printStackTrace();}//持有锁Asynchronized(A){System.out.println("holdLockA");}}}});t1.start();t2.start();}publicstaticvoidmain(String[]args){newDeadLockDemo().deadLock();}}

这段代码演示的是简单的死锁场景,在现实中大家都不会写出这样的代码。但是,在一些更为复杂的场景中,你可能会遇到这样的问题,比如t1拿到锁之后,因为一些异常情况并没有释放锁(比如死循环)。又或者t1拿到一个数据库锁,释放锁的时候抛出了异常,没有释放掉。

现实中,一旦出现了死锁,业务是可感知的,因为不能继续提供服务了,那么只能通过dump线程查看到底是哪个线程出现了问题,我们分析如下Dump出的线程信息:

"Thread-1"#13prio=5os_prio=0tid=0x000000001e011000nid=0x5318waitingformonitorentry[0x000000001fcef000]java.lang.Thread.State:BLOCKED(onobjectmonitor)atcom.lizba.p1.DeadLockDemo$2.run(DeadLockDemo.java:50)-waitingtolock<0x000000076b042000>(ajava.lang.String)-locked<0x000000076b042030>(ajava.lang.String)atjava.lang.Thread.run(Thread.java:748)Lockedownablesynchronizers:-None"Thread-0"#12prio=5os_prio=0tid=0x000000001e00f800nid=0x4b38waitingformonitorentry[0x000000001fbef000]java.lang.Thread.State:BLOCKED(onobjectmonitor)atcom.lizba.p1.DeadLockDemo$1.run(DeadLockDemo.java:33)-waitingtolock<0x000000076b042030>(ajava.lang.String)-locked<0x000000076b042000>(ajava.lang.String)atjava.lang.Thread.run(Thread.java:748)Lockedownablesynchronizers:-None

从上可以看出第33行和第50行引发了死锁。

2.2避免产生死锁

避免一个线程同时获取多个锁。

避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。

尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。

对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

三、资源限制3.1什么是资源限制

资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或者软件资源。例如,服务器的带宽只有2MB/s,某个资源的下载速度是1MB/s,系统启动10个线程下载资源,下载速度不会变成10MB/s,所以在并发编程时,要考虑这些资源的限制。

硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU处理速度。

软件资源的限制有数据库的连接和socket连接数等。

3.2资源限制引发的问题

在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这样程序不仅不会加快,反而会更慢,因为增加上下文切换和资源调度的时间。

3.3如何解决资源限制的问题

对于硬件资源的限制,可以考虑使用集群并行执行程序

对应软件资源的限制,可以考虑使用资源池将资源复用

3.4在资源限制情况下并发编程

如何在资源限制的情况下,让程序执行的更加快呢?方法就是,根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源-宽带和硬盘的读写速度。有数据库操作时,涉及数据库连接,如果SQL执行非常快,而线程的数量比数据量连接数大很多,则某些线程会被阻塞,等待数据库连接。

本文总结至--《Java并发编程的艺术》/《TheArtofJavaConcurrencyProgramming》

声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
悲观的意思是什么悲观是什么意思 ...坐立不安的。还总想挠挠手呀、胳膊什么的。这是怎么回事啊?是一种... ...胳膊肘麻,有时会麻到感觉大拇指根部疼,食指也有点... ...入睡感觉左胳膊从手腕到肩膀酥溜溜的酸,睁开眼又好了,什么原因... 胳膊上麻溜溜的 像是有小疙瘩 还很痒 有点硬 少量蔗糖,食盐,食油,食醋分别倒入一定量水中,并用筷子不断搅拌,观察... 将食盐 蔗糖 泥土 植物油与水混合 英文会计的provision对应中文会计的哪个词啊? 双人旁一个直一个心念什么,什么意思 白瓷有隙是成语吗 兰州大学的大专毕业证(业余)是国家承认的吗?可以考公务员吗?可以... 沈阳唬哈科技是不是培训公司?面试java的 泡酸菜的传统方法有哪些? 本人应届毕业,想在沈阳学java ,不知哪里好,最好是包就业的 兰州大学信息院的专业情况 切完酸菜手上有味用什么可以去掉? 甘肃省联合中等专业学校学校简介 文曲星V5100能否帮助用户全面提高英语水平? 兰州大学继续教育学院学生培养 兰州大学继续教育学院——打造您职业发展的新引擎 兰州大学网络教育学院办学成果 兰州大学网络教育学院师资力量 通州区张家湾邮编或太玉园的邮编号是多少? 兰州大学网络教育学院—打造您的在线学习之路 如何看浴室玻璃是否有防爆膜 腌过的酸菜怎么清洗才能吃 炖酸菜放什么去臭味,去除酸菜臭味的方法 高铁管家是什么? 高铁管家app人工客服电话。 java程序如何改成绿色版 什么是可见性?为什么会出现”不可见“?Java并发 java和c++的效率(java和c++哪个厉害) java和c++效率差异? 我想学日语该怎么着手啊?我刚大专毕业,学的生物,现在在家因为某些原因想... 成都融创雪世界游玩攻略 你关心的门票、教练、营业时间都在这里_百度... 2023成都融创水世界关门了吗 附闭园时间 java 错误 the method getSource() is undefined for the type MouseEv... 為什么用手按死了螞蟻,手會留下一種味道? 12306的客服在哪里咨询 买车容易养车难啊,大家有没有最省油的SUV推荐,性价比高的最好? 什么车省油质量又好又便宜空间大 tds如何测量 ...以内的车,要求底盘高、结实耐撞、油耗低,有哪些可以选择? ...农村的7万以内买什么裸车好?父亲青睐轿车,油耗低维护便宜,我青睐城 ... ...那个油耗经济一些。主要考虑国产的,性价比高的有那几款? AMD锐龙7 2700X配GTX1070八核主机多少钱? 9700元预算下,R7-2700x搭配RTX2060的迷你主机性能如何? 新民主主义革命胜利的三大法宝之间是什么关系? 新发布AMD锐龙7 2700X配GTX1070主机多少钱? AMD最牛配置:1.6万元R7-2700X+GTX1080Ti高端主机值得买吗?