Java并发编程(一): 并发编程的挑战

本文主要内容出自《Java并发编程的艺术》一书,是对该书内容的归纳和理解,有兴趣的朋友请购买正版阅读全部内容。

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

1. 上下文切换

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

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

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

1.1.1 多线程一定快吗

下面的代码演示串行和并发执行累加操作的时间,请思考下面的代码并发执行一定比串行执行快些吗?

public class ConcurrencyTest {/** 执行次数 */private static final long count = 10000l;public static void main(String[] args) throws InterruptedException {//并发计算concurrency();//单线程计算serial();}private static void concurrency() throws InterruptedException {long start = System.currentTimeMillis();Thread thread = new Thread(new Runnable() {@Overridepublic void run() {int a = 0;for (long i = 0; i < count; i++) {a += 5;}System.out.println(a);}});thread.start();int b = 0;for (long i = 0; i < count; i++) {b--;}long time = System.currentTimeMillis() - start;thread.join();System.out.println("concurrency :" + time + "ms,b=" + b);}private static void serial() {long start = System.currentTimeMillis();int a = 0;for (long i = 0; i < count; i++) {a += 5;}int b = 0;for (long i = 0; i < count; i++) {b--;}long time = System.currentTimeMillis() - start;System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a);}}

答案是不一定,测试结果如表1-1所示:
表1-1 测试结果

循环次数 串行执行耗时(单位ms) 并发执行耗时 并发比串行快多少
1亿 130 77 约1倍
1千万 18 9 约1倍
1百万 5 5 差不多
10万 4 3
1万 0 1

从表1-1可以发现当并发执行累加操作不超过百万次时,速度会比串行执行累加操作要慢。那么为什么并发执行的速度还比串行慢呢?因为线程有创建和上下文切换的开销。

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

下面我们来看看有什么工具可以度量上下文切换带来的消耗。

  • 使用Lmbench3可以测量上下文切换的时长。
  • 使用vmstat可以测量上下文切换的次数。

下面是利用vmstat测量上下文切换次数的示例。

$ vmstat 1procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----r b   swpd   free   buff cache   si   so   bi   bo   in   cs us sy id wa st0 0     0 127876 398928 2297092   0   0     0     4   2   2 0 0 99 0 00 0     0 127868 398928 2297092   0   0     0     0 595 1171 0 1 99 0 00 0     0 127868 398928 2297092   0   0     0     0 590 1180 1 0 100 0 00 0     0 127868 398928 2297092   0   0     0     0 567 1135 0 1 99 0 0

CS(Content Switch)表示上下文切换的次数,从上面的测试结果中,我们可以看到其中上下文的每一秒钟切换1000多次。

1.3 如何减少上下文切换

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

  • 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据用ID进行Hash算法后分段,不同的线程处理不同段的数据。
  • CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
  • 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

2. 死锁

锁是个非常有用的工具,运用场景非常多,因为其使用起来非常简单,而且易于理解。但同时它也会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,会造成系统功能不可用。让我们先来看一段代码,这段代码会引起死锁,线程t1和t2互相等待对方释放锁。

public class DeadLockDemo {/** A锁 */private static String A = "A";/** B锁 */private static String B = "B";public static void main(String[] args) {new DeadLockDemo().deadLock();}private void deadLock() {Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {synchronized (A) {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (B) {System.out.println("1");}}}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {synchronized (B) {synchronized (A) {System.out.println("2");}}}});t1.start();t2.start();}
}

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

一旦出现死锁,业务是可感知的,因为不能继续提供服务了,那么只能通过dump线程看看到底是哪个线程出现了问题,以下线程信息告诉我们是DeadLockDemo类的42行和31号引起的死锁:

