《Java并发编程的艺术》第一章-阿里云开发者社区

开发者社区> 青衫无名> 正文

《Java并发编程的艺术》第一章

简介:
+关注继续查看

第1章并发编程的挑战

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

1.1     上下文切换

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

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

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

1.1.1    多线程一定快吗?

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

01 package chapter01;
02  
03 /**
04  * 并发和单线程执行测试
05  * @author tengfei.fangtf
06  * @version $Id: ConcurrencyTest.java, v 0.1 2014-7-18 下午10:03:31 tengfei.fangtf Exp $
07  */
08 public class ConcurrencyTest {
09  
10     /** 执行次数 */
11     private static final long count = 10000l;
12  
13     public static void main(String[] args) throws InterruptedException {
14         //并发计算
15         concurrency();
16         //单线程计算
17         serial();
18     }
19  
20     private static void concurrency() throws InterruptedException {
21         long start = System.currentTimeMillis();
22         Thread thread = new Thread(new Runnable() {
23             @Override
24             public void run() {
25                 int a = 0;
26                 for (long i = 0; i < count; i++) {
27                     a += 5;
28                 }
29                 System.out.println(a);
30             }
31         });
32         thread.start();
33         int b = 0;
34         for (long i = 0; i < count; i++) {
35             b--;
36         }
37         long time = System.currentTimeMillis() - start;
38         thread.join();
39         System.out.println("concurrency :" + time + "ms,b=" + b);
40     }
41  
42     private static void serial() {
43         long start = System.currentTimeMillis();
44         int a = 0;
45         for (long i = 0; i < count; i++) {
46             a += 5;
47         }
48         int b = 0;
49         for (long i = 0; i < count; i++) {
50             b--;
51         }
52         long time = System.currentTimeMillis() - start;
53         System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a);
54     }
55  
56 }

答案是不一定,测试结果如表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.1.2    测试上下文切换次数和时长

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

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

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

01 $ vmstat 1
02  
03 procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
04  
05 r b   swpd   free   buff cache   si   so   bi   bo   in   cs us sy id wa st
06  
07 0 0     0 127876 398928 2297092   0   0     0     4   2   2 0 0 99 0 0
08  
09 0 0     0 127868 398928 2297092   0   0     0     0 595 1171 0 1 99 0 0
10  
11 0 0     0 127868 398928 2297092   0   0     0     0 590 1180 1 0 100 0 0
12  
13 0 0     0 127868 398928 2297092   0   0     0     0 567 1135 0 1 99 0 0

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

1.1.3    如何减少上下文切换

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

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

 

1.1.4    减少上下文切换实战

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

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

1 sudo -u admin /opt/ifeve/java/bin/jstack 31177 &gt; /home/tengfei.fangtf/dump17

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

1 [tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}' | sort | uniq -c
2 39 RUNNABLE
3 21 TIMED_WAITING(onobjectmonitor)
4 6 TIMED_WAITING(parking)
5 51 TIMED_WAITING(sleeping)
6 305 WAITING(onobjectmonitor)
7 3 WAITING(parking)

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

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

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

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

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

1 [tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}' | sort | uniq -c
2 44 RUNNABLE
3 22 TIMED_WAITING(onobjectmonitor)
4 9 TIMED_WAITING(parking)
5 36 TIMED_WAITING(sleeping)
6 130 WAITING(onobjectmonitor)
7 1 WAITING(parking)

1.2 死锁

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

