Java 程序从 main() 函数开始开始执行,就是启动一个名称为 main 的线程。在程序顺序执行的过程中,看似没有其他线程参与,实际上有许多线程和 main 线程一起执行着。
Java 天生就是多线程的,为什么 Java 会设计成多线程的呢?并发编程是不是有它优点?然而事物都是有两面性的,并发编程的缺点是什么?应该怎么避免?
1. 为什么要使用并发?(优点)
1.1 更好地利用计算机处理器
现在 CPU 都是多核的,而且核心越来越多。如果一个程序是单线程的,无论如何只能使用一个核心,CPU 的核心再多也无法提升机器的性能。
如果一个程序是多线程的,那么它就可以利用 CPU 的多个核心进行运算,CPU 的核心变得越多,运行速度也能越快。
1.2 更快的响应速度
多线程可以让一系列的操作并发地执行,这样就可以提高程序响应的速度。比如用户下单,它包括插入订单数据、生成订单快照、发送邮件通知卖家和记录货品销售数量等。
等这些业务操作做完,用户要等 1 秒。把生成订单快照、发送邮件这样的操作给其他线程处理,用户可能就等 0.5 s。
1.3 更简单地进行开发
Java 设计之初就考虑了并发编程,提供了一致的编程模型,使用 Java 的程序员可以把更多精力放在业务上,而不用考虑怎么使用多线程。
2. 并发编程的缺点及解决方案
2.1 增加上下文切换,影响执行速度
我们在阅读英文书籍的时候碰到了一个不认识的单词,这时候就去字典里查一下这个单词是什么意思,查完再回过头继续往下看书。这个一来一回的过程就相当于是上下文切换的过程。
线程之间上下文切换的过程是这样的。CPU 个每个线程分配一个时间片,一个时间片就表示一段很短的时间。CPU 执行线程时,时间片用完了就会换一个线程执行,就这样在各个线程之间切换。
查完字典再回过头继续阅读,这样会影响我们的阅读效率。上下文切换自然也会影响执行速度。
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。
无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
2.2 出现死锁,导致系统不可用
|
|
比如t1拿到锁之后,因为一些异常情况没有释放锁(死循环)。又或者是t1拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉。
一旦出现死锁,系统功能就不可用了,需要花许多精力去排查错误。
避免死锁的几个常见方法有:
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
2.3 资源限制影响性能
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。
比如带宽受限的情况下及时启动多个线程进行下载,下载速度永远无法超过带宽。
在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行。
但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。
对于硬件资源的限制,可以采用集群的方式,一台机器的资源有限那就用多台,比如部署 hadoop 集群。
对于软件资源限制可以采用连接池,提高资源的复用率。