"Thread-2" prio=5 tid=7fc0458d1000 nid=0x116c1c000 waiting for monitor entry [116c1b000]java.lang.Thread.State: BLOCKED (on object monitor)at com.ifeve.book.forkjoin.DeadLockDemo$2.run(DeadLockDemo.java:34)- waiting to lock <7fb2f3ec0> (a java.lang.String)- locked <7fb2f3ef8> (a java.lang.String)at java.lang.Thread.run(Thread.java:695)"Thread-1" prio=5 tid=7fc0430f6800 nid=0x116b19000 waiting for monitor entry [116b18000]java.lang.Thread.State: BLOCKED (on object monitor)at com.ifeve.book.forkjoin.DeadLockDemo$1.run(DeadLockDemo.java:23)- waiting to lock <7fb2f3ef8> (a java.lang.String)- locked <7fb2f3ec0> (a java.lang.String)at java.lang.Thread.run(Thread.java:695)

现在我们介绍下如何避免死锁的几个常见方法。

  • 避免一个线程同时获取多个锁。
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用tryLock(timeout)来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败。

3. 资源限制的挑战

(1)什么是资源限制?
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源的限制。比如服务器的带宽只有2M,某个资源的下载速度是1M每秒,系统启动十个线程下载资源,下载速度不会变成10M每秒,所以在进行并发编程时,要考虑到这些资源的限制。硬件资源限制有带宽的上传下载速度,硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接数和Sorket连接数等。

(2)资源限制引发的问题
并发编程将代码执行速度加速的原则是将代码中串行执行的部分变成并发执行,但是如果某段串行的代码并发执行,但是因为受限于资源的限制,仍然在串行执行,这时候程序不仅不会执行加快,反而会更慢,因为增加了上下文切换和资源调度的时间。例如,之前看到一段程序使用多线程在办公网并发的下载和处理数据时,导致CPU利用率100%,任务几个小时都不能运行完成,后来修改成单线程,一个小时就执行完成了。

(3)如何解决资源限制的问题?
对于硬件资源限制,可以考虑使用集群并行执行程序,既然单机的资源有限制,那么就让程序在多机上运行,比如使用ODPS,hadoop或者自己搭建服务器集群,不同的机器处理不同的数据,比如将数据ID%机器数,得到一个机器编号,然后由对应编号的机器处理这笔数据。

对于软件资源限制,可以考虑使用资源池将资源复用,比如使用连接池将数据库和Sorket连接复用,或者调用对方webservice接口获取数据时,只建立一个连接。

(4)在资源限制情况下进行并发编程
那么如何在资源限制的情况下,让程序执行的更快呢?根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源,带宽和硬盘读写速度。有数据库操作时,要数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞住,等待数据库连接。

4. 小结

本文介绍了在进行并发编程的时候,大家可能会遇到的几个挑战,并给出了一些解决建议。有的并发程序写的不严谨,在并发下如果出现问题,定位起来会比较耗时和棘手。所以对于Java开发工程师,笔者强烈建议多使用JDK并发包提供的并发容器和工具类来帮你解决并发问题,因为这些类都已经通过了充分的测试和优化,解决了本章提到的几个挑战。