01 package chapter01;
02  
03 /**
04  * 死锁例子
05  *
06  * @author tengfei.fangtf
07  * @version $Id: DeadLockDemo.java, v 0.1 2015-7-18 下午10:08:28 tengfei.fangtf Exp $
08  */
09 public class DeadLockDemo {
10  
11     /** A锁 */
12     private static String A = "A";
13     /** B锁 */
14     private static String B = "B";
15  
16     public static void main(String[] args) {
17         new DeadLockDemo().deadLock();
18     }
19  
20     private void deadLock() {
21         Thread t1 = new Thread(new Runnable() {
22             @Override
23             public void run() {
24                 synchronized (A) {
25                     try {
26                         Thread.sleep(2000);
27                     catch (InterruptedException e) {
28                         e.printStackTrace();
29                     }
30                     synchronized (B) {
31                         System.out.println("1");
32                     }
33                 }
34             }
35         });
36  
37         Thread t2 = new Thread(new Runnable() {
38             @Override
39             public void run() {
40                 synchronized (B) {
41                     synchronized (A) {
42                         System.out.println("2");
43                     }
44                 }
45             }
46         });
47         t1.start();
48         t2.start();
49     }
50  
51 }

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

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

01 "Thread-2" prio=5 tid=7fc0458d1000 nid=0x116c1c000 waiting for monitor entry [116c1b000]
02    java.lang.Thread.State: BLOCKED (on object monitor)
03         at com.ifeve.book.forkjoin.DeadLockDemo$2.run(DeadLockDemo.java:42)
04         - waiting to lock <7fb2f3ec0> (a java.lang.String)
05         - locked <7fb2f3ef8> (a java.lang.String)
06         at java.lang.Thread.run(Thread.java:695)
07  
08 "Thread-1" prio=5 tid=7fc0430f6800 nid=0x116b19000 waiting for monitor entry [116b18000]
09    java.lang.Thread.State: BLOCKED (on object monitor)
10         at com.ifeve.book.forkjoin.DeadLockDemo$1.run(DeadLockDemo.java:31)
11         - waiting to lock <7fb2f3ef8> (a java.lang.String)
12         - locked <7fb2f3ec0> (a java.lang.String)
13         at java.lang.Thread.run(Thread.java:695)

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

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

1.3 资源限制的挑战

(1)什么是资源限制?

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

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

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

 

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

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

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

 

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

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

1.4 本章小结

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

[1] Lmbench3是一个性能分析工具。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
10052 0
java-面试- Java并发编程(一)——并发编程需要注意的问题
并发是为了提升程序的执行速度,但并不是多线程一定比单线程高效,而且并发编程容易出错。若要实现正确且高效的并发,就要在开发过程中时刻注意以下三个问题: 上下文切换 死锁 资源限制 接下来会逐一分析这三个问题,并给出相应的解决方案。
1196 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
10882 0
Java技术周刊第11期:Java并发编程的艺术,解读并发编程的优缺点
Java的开发者们:云栖社区已有5000位Java开发者,发布了30000+Java文章(文章列表),沉淀了7000+的Java精品问答(问答列表)。 Java技术周刊将会为大家介绍最新的Java技术与动态、预告活动、最热问答、直播教程等,欢迎大家订阅Java技术周刊。
2742 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
13865 0
Java并发编程-并发编程带来的问题
并发编程带来的问题 1.上下文切换问题 2.死锁问题 上下文切换 多线程不一定快 1.线程有创建和上下文切换的开销 如何减少上下文切换 减少上下文切换的方法有无锁编程、CAS算法、使用最小线程、使用协程 1.无锁并发编程,多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照hash算法取模分段,不同的线程处理不同段的数据 2.CAS算法。
681 0
java面试-Java并发编程(五)——中断
什么是中断? 在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
891 0
java面试-Java并发编程(三)——volatile
1. 并发编程的两个关键问题 并发是让多个线程同时执行,若线程之间是独立的,那并发实现起来很简单,各自执行各自的就行;但往往多条线程之间需要共享数据,此时在并发编程过程中就不可避免要考虑两个问题:通信 与 同步。
1195 0
java面试-Java并发编程(二)——重排序
当我们写一个单线程程序时,总以为计算机会一行行地运行代码,然而事实并非如此。 什么是重排序? 重排序指的是编译器、处理器在不改变程序执行结果的前提下,重新排列指令的执行顺序,以达到最佳的运行效率。
899 0
+关注
3598
文章
840
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载