目录

一、上下文切换

1. 多线程一定快吗

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

3. 如何减少上下文切换

4. 减少上下文切换实战

二、死锁

三、资源限制的挑战

四、本章小结

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

一、上下文切换

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

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

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

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() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
}
});
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);
}
}

上述问题的答案是“不一定”,测试结果如下表所示。

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

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

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

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

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

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

$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 327348 139764 1078188 0 0 6 79 1 0 1 1 97 1 0
0 0 0 327172 139764 1078188 0 0 0 0 718 1376 1 1 99 0 0
0 0 0 327064 139764 1078188 0 0 0 0 554 1043 1 1 99 0 0
0 0 0 327144 139764 1078188 0 0 0 0 568 1108 1 1 98 0 0
1 0 0 326424 139764 1078188 0 0 0 0 576 1104 1 1 99 0 0
0 0 0 326472 139764 1078188 0 0 0 128 548 1039 1 1 98 1 0
0 0 0 326520 139764 1078192 0 0 0 0 572 1095 1 1 98 0 0

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

3. 如何减少上下文切换

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

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

4. 减少上下文切换实战

本节将通过减少线上大量WAITING的线程,来减少上下文切换次数。

第一步:用jstack命令dump线程信息,看看pid为3117的进程里的线程都在做什么。

sudo -u admin /opt/ifeve/java/bin/jstack 31177 > /home/dump17

第二步:统计所有线程分别处于什么状态,发现300多个线程处于WAITING(onobjectmonitor)状态。

$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}' | sort | uniq -c
39 RUNNABLE
21 TIMED_WAITING(on object monitor)
6 TIMED_WAITING(parking)
51 TIMED_WAITING(sleeping)
305 WAITING(on object monitor)
3 WAITING(parking)

第三步:打开dump文件查看处于WAITING(on object monitor)的线程在做什么。发现这些线程基本全是JBOSS的工作线程,在await。说明JBOSS线程池里线程接收到的任务太少,大量线程都闲着。

"http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in Object.wait() [0x0000000052423000]
  java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
    at java.lang.Object.wait(Object.java:485)
    at org.apache.tomcat.util.net.AprEndpoint$Worker.await(AprEndpoint.java:1464)
    - locked <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
    at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1489)
    at java.lang.Thread.run(Thread.java:662)

第四步:减少JBOSS的工作线程数,找到JBOSS的线程池配置信息,将maxThreads降到100。

<maxThreads="250" maxHttpHeaderSize="8192"
  emptySessionPath="false" minSpareThreads="40" maxSpareThreads="75"
  maxPostSize="512000" protocol="HTTP/1.1"
  enableLookups="false" redirectPort="8443" acceptCount="200" bufferSize="16384"
  connectionTimeout="15000" disableUploadTimeout="false" useBodyEncodingForURI= "true">

第五步:重启JBOSS,再dump线程信息,然后统计WAITING(on object monitor)的线程,发现减少了175个。WAITING的线程少了,系统上下文切换的次数就会少,因为每一次从WAITTING到RUNNABLE都会进行一次上下文的切换。读者也可以使用vmstat命令测试一下。

$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}' | sort | uniq -c
44 RUNNABLE
22 TIMED_WAITING(on object monitor)
9 TIMED_WAITING(parking)
36 TIMED_WAITING(sleeping)
130 WAITING(on object monitor)
1 WAITING(parking)

二、死锁

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

public class DeadLockDemo {
private static String A = "A";
private static String B = "B"; public static void main(String[] args) {
new DeadLockDemo().deadLock();
} private void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public 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:42)
    - 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:31)
    - waiting to lock <7fb2f3ef8> (a java.lang.String)
    - locked <7fb2f3ec0> (a java.lang.String)
    at java.lang.Thread.run(Thread.java:695)

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

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

三、资源限制的挑战

(1)什么是资源限制

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

(2)资源限制引发的问题

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

(3)如何解决资源限制的问题

对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让程序在多机上运行。比如使用ODPS、Hadoop或者自己搭建服务器集群,不同的机器处理不同的数据。可以通过“数据ID%机器数”,计算得到一个机器编号,然后由对应编号的机器处理这笔数据。

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

(4)在资源限制情况下进行并发编程

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

四、本章小结

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