Java并发编程(一):并发编程的挑战相关推荐

  1. Java并发编程:并发容器之CopyOnWriteArrayList(转载)

    Java并发编程:并发容器之CopyOnWriteArrayList(转载) 原文链接: http://ifeve.com/java-copy-on-write/ Copy-On-Write简称COW ...

  2. java并发排它锁_Java并发编程进阶——锁(解析)

    一.锁是什么 java开发中进行并发编程时针对操作同一块区域时,如果不加锁会出现并发问题,数据不是自己预计得到的值.我觉得有点像mysql事务中脏读.不可重复读.幻读的问题.加锁的目的是为了保证同一时 ...

  3. Java并发编程以及并发须知的几个概念:什么是线程安全?

    众所周知,在Java的知识体系中,并发编程是非常重要的一环,也是面试的必问题,一个好的Java程序员是必须对并发编程这块有所了解的.为了追求成为一个好的Java程序员,我决定从今天开始死磕Java的并 ...

  4. java并发常量_Java并发编程-常量对象(七)

    在创建后状态不再发生改变的对象称作常量对象(Immutable Objects).常量对象其可靠性使其广泛地用作开发简单可靠代码的策略.常量对象在开发并发程序中非常有用.由于创建后不能被改变状态,它们 ...

  5. 【Java并发编程】并发编程大合集

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容 ...

  6. Java 从多线程到并发编程(五)—— 线程调度 优先级倒置(反转) 阻塞 死锁 suspend

    文章目录 前言 ´・ᴗ・` 线程调度策略 优先级倒置问题 优先级倒置解决方案 死锁 dead lock suspend 被阻塞的同时持有资源不放 是上述问题的诱因 总结 ´◡` 前言 ´・ᴗ・` 这一 ...

  7. 【转】Java并发编程:并发容器之ConcurrentHashMap

    JDK5中添加了新的concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为同步容器将所有对容器状态的访问都串行化了,这样保证了线程的安全性,所以这种方法的代价就是严重降低了 ...

  8. Java编程思想-并发

    21 并发 21.1 并发的多面性 操作系统通常会将进程互相隔离开,使得进程之前相互不干涉.但是操作系统对于进程通常会有数量和开销的限制,导致进程不能无限创建. Java中线程调度采用抢占式,表示调度 ...

  9. Java面试系列之并发编程专题-Java线程池灵魂拷问

    金三银四跳槽季即将来临,想必有些猿友已经蠢蠢欲动在做相关的准备了!在接下来的日子里,笔者将坚持写作.分享Java工程师在面试求职期间的方方面面,包括简历制作.面试场景复现.面试题解答.谈薪技巧 以及 ...

  10. java 并发指南_并发编程 :Concurrent 用户指南 ( 下 )

    原标题:并发编程 :Concurrent 用户指南 ( 下 ) 来源:高广超, www.jianshu.com/p/8cb5d816cb69 20. 锁 Lock java.util.concurre ...

最新文章

  1. 【Linux】40.date设定系统时间
  2. flex采用blazeds实现服务器向客户端推数据
  3. mongoDB连接配置
  4. eolinker使用初体验(一)
  5. 涨知识!Google 黑客常用搜索语句一览 | 原力计划
  6. laravel完全安装手册
  7. c/c++ utf-8与gbk的互相转化
  8. 阿尔卑斯山时间规划法精髓(提高工作效率)
  9. 信度效度难度区分度是什么意思_如何区分信度、效度、难度、区分度?
  10. 数据库的ACID是什么
  11. (待补充)【读书笔记】20190816《码农翻身》——刘欣
  12. WPF界面美化(整体作用到控件),一步步教你使用FirstFloor.ModernUI
  13. 人工智能——文本分类(大作业必备)
  14. DirectX11 使用Cube Mapping 立方体环境贴图实现天空、物体反射效果
  15. 文本关键词的提取算法实验
  16. zencart iis 伪静态设置 测试可用
  17. 两种 Type-C 耳机:模拟耳机 数字耳机
  18. mysql表情符存储设置
  19. Google Earth Engine(GEE)——GEE版本的全球森林火灾信息获取并呈现2001-2020年四川省火灾亮度时间序列分析
  20. 雷达通信 技术《相控阵入门到精通》 视频教程 代码 下载

热门文章

  1. sns.regplot 和 sns.distplot你知道多少
  2. 3344avd_AVD生根
  3. 为什么用红墨水试验检测BGA焊接情况?我教你如何对结果预判!
  4. kotlin和java相互转换的实操
  5. 无法下载图片 App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insec
  6. css绘制安卓机器人
  7. 诺基亚C2-03 - 简单才是首要的诺基亚C2-03
  8. MATLAB实现控制系统的根轨迹分析
  9. 名企笔试:网易游戏2017招聘笔试题(赶去公司)
  10. python整除运算符_Python