【并发编程的艺术】并发编程的挑战

并发编程的目的是为了程序运行得更快,但并不是启动更多线程就能让程序最大程度地并发执行
在通过多线程执行任务让程序运行得更快将面临很多挑战

一、上下文切换

1.1 什么是上下文切换

即使是单核CPU也支持多线程代码执行,CPU通过给每个线程分配CPU时间片来实现这个机制

时间片
CPU分配给各个线程的时间
因为时间片非常短,所以CPU通过不断切换线程执行,我们感觉多个线程是同时执行的
时间片一般是几十毫秒(ms)

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

所以任务从保存到再加载的过程就是一次上下文切换

1.2 多线程一定快么

代码串行执行和并发执行累计的时间,串行一定比并发执行慢么?
首先看一段代码

public class ConcurrencyTest {
    private static final long count = 1000000l;
    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并发执行耗时/ms
10亿1255641
1亿9168
1000万1612
100万77
10万35
1万01

可以看出,当并发执行累加操作不超过100w次时,速度会比串行累加操作要慢
为什么会出现这样的情况呢?这是因为线程有创建和上下文切换的开销

1.3 上下文切换的次数和时长

有什么工具可以度量上下文切换带来的消耗

Lmbench3
一个性能分析工具,测量上下文切换的时长

vmstat
测量上下文切换的次数
在这里插入图片描述
CS(Content Switch)表示上下文切换的次数,从上面的测试结果可以看出,上下文切换的次数为每秒2000次左右

1.4 如何减少上下文切换

减少上下文切换的方法

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

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

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

  • 协程
    在单线程中实现多任务的调度,并在单线程中维持多个任务间的切换

1.5 减少上下文切换实战

如何定位分析并减少大量WAITING的线程,来减少上下文切换的次数

1. jstack命令dump线程信息
看看pid为1的进程里的线程都在做什么

jstack 1 > /app/dump17

2. 统计线程都处于什么状态

grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}' | sort | uniq -c
     22 RUNNABLE
      5 TIMED_WAITING(onobjectmonitor)
     42 TIMED_WAITING(parking)
      8 TIMED_WAITING(sleeping)
     305 WAITING(onobjectmonitor)
     39 WAITING(parking)

3. 查看 WAITING线程
打开dump文件查看处于WAITING(onobjectmonitor)的线程在做什么

"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的工作线程,处于await
说明JBOSS线程池中的线程接收到的任务太少,大量的线程都闲着

4. 减少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">

5. 重启JBOSS
再dump线程信息,然后统计WAITING(onobjectmonitor)的线程数量,发现处于该状态的线程大量减少
WAITING状态的线程少了,系统上下文切换的次数就会少,因为每一次从WAITING到RUNNABLE都会进行一次上下文切换

二、死锁

锁在并发编程中非常常见和实用,但也会带来一些困扰,那就是可能引起死锁,一旦尝试死锁,就会造成系统功能不可用

2.1 死锁实例

以下的一段代码,线程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拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉

2.2 死锁问题确定

一旦出现死锁的情况,业务是有感知的,因为不能继续提供服务了,可以通过dump线程查看到底是哪个线程出现了问题

1. 查看服务的进程id

ps -ef | grep DeadLockDemo

2. dump进程信息

jstack 58791 > /Users/lluozh/docs/dump17

3. 搜索dump文件中BLOCKED进程

"Thread-1" #11 prio=5 os_prio=31 tid=0x00007fa432b74000 nid=0x4003 waiting for monitor entry [0x0000700007dab000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.cvte.fagent.lluozh.DeadLockDemo$2.run(DeadLockDemo.java:37)
	- waiting to lock <0x0000000795f70e98> (a java.lang.String)
	- locked <0x0000000795f70ec8> (a java.lang.String)
	at java.lang.Thread.run(Thread.java:748)

"Thread-0" #10 prio=5 os_prio=31 tid=0x00007fa4343c9000 nid=0x4203 waiting for monitor entry [0x0000700007ca8000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.cvte.fagent.lluozh.DeadLockDemo$1.run(DeadLockDemo.java:27)
	- waiting to lock <0x0000000795f70ec8> (a java.lang.String)
	- locked <0x0000000795f70e98> (a java.lang.String)
	at java.lang.Thread.run(Thread.java:748)

2.3 避免死锁常见方法

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

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

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

4. 数据库锁
加锁和解锁必须在一个数据库连接中,否则会出现数据库解锁失败的情况

三、资源限制的挑战

3.1 什么是资源限制

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

在进行并发编程时,要考虑资源的限制

硬件资源限制

  • 带宽的上传/下载速度
  • 硬盘读写速度
  • CPU的处理速度

软件资源限制

  • 数据库的连接数
  • socket连接数

3.2 资源限制引发的问题

代码执行速度加快的原则:
将代码中串行执行的部分变成并发执行

资源受限引发的问题
如果将某段串行执行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会变慢,因为增加上下文切换和资源调度的时间

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

硬件资源

集群并发
对于硬件资源的限制,可以考虑使用集群并发执行程序,既然单机的资源有限制,那么就让程序在多机上运行

分布式处理
不同的机器处理不同的数据,可以通过"数据ID%机器数",计算得到一个机器编号,然后由对应编号的机器处理对应的数据

软件资源

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

  • 使用连接池将数据库和socket连接复用
  • 在调用对方webservice接口获取数据时只建立一个连接

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

根据不同的资源限制调整程序的并发度

下载文件
涉及:

  • 带宽
  • 硬盘的读写速度

根据带宽和硬盘的读写速度调整并发执行线程数

数据库操作
涉及:

  • 数据库连接

如果SQL语句执行非常快,而线程的数量比数据库的连接数大很多,则某些线程会被阻塞,等待数据库连接

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sysu_lluozh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值