走进并发世界

并发计算的由来

20世纪中期以来,硬件的快速发展也使得单核CPU的主频逐步逼近极限,多核CPU架构称为了一种必然的技术趋势。所以,多线程并发计算便显得越来越重要。线程并发的一个重要应用场景就是服务端编程。

在了解并发之前,我们可能需要先明白为什么需要并发?并发有什么作用?首先我们先看下面两个例子:

图像处理:一张1024*768像素的图片,包含多达78万6千多个像素,我们即使将所有的像素都遍历一遍,也得花大量的时间,更何况,图像处理会涉及到大量的矩阵计算,矩阵的规模和数量都非常大,这么大密集的计算,我们该怎么去解决?
淘宝双十一:在淘宝”双十一“一天内,支付宝核心数据库集群处理了41亿个事务,执行了285亿次SQL,生成了15TB日志,访问了1931亿次内存数据块,13亿个物理读。如此密集的访问,我们又该如何去解决?
很明显,对于上面的第一个例子,我们使用单核CPU单线程也能解决,只不过会花费巨大的时间,但是对于淘宝双十一的处理,一天的时间处理这么多的数据,依目前的计算机水平,恐怕任何一台单机都难以胜任,因此,并发计算也就自然成了唯一出路。

摩尔定律的消失

摩尔定律是由英特尔创始人之一戈登·摩尔提出来的。其内容为:集成电路上可容纳的电晶体(晶体管)数目,约每隔24个月便会增加一倍,英特尔首席执行官大卫·豪斯后来提出:预计18个月会将芯片性能提高一倍(即更多的晶体管使其更快)。

说的直白点,就是每隔18个月到24个月,我们的计算机性能就能翻一倍。

但是,摩尔定律并不是一种自然法则或者物理定律,它只是基于人为观测数据后,对未来的预测。摩尔定律的有效性一直持续了半个世纪,直到2004年,Intel宣布将4GHz芯片的发布时间推迟到2005年,在2004年秋季,Intel宣布彻底取消4GHz计划。因此,摩尔定律在CPU的计算性能上可能已经失效。虽然,现在Intel已经研制出了4GHz芯片,但可以看到,在近十几年的发展中,CPU主频的提升已经明显遇到了一些暂时不可逾越的瓶颈。摩尔定律的失效,意味着很难在单个CPU的技术上取得重大的性能提升了,但是这个时候多核CPU产生了,我们不再追求单核CPU的计算速度,但是我们可以将多个独立的计算单元整合到一个CPU中,也就是我们所说的多核CPU,摩尔定律在CPU的计算性能上已经失效了,CPU开始向多核发展。我们可以预测,在未来的每过18~24个月,CPU的核心数便会翻一倍,那么计算机的性能也会提高一倍。

所以,如何让多个CPU有效并且正确地工作也就成为了一门技术,比如:多线程间如何保证线程安全,如何正确理解线程间的无序性、可见性,如何尽可能提高并发程序的设计,又如何将串行程序改造为并发程序等等,都是程序员在软件设计中必须考虑的问题。

串行、并发与并行

在讲解并发之前先理解下串行、并发与并行之间的关系。假设目前有3件事需要处理,每件事情所需的时间包括两部分(处理时间与等待时间),假设完成这些事情所需的时间分别为:事情A(处理5分钟,等待10分钟),事情B(处理3分钟,等待7分钟),事情C(处理8分钟,无等待)。那么我们有3种方式来完成这几件事情。如下图所示:采用串行的方式逐一完成所有事情,只需一人完成耗时需要33(15+10+8)分钟。采用并发的方式,完成事件A后在其等待事件内完成事件B,那么在这个过程中只需一人总耗时为16(5+3+8)分钟。采用并行的方式,这需要3个人分别同时去处理这三件事总耗时是15分钟。

image

可见,并发是串行的对立面,一般情况下并发往往能够提高处理效率,而并行是并发的特例。从软件角度来说,并发就是在一段时间内以交替的方式去完成多个任务。从硬件的角度来说,在一个处理器一次只能够运行一个线程的情况下,由于处理器可以使用时间片来实现同一段时间内运行多个线程,因此一个处理器就可以实现并发。而并行则需要多个处理器在同一时刻各自运行一个线程来实现。

2 并发的缺点

并发在提高系统的吞吐率以及充分利用多核处理器资源的同时也会带来自身伴随的风险和问题。

2.1 线程安全问题

多个线程共享数据的时候,如果没有采用相应的并发访问控制措施,那么就会产生数据一致性的问题,例如读取脏数据、丢失更新等。

public class RequestIDGenerator {
    private int sequence = 0;
    private final static RequestIDGenerator INSTANCE = new RequestIDGenerator();

    public static RequestIDGenerator getINSTANCE() {
        return INSTANCE;
    }

    public void newSequence() {
        try {
            System.out.println(Thread.currentThread().getName() + ": " + sequence);
            Thread.sleep(50);
            sequence++;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Demo {
    public static void main(String[] args) {
        int numberOfThread = 3;
        Thread[] threads = new Thread[numberOfThread];
        for (int i = 0; i < threads.length; i++) {
            new WorkerThread(i, 50).start();
        }
    }
}

class WorkerThread extends Thread {
    private int count;

    public WorkerThread(int id, int count) {
        super("worker_" + id);
        this.count = count;
    }

    @Override
    public void run() {
        RequestIDGenerator instance = RequestIDGenerator.getINSTANCE();
        while (count-- > 0) {
            instance.newSequence();
        }
    }
}

在上面demo中,不同的线程“拿到”了重复的sequence。多个线程访问共享变量时,由于任何一个线程在访问共享变量的过程中都可以切换到其他的线程上。而其他线程一旦把共享变量的数据改变了,再切换回来时,错误数据就产生了。于是,我们很容易联想到一种保障线程安全的方法——将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问,该线程发访问结束后其他线程才能对其进行访问。最简单的就是在改变共享数据的访问方法上加上synchronized关键字。

2.2 上下文切换

我们知道,在单处理器上也能够以多线程的方式实现并发,即一个处理器可以在同一时间段内运行多个线程。实际上是通过时间片分配的方式实现的。时间片决定了一个线程可以连续占用处理器运行的时间长度。当一个线程由于时间片用完或者需要等待等原因暂停其运行时,另外一个线程就可以被线程调度器选中开始或继续运行。这个过程就叫作线程上下文切换。
这种方式意味着线程在切换的时候,需要在内存中保存或恢复相应的线程进度信息,这个进度信息被称为上下文。从性能方面上看,上下文切换有其不容小觑的开销,过于频繁地切换反而无法发挥出并发的优势。因此,减少上下文切换可以降低性能开销。通常可采用ConcurrentHashMap的锁分段技术,CAS算法,减少不必要的线程等方式。

2.3 线程活性故障

使用线程是为了提高处理效率,理想情况下希望线程一直处在RUNNABLE状态。但事实上由于程序自身缺陷故障或处理器资源稀缺会导致一个线程处于非RUNNABLE状态。由于程序自身缺陷故障或处理器资源稀缺会导致一个线程处于非RUNNABLE状态,或者线程处于RUNNABLE状态但执行的任务一直无法进展的现象称之为线程活性故障。

常见的活性故障包括以下几种:

  • 死锁。死锁产生的典型场景是一个线程X持有资源A的时候等待另一个线程释放资源B,而另一个线程Y持有线程B的时候却等待线程X释放资源A。死锁的外在表现是当前线程的生命周期永远处于非RUNNABLE状态而使其任务无法继续。
public class Demo {

    private static Object lock_A = new Object();
    private static Object lock_B = new Object();

    public static void main(String[] args) {
        deadLock();
    }

    public static void deadLock() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock_A) {
                    try {
                        System.out.println("get Resource_1 lock_A ");
                        Thread.sleep(30);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lock_B) {
                        System.out.println("get resource_1 lock_B ");
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock_B) {
                    System.out.println("get Resource_2 lock_B ");
                    synchronized (lock_A) {
                        System.out.println("get resource_2 lock_A ");
                    }
                }
            }
        }).start();
    }
}

执行结果:

get Resource_1 lock_A 
get Resource_2 lock_B 

通常可以采用以下几种方式规避死锁:

  • 使用一个粗粒度的锁代替多个锁
  • 避免一个线程同时获得多个锁;
  • 相关线程使用全局统一的顺序申请锁
  • 针对那些不可能实现按序加锁并且锁超时也不可行的场景使用死锁检测
  • 使用ReentrantLock.tryLock(long,TimeUnit)来申请锁

  • 活锁。外在表现为线程可能处于RUNNABLE状态,但线程所要执行的任务没有丝毫进展。在试图进行死锁故障恢复可能导致活锁。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容

  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,224评论 4 56
  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    小徐andorid阅读 2,796评论 3 53
  • 新年第一课,一定是要聊聊你的新年愿望或者你如何度过新年第一天的。因为临近期末考试,几个孩子都过的平淡无奇。...
    蕙心紈质阅读 160评论 0 0
  • 今晚明月当空 如金盘一般 在黑夜中一抹耀眼的光 引人追寻 却又若隐若现 望着那明月追寻几千公里 看着月光照亮一处低...
    菁鹤堂_刘昭川鹤阅读 372评论 0 0
  • OMG 原来这个世界不止我一个人如此焦虑,不止我一个人凡事想的太多,不止我一个人会时不时感到孤单,不止我一个人抱怨...
    温润先生阅读 558评论 2 0