Fork me on GitHub

Java并发系列(一)

并发编程面临的挑战

1. 多线程一定快吗?

毫无疑问,对于很多程序员来说,当被问到,怎么缩短程序运行的时间,提高程序的效率。很多人的第一个反应便是,使用并发,多线程的方式进行编写程序。无可否认,多线程确实在很多情况下可以达到我们需要的效果,但是也不是绝对的。

学过多线程的应该了解,单核处理器中执行多线程的代码的时候,并不是真正意义上的并发,同一时刻CPU可以执行多个任务,CPU是通过给每个线程分配CPU时间片来执行的,多个线程进行切换,因为时间片一般是几十毫秒,因此在我们看来,像是多个线程同时执行的。

现在问题来了,多个线程进行频繁的切换,到底会产生怎样的影响?此处需要引入一个上下文切换的概念,当切换线程的时候,CPU需要先保存上一个线程的状态,例如:本地数据,程序指针等,以便下次切换回该线程的时候,可以再加载这个任务的状态,然后再载入另一个线程。一个线程从保存到被再加载就是一个上下文切换的过程。而这个过程显然并不廉价。

举个栗子,假如现在有一个人在挖山,单线程就好比,他从山的这一头挖到另一头,而多线程就好比从两头向中间挖,而如果这个时候,采用多线程的话,他这个人就必须在山的这一头挖一点,又得绕个大圈去到山的另一头,这样来回的切换,你认为效率会变吗?也许这个栗子有点夸大,但当我们程序执行一个简单的任务,就两个for循环吧。

1
2
3
4
5
6
7
8
9
10
11
12
int b = 0;
new Thread(new Runnable(){
public void run(){
for(int i=0 ; i<10000; i++){
b++;
}
}
}).start();

for(int j=0; i<10000; j++){
System.out.println(b--);
}

这其实是一个简单的串行任务,便可以解决的,当使用的多线程的实现时候,如果数据没有达到百万级别的时候,并发的运行效率是低于单线程的。原因在于多线程创建和上下文的切换是需要耗费时间的。

什么时候才应该使用多线程呢?

充分利用CPU的效率,便是多线程提高程序运行效率的本质。

还是举刚刚挖山的栗子,现在你在挖山,你那些是石头是要有清理的吧,假如现在是单线程处理,那便意味着,在清理石头的时候,工人是在等待的,那如果是多线程处理,现在有了一条运石头线程,在你挖完这一边的时候,运石头线程在执行它的操作时候,工人继续到另一边执行它的挖山。此处,需要注意的便是充分利用了工人的空余时间

举下多线程用的栗子

  • 当我们在进行磁盘读取文件的时候,大部分的CPU时间都用于等待磁盘去读取数据,此时,CPU是非常空闲的,我们便可以让CPU去执行其他操作。网络io的时候、等待用户输入的时候也同理。
  • 当我们一个服务器接收客户端的响应的时候,我们需要开一条线程去监听客户端的行为,和开一条工作线程去执行操作返回给客户端。假如我们使用单线程处理的话,如果我们在执行的操作是耗时的,那下一个用户的行为,我们将无法立即接收到。

如何减少上下文切换?

  • 无锁并发编程:多线程在竞争锁的时候,会引起上下文切换,因此我们使用多线程处理数据的时候,可以采用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
  • CAS算法。(下面会提下)
  • 使用最少线程:避免创建不需要的线程,避免处于等待状态的线程过多,减少线程创建和上下文切换的开销。
  • 协程:在单线程中实现多任务的调度,并在单线程中维持多个任务之间的切换。

2. 死锁

锁是实现线程同步的一个很有效的工具,相信很多人都并不陌生,当被提到如何同步线程,如何实现线程安全,第一个想到的便是——锁机制。无可否认,加锁的确能实现线程的同步,但是如果使用不当,便会产生死锁

  1. 何为线程死锁?

    死锁就是两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,谁也不让着谁,若无外力的作用,他们将无法推进下去。

  2. 举一个简单的栗子

1
2
3
4
5
6
7
8
9
10
11
12
// 线程1中的run方法代码
syschronized(A){
...
syschronized(B);
...
}
// 线程2
syschronized(B){
...
syschronized(A);
...
}

这只是一个简单的展示,应该没有人会写出这样的代码,但这种都想获得对方的资源,但不肯释放自己的资源,便是产生死锁的一个原因了。在一些复杂的场景下,假设1线程拿到锁之后,出于某种情况无法释放锁,也是会出现死锁的情况。

  1. 死锁产生的条件
  • 互斥性:一个资源只能被一个线程使用。
  • 请求和保持:一个线程请求新的资源的时候,不会释放已有的资源。
  • 不剥夺条件:线程已有的资源在完成任务之前不能剥夺,只能自己释放。
  • 循环等待条件:线程之间形成首尾相接的循环等待资源的关系。
  1. 如何避免死锁?
    1. 避免一个线程中同时获取多个锁。
    2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
    3. 改变加锁的顺序,当使用多个锁的时候,如果能确保所有的线程都是按照相同的顺序获取锁,那么死锁便不会发生了。
    4. 尝试使用定时锁,使用lock.tryLock(long time, TimeUnit unit)来替代使用内部锁机制。在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试 。

3.资源限制

  1. 什么是资源限制?

    1. 资源限制是指在进行并发编程的时候,程序执行的速度受限于计算机硬件资源和软件资源。

    2. 举个栗子

      假设服务器的带宽只用2Mb/s,某个资源的下载速度是1Mb/s,系统启动了10个线程去下载资源,下载速度是不可能变成10Mb/s的。因此我们在进行并发编程的时候,要考虑这些资源的限制,

  2. 硬件资源限制:有带宽的上传/下载速度,硬盘读写速度和CPU的处理速度。

  3. 软件资源限制:有数据的连接数和socket的连接数等。

  4. 会引发什么问题?

    当我们将某些串行的代码并发执行的时候,如果受限于资源,此时依然是串行执行的,这时候程序不但不会加快执行,还会比原先串行的时候还慢,因为增加了上下文切换、线程创建和资源调度的时间。

  5. 如何解决资源限制的问题呢?

    1. 对于硬件资源的限制,可以考虑集群并行执行程序。既然单机的资源有限制,那就让程序在多机上运行。
    2. 对于软件资源的限制,可以考虑使用资源池将资源进行复用。
-------------本文结束感谢您的阅读-------------

本文标题:Java并发系列(一)

文章作者:AllenYu

发布时间:2019年01月11日 - 16:01

最后更新:2019年01月11日 - 16:01

原始链接:http://yuzeduan.github.io/2019/01/11/Java并发系列-一/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。