Java并发(一)并发编程的挑战的更多相关文章

  1. Java并发编程的艺术读书笔记(1)-并发编程的挑战

    title: Java并发编程的艺术读书笔记(1)-并发编程的挑战 date: 2017-05-03 23:28:45 tags: ['多线程','并发'] categories: 读书笔记 --- ...

  2. 那些年读过的书《Java并发编程的艺术》一、并发编程的挑战和并发机制的底层实现原理

    一.并发编程的挑战 1.上下文切换 (1)上下文切换的问题 在处理器上提供了强大的并行性就使得程序的并发成为了可能.处理器通过给不同的线程分配不同的时间片以实现线程执行的自动调度和切换,实现了程序并行 ...

  3. java并发编程--第一章并发编程的挑战

    一.java并发编程的挑战 并发编程需要注意的问题: 并发编程的目的是让程序运行的更快,然而并不是启动更多的线程就能让程序最大限度的并发执行.若希望通过多线程并发让程序执行的更快,会受到如下问题的挑战 ...

  4. 《Java并发编程的艺术》并发编程的挑战(一)

    并发编程的挑战 并发编程的初衷是让程序运行的更快,但是更多的使用多线程真的会让程序变快吗? 1.线程上下文切换 关于线程上下文切换 多个线程在一个处理器里并不是同时进行的,而是非常快速地在线程之间进行 ...

  5. Java 多线程高并发编程 笔记(一)

    本篇文章主要是总结Java多线程/高并发编程的知识点,由浅入深,仅作自己的学习笔记,部分侵删. 一 . 基础知识点 1. 进程于线程的概念 2.线程创建的两种方式 注:public void run( ...

  6. Java 多线程:并发编程的三大特性

    Java 多线程:并发编程的三大特性 作者:Grey 原文地址: 博客园:Java 多线程:并发编程的三大特性 CSDN:Java 多线程:并发编程的三大特性 可见性 所谓线程数据的可见性,指的就是内 ...

  7. JAVA 多线程和并发学习笔记(三)

    Java并发编程中使用Executors类创建和管理线程的用法 1.类 Executors Executors类可以看做一个“工具类”.援引JDK1.6 API中的介绍: 此包中所定义的 Execut ...

  8. JAVA多线程和并发基础面试问答(转载)

    JAVA多线程和并发基础面试问答 原文链接:http://ifeve.com/java-multi-threading-concurrency-interview-questions-with-ans ...

  9. [转] JAVA多线程和并发基础面试问答

    JAVA多线程和并发基础面试问答 原文链接:http://ifeve.com/java-multi-threading-concurrency-interview-questions-with-ans ...

随机推荐

  1. 关于python操作带有中文文件名报错的解决办法

    python代码的编码格式       #coding:utf-8 在操作文件时,如果文件名带有中文,则需要将文件路径以Unicode的编码格式进行操作 具体的方式如下 path = "你的 ...

  2. centos安装lamp

    http://bbs.qcloud.com/thread-1316-1-1.html 启动MySQL http://www.cnblogs.com/starof/p/4680083.html 修改密码 ...

  3. MySQL数据库封装和分页查询

    1.数据库封装 <?php //我用的数据库名是housedb class DBDA {public $host="localhost";public $uid=" ...

  4. nginx深入剖析

    1.nginx功能模块说明 nginx之所以很强大,是因为具有很多的强大的模块 nginx核心功能模块:nginx的核心功能模块负责nginx的全局应用,主要对应的是主配置文件中的Main区块和Eve ...

  5. HttpWatch HttpWatch时间表(HttpWatch Time Charts)

    HttpWatch时间表(HttpWatch Time Charts) by:授客 QQ:1033553122 截图 说明 页面事件线(Page Event Lines)

  6. 基于localStorge开发登录模块的记住密码与自动登录

    前沿||我是乐于分享,善于交流的鸟窝 先做写一篇关于登录模块中记住密码与自动登录的模块.鸟窝微信:jkxx123321 关于这个模块功能模块的由来,这是鸟大大的处女秀,为什么这么说呢?一天在群里,一个 ...

  7. JSP隐含对象

    1.out隐含对象 (输出对象) 直接用于在JSP页面输出内容 javax.servlet.jsp.JspWriter(抽象类) 继承自java.io.Writer JSP中的out对象最终被转化成s ...

  8. zabbix系列之九——安装后配置四web监控

    1web监控 描述 详细 备注 概要 1)      需要定义 web 场景(包括一个或多个 HTTP请求),zabbix服务器根据预定义的命令周期性的执行这些步骤. 2)      Web 场景和 ...

  9. 分享今天在客户那里遇到的SQLSERVER连接超时以及我的解决办法

    分享今天在客户那里遇到的SQLSERVER连接超时以及我的解决办法 客户的环境:SQLSERVER2005,WINDOWS2003 SP2  32位 这次发生连接超时的时间是2013-8-5  21: ...

  10. Eclipse+Weblogic 12开发简单的Enterprise Application

    学到EJB方面的内容,遇到了很多问题,翻阅了无数遍Java EE和Weblogic的官方文档,在google上进行了无数次搜索都没有答案,可能我要找的答案太冷门.这一切都起源于Java EE官方文档里 ...