并发编程——如何让程序最大程度的并发执行?

本帅博主和伙伴们正在学习,遇上一个较为冗长的程序例子,又必须打出来,但是又不想占用太多时间。像我这样,既想要敲出这个程序,又不想占用自己太多时间,该怎么办呢?这就需要并发编程来帮忙了。A君写一段,B君写一段,C君再写一段,凑起来,就变成一个完成的程序啦,这样就提升了效率。

看完上面这个例子,大家也许就对并发编程有了一个大概的了解, 但并发编程到底是什么呢?

这段时间本帅博主一直在JVM和并发编程中来回切换学习总结,前几篇博客总结了一下关于JVM内存分配和垃圾回收的知识,今天就来了解一下并发编程里的基础知识。

一个东西存在就有它存在的理由,那么为什么要用并发编程呢

这一点从上面这个例子就可以看出来,我们这样一起敲代码,使得我们这个“敲出这个完整程序”的任务完成时间大幅减少。

并发编程的目的是为了让程序运行的更快,但是启动更多的线程不一定会让程序能够最大程度的并发执行,甚至有时候,并发比串行还要慢。而在使用并发编程的时候,我们也会面临许多挑战。是什么挑战呢?且看下文。

1.上下文切换

多线程听起来似乎是需要多个处理器,但其实并不是,单核处理器也是可以支持多线程执行代码的。因为CPU通过分配时间片来实现多线程的。

时间片是 CPU 分配给各个线程的时间, CPU 会通过不断切换线程执行。而因为时间片很短,让我们感觉多个线程是同时执行的。

每次时间片切换之前都会保存上一个任务的状态(这里是通过程序计数器来记录线程进行到哪一步的,如果大家对程序计数器不了解,可以参考:JVM——内存模型(一):程序计数器),这样下一次切换回这个任务的时候,可以再次加载这个任务的状态。而这个保存到加载的过程就是一次上下文切换

这样的切换是会影响多线程的执行效率的。想象我们看一本英文书籍,如果遇到单词不认识,我们会去查阅词典,但是在查阅之前,我们得先记住我们看到那一页了,以便等到查到单词后还能继续在之前看的位置读下去。虽然这样能够保证阅读的连贯性,但阅读的速度必然是受到影响的。

多线程并发执行不一定比串行执行快。测试发现,串行和并行做同一件循环操作,在达到一定的循环次数之前,并发是没有串行速度快的。这正是因为线程的创建以及上下文切换有开销的缘故。

那么怎么度量上下文带来的消耗呢?我们可以:

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

那么如何减少上下文的切换呢?

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

2.避免死锁

锁是个非常有用的工具,使用也很简单易懂,不过,要是使用不当,可能会引起死锁,从而导致系统不可用。

在一些复杂的场景中,可能会遇到死锁问题,比如线程 T1拿到锁之后,因为一些异常情况没有释放锁(例如死循环)。又或者是 T2拿到了一个数据库锁,但释放锁的时候抛出了异常,没有释放掉,这些情况都会出现死锁。

一旦出现死锁,业务是可以感知的,因为无法继续提供服务了,我们可以通过 dump 现场来查看哪个线程除了问题,并根据日志信息进行跟踪代码。

以下是几个常见的避免死锁的方法:

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

3.资源限制的挑战

资源限制可以分为计算机硬件资源软件资源

  • 硬件资源限制有宽带速度硬盘读写速度CPU 处理速度。所以在下载东西的时候,虽然宽带速度只有 2Mb/s,某个资源下载速度是 1Mb/s,但系统即使启动了 10 个线程下载资源,下载速度也不会变成10Mb/s,。
  • 软件资源限制有数据库的连接数socket 连接等。

因为资源限制的原因,有时候会导致并发执行的任务因为资源不足,甚至还没有串行执行速度快。比如因为开了多线程导致了一些资源的请求,但资源又不够用,就会导致一些资源调度和上下文切换的开销,从而降低了执行速度。

我们知道,限制从来都可以最小化,那怎么将这些限制最小化呢? 

  • 对于硬件资源限制,可以使用集群并行执行程序。
  • 对于软件资源,可以考虑使用资源池将资源复用。在资源限制的情况下,要根据不同的资源限制调整程序的并发度。例如有数据库操作时,设计数据库连接数,如果 SQL 语句执行的非常快,但线程的数量要比数据库连接数大很多,那么某些线程将会被阻塞,等待数据库连接。

4.总结

并发编程有很多挑战,如果并发程序写的不严谨,出现了问题,定位和解决起来都比较棘手和耗时。所以对于我等 Java 开发工程师来说,要多多使用 JDK 并发包提供的并发容器和工具类来解决并发问题,因为这些类已经通过了充分的测试和优化,解决上文中描述的问题,那是几乎是没有问题的。


 

展开阅读全文

没有更多推荐了,返回首页