资讯专栏INFORMATION COLUMN

并发编程 - 探索一

pcChao / 2900人阅读

摘要:并发表示在一段时间内有多个动作存在。并发带来的问题在享受并发编程带来的高性能高吞吐量的同时,也会因为并发编程带来一些意想不到弊端。并发过程中多线程之间的切换调度,上下文的保存恢复等都会带来额外的线程切换开销。

0x01 什么是并发

要理解并发首选我们来区分下并发和并行的概念。

并发:表示在一段时间内有多个动作存在。
并行:表示在同一时间点有多个动作同时存在。

例如:
此刻我正在写博客,但是我写着写着停下来吃一下东西(菠萝片)再写、再吃。这两个动作在一段时间内都在发生着,这可以理解为并发。
另一方面我在写这个博客的同时我在听音乐。那么同时存在的两个动作(写博客、听音乐)是同时在发生的这就是所谓的并行。

从上面两个概念明显可以感受到并发是包含并行操作。所以我们通常说的并发编程对于cpu来说有可能是并发的在执行也有可能是交替的在执行。

说到这里你可能会问为什么我们需要并发编程?
在求解单个问题的时候凡是涉及多个执行流程的编程模式都叫并发编程。

0x02 为什么需要并发

硬件的发展推动软件的进度,多核时代的到来

应用系统对性能和吞吐量的苛刻要求

大数据时代的到来

移动互联网、云计算对计算体系的冲击

0x03 并发编程方式

Java:多进程/多线程的并发实现方式

Go:协程--用户态实现的多线程方式(goroutine)

Java并发模型

在介绍java并发模型前我们来介绍下系统对多线程的实现方式。系统支持用户态线程和内核态两种线程的实现方式,内核态线程是cpu去调度的最小单位,所以这牵涉到用户态线程和内核态线程之间的映射关系,用户态线程:内核态线程 = 1:1 、 N:1 、 M:N。

1:1 这种映射关系充分利用多核的优势,但是这种方式在用户态进行线程切换的过程中都会涉及到内核态线程之间的切换,切换开销大。(主要涉及内核线程运行时上下文的保存与恢复)
N:1 没法充分利用多核的优势,但是这种由于是用户态的内存切换不涉及内核态线程之间的切换所以这种映射关系在线程之间切换代价小。
M:N 这种是上面两种映射关系的结合体,集合了上面两种映射关系的优势,但是这也增加了线程之间这种映射关系的调度复杂度。

Java的并发编程模式是通过1:1这种映射关系来实现线程之间的并发调度。

Go并发模型

Go的并发模式是通过M:N这种方式来实现并发调度的。
Go调度器中有三种重要结构:M(posix thread)、P(调度上下文,一般数量设置为和机器内核数相同,这样能充分发挥机器的并发性能)、G(goroutine)。

一个调度上下文可以包含多个Goroutine,多个上下文所以可以所有的Goroutine都能并发的运行在CPU的多核上面。
如果有Goroutine发现找不到调度上下文,就会被放到global runqueue中,等清闲的调度上下文来捞取它进行调度。
如果调度上下文上面挂载的所有Goroutine都已经执行完毕,此时他会去global runqueue中获取Goroutine,如果发现此时没有获取到,则会去别的调度上文中抢Goroutine,一般一次抢都是抢此时被抢调度上下文的一半Goroutine,确保充分利用M去被多核调度。

0x04 并发带来的问题

在享受并发编程带来的高性能、高吞吐量的同时,也会因为并发编程带来一些意想不到弊端。

资源的消耗,要管理这么多用户线程、内核线程、用户线程内核线程之间的切换调度,上下文等等这些都是由于引用了并发编程所带来的额外消耗。

并发过程中多线程之间的切换调度,上下文的保存恢复等都会带来额外的线程切换开销。

编码、测试的复杂性。和我们生活中的例子很相像,三五个人一起出去活动很容易把控,如果带着几十、上百人的团队出去活动这些都会带来额外的管理上的开销。

真的是有阳关的地方就有黑暗啊!

上面这些都是我们没法避免的一些问题,要引用并发编程必然会要付出点额外的代价才行。但是并发编程还带来了一个不能忽视的问题,线程之间对同一资源的竞争访问,造成内存对象状态和自己的想象千差万别。

Java线程和内存对象之间的交互

java线程对内存的理解分为两部分:线程工作内存(每个线程独有的)、共享内存也叫主内存(所有的线程所共有的),下面是java线程对内存中Count对象的一次修改操作。

从主线程中读取Count对象放入线程工作内存,后面的读取修改都在线程工作内存中,最后(更新到主内存的时间不是确定的,可能会插入别的操作在store、write之间)更新到主内存中。所有的上述操作都是顺序执行的,但是不保证连续执行。

volatile变量、synchronized同步块

volatile变量、synchronized块执行结束后能保证每次去更新的值都会立即写入到主内存中。
volatile变量很多人会认为这样就是线程安全的,但是通过上面我们可以看到如果两个线程同时去读了一个volatile变量,最后一前一后更新到主内存中,这样也会出现写丢失的情况,所以volatile不能保证线程安全。

0x05 并发实战

1) 定义线程池

private static final ExecutorService executor = Executors.newFixedThreadPool(20);

2)定义并发服务

CompletionService completionService = new 
      ExecutorCompletionService(executor);

3)提交并发任务

completionService.submit(new Callable() {
    @Override
    public void call() throws Exception {
        return ;
    }
});

4)等待并发结果

for (int i = 0; i < taskSize; ++i) {
    Future future = completionService.poll(TIME_OUT,
                    TimeUnit.SECONDS);
    Result result = future.get();
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70030.html

相关文章

  • 长文慎入-探索Java并发编程与高并发解决方案

    摘要:所有示例代码请见下载于基本概念并发同时拥有两个或者多个线程,如果程序在单核处理器上运行多个线程将交替地换入或者换出内存这些线程是同时存在的,每个线程都处于执行过程中的某个状态,如果运行在多核处理器上此时,程序中的每个线程都 所有示例代码,请见/下载于 https://github.com/Wasabi1234... showImg(https://upload-images.jians...

    SimpleTriangle 评论0 收藏0
  • 浅谈Java并发编程系列()—— 如何保证线程安全

    摘要:比如需要用多线程或分布式集群统计一堆用户的相关统计值,由于用户的统计值是共享数据,因此需要保证线程安全。如果类是无状态的,那它永远是线程安全的。参考探索并发编程二写线程安全的代码 线程安全类 保证类线程安全的措施: 不共享线程间的变量; 设置属性变量为不可变变量; 每个共享的可变变量都使用一个确定的锁保护; 保证线程安全的思路: 1. 通过架构设计 通过上层的架构设计和业务分析来避...

    mylxsw 评论0 收藏0
  • 精读《前端未来展望》

    摘要:精读前端可以从多个角度理解,比如规范框架语言社区场景以及整条研发链路。同是前端未来展望,不同的文章侧重的格局不同,两个标题相同的文章内容可能大相径庭。作为使用者,现在和未来的主流可能都是微软系,毕竟微软在操作系统方面人才储备和经验积累很多。 1. 引言 前端展望的文章越来越不好写了,随着前端发展的深入,需要拥有非常宽广的视野与格局才能看清前端的未来。 笔者根据自身经验,结合下面几篇文章...

    MadPecker 评论0 收藏0
  • 函数式编程与面向对象编程[5]:编程的本质

    摘要:函数式编程与面向对象编程编程的本质之剑目录编程的本质读到两篇文章写的不错综合摘录一下复合是编程的本质函数式程序员在洞察问题方面会遵循一个奇特的路线。在面向对象编程中,类或接口的声明就是表面。 函数式编程与面向对象编程[5]:编程的本质 之剑 2016.5.6 01:26:31 编程的本质 读到两篇文章,写的不错, 综合摘录一下 复合是编程的本质 函数式程序员在洞察问题方面会遵循...

    miracledan 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<