1.并发编程的优缺点

一直以来并发编程对于刚入行的小白来说总是觉得高深莫测,于是乎,就诞生了想写点东西记录下,以提升理解和堆并发编程的认知。为什么需要用的并发?凡事总有好坏两面,之间的trade-off是什么,也就是说并发编程具有哪些缺点?以及在进行并发编程时应该了解和掌握的概念是什么?这篇文章主要以这三个问题来谈一谈。

1. 为什么要用到并发

一直以来,硬件的发展极其迅速,也有一个很著名的"摩尔定律",可能会奇怪明明讨论的是并发编程为什么会扯到了硬件的发展,这其中的关系应该是多核CPU的发展为并发编程提供的硬件基础。摩尔定律并不是一种自然法则或者是物理定律,它只是基于认为观测数据后,对未来的一种预测。按照所预测的速度,我们的计算能力会按照指数级别的速度增长,不久以后会拥有超强的计算能力,正是在畅想未来的时候,2004年,Intel宣布4GHz芯片的计划推迟到2005年,然后在2004年秋季,Intel宣布彻底取消4GHz的计划,也就是说摩尔定律的有效性超过了半个世纪戛然而止。但是,聪明的硬件工程师并没有停止研发的脚步,他们为了进一步提升计算速度,而不是再追求单独的计算单元,而是将多个计算单元整合到了一起,也就是形成了多核CPU。短短十几年的时间,家用型CPU,比如Intel i7就可以达到4核心甚至8核心。而专业服务器则通常可以达到几个独立的CPU,每一个CPU甚至拥有多达8个以上的内核。因此,摩尔定律似乎在CPU核心扩展上继续得到体验。因此,多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升

顶级计算机科学家Donald Ervin Knuth如此评价这种情况:在我看来,这种现象(并发)或多或少是由于硬件设计者无计可施了导致的,他们将摩尔定律的责任推给了软件开发者。

另外,在特殊的业务场景下先天的就适合于并发编程。比如在图像处理领域,一张1024X768像素的图片,包含达到78万6千多个像素。即时将所有的像素遍历一边都需要很长的时间,面对如此复杂的计算量就需要充分利用多核的计算的能力。又比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。正是因为这些优点,使得多线程技术能够得到重视,也是一名CS学习者应该掌握的:

  • 充分利用多核CPU的计算能力;
  • 方便进行业务拆分,提升应用性能

2. 并发编程有哪些缺点

多线程技术有这么多的好处,难道就没有一点缺点么,就在任何场景下就一定适用么?很显然不是。

2.1 频繁的上下文切换

时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。通常减少上下文切换可以采用无锁并发编程,CAS算法,使用最少的线程和使用协程。

  • 无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
  • CAS算法,利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换
  • 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

由于上下文切换也是个相对比较耗时的操作,所以在"java并发编程的艺术"一书中有过一个实验,并发累加未必会比串行累加速度要快。 可以使用Lmbench3测量上下文切换的时长 vmstat测量上下文切换次数

2.2 线程安全

多线程编程中最难以把握的就是临界区线程安全问题,稍微不注意就会出现死锁的情况,一旦产生死锁就会造成系统功能不可用。

public class DeadLockDemo {    private static String resource_a = "A";    private static String resource_b = "B";?    public static void main(String[] args) {        deadLock();    }?    public static void deadLock() {        Thread threadA = new Thread(new Runnable() {            @Override            public void run() {                synchronized (resource_a) {                    System.out.println("get resource a");                    try {                        Thread.sleep(3000);                        synchronized (resource_b) {                            System.out.println("get resource b");                        }                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        });        Thread threadB = new Thread(new Runnable() {            @Override            public void run() {                synchronized (resource_b) {                    System.out.println("get resource b");                    synchronized (resource_a) {                        System.out.println("get resource a");                    }                }            }        });        threadA.start();        threadB.start();?    }}

在上面的这个demo中,开启了两个线程threadA, threadB,其中threadA占用了resource_a, 并等待被threadB释放的resource _b。threadB占用了resource _b正在等待被threadA释放的resource _a。因此threadA,threadB出现线程安全的问题,形成死锁。同样可以通过jps,jstack证明这种推论:

"Thread-1":  waiting to lock monitor 0x000000000b695360 (object 0x00000007d5ff53a8, a java.lang.String),  which is held by "Thread-0""Thread-0":  waiting to lock monitor 0x000000000b697c10 (object 0x00000007d5ff53d8, a java.lang.String),  which is held by "Thread-1"?Java stack information for the threads listed above:==================================================="Thread-1":        at learn.DeadLockDemo$2.run(DeadLockDemo.java:34)        - waiting to lock <0x00000007d5ff53a8(a java.lang.String)        - locked <0x00000007d5ff53d8(a java.lang.String)        at java.lang.Thread.run(Thread.java:722)"Thread-0":        at learn.DeadLockDemo$1.run(DeadLockDemo.java:20)        - waiting to lock <0x00000007d5ff53d8(a java.lang.String)        - locked <0x00000007d5ff53a8(a java.lang.String)        at java.lang.Thread.run(Thread.java:722)?Found 1 deadlock.

如上所述,完全可以看出当前死锁的情况。

那么,通常可以用如下方式避免死锁的情况:

  1. 避免一个线程同时获得多个锁;
  2. 避免一个线程在锁内部占有多个资源,尽量保证每个锁只占用一个资源;
  3. 尝试使用定时锁,使用lock.tryLock(timeOut),当超时等待时当前线程不会阻塞;
  4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

所以,如何正确的使用多线程编程技术有很大的学问,比如如何保证线程安全,如何正确理解由于JMM内存模型在原子性,有序性,可见性带来的问题,比如数据脏读,DCL等这些问题(在后续篇幅会讲述)。而在学习多线程编程技术的过程中也会让你收获颇丰。

3. 应该了解的概念

3.1 同步VS异步

同步和异步通常用来形容一次方法调用。同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。比如,在超时购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知去取就好。

3.2 并发与并行

并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。

3.3 阻塞和非阻塞

阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。

3.4 临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。

原文地址:https://www.cnblogs.com/itxiaok/p/10356569.html

时间: 2024-08-07 17:41:56

1.并发编程的优缺点的相关文章

Java并发编程的艺术,解读并发编程的优缺点

并发编程的优缺点 使用并发的原因 多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升. 在特殊的业务场景下先天的就适合于并发编程. 比如在图像处理领域,一张1024X768像素的图片,包含达到78万6千多个像素.即时将所有的像素遍历一边都需要很长的时间, 面对如此复杂的计算量就需要充分利用多核的计算的能力.又比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存, 生成订单等等这些操作,就可以进行拆分利用多线程的技术完成. 面对复

并发编程学习记录

重要知识点 现代系统基本的构造并发程序的方法 进程:用这个方法,每个逻辑流都是一个进程,由内核来调度和维护.因为进程有独立的虚拟地址空间,想要和其他流通信,控制流必须使用某种显式的进程间通信机制. I/O多路复用:在这种形式的并发编程中,应用程序在一个进程的上下文中显示地调度它们自己的逻辑流.逻辑流被模型化为状态机,数据到达文件描述符后,主程序显示地从一个状态转化到另一个状态.因为程序是一个单独的进程,所以所用流共享同一个地址空间. 线程:线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度

基本的并发编程模型

基于进程的并发 基本模型 在TCP服务器编程中,多进程并发服务器通常由主进程负责连接的建立,然后fork出子进程,负责该连接剩下的行为,直到关闭. 关于多进程并发服务器有几点重要的内容: 通常服务器会运行很长时间,因此必须要包括一个SIGCHLD处理程序,来回收僵死子进程的资源.因为当SIGCHLD处理程序执行时,SIGCHLD信号是阻塞的,而Unix信号是不排队的,所以SIGCHLD处理程序必须准备好回收多个僵死子进程的资源. 父进程在fork调用后,将连接交给子进程处理,父子进程必须关闭他们

python并发编程&amp;多线程(一)

本篇理论居多,实际操作见:  python并发编程&多线程(二) 一 什么是线程 在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程 线程顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程是一个进程 车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一个流水线 流水线的工作需要电源,电源就相当于cpu 所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位. 多线程(即多个控制线程)的概念

day10-python并发编程之多线程协程及MySQL

第1章 python并发编程之多线程 1.1 死锁现象与递归锁 1.1.1 死锁概念 进程也有死锁与递归锁,在进程那里忘记说了,放到这里一切说了额 所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁 1.1.2 博客实例 from threading import Thread,Lock import time mutexA=L

并发编程艺术-锁类型以及底层原理

Java并发编程艺术-并发机制的底层原理实现 1.Volatile 定义: Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量. volatile借助Java内存模型保证所有线程能够看到最新的值.(内存可见性) 实现原理: 将带有volatile变量操作的Java代码转换成汇编代码后,可以看到多了个lock前缀指令(X86平台CPU指令).这个lock指令是关键,在多核处理器下实现两个重要操作: 1.将当前处理器缓存行的数据写回到系

python 闯关之路四(下)(并发编程与数据库编程) 并发编程重点

并发编程重点: 1 2 3 4 5 6 7 并发编程:线程.进程.队列.IO多路模型 操作系统工作原理介绍.线程.进程演化史.特点.区别.互斥锁.信号. 事件.join.GIL.进程间通信.管道.队列. 生产者消息者模型.异步模型.IO多路复用模型.select\poll\epoll 高性 能IO模型源码实例解析.高并发FTP server开发 1.请写一个包含10个线程的程序,主线程必须等待每一个子线程执行完成之后才结束执行,每一个子线程执行的时候都需要打印当前线程名.当前活跃线程数量: 1

[并发编程的艺术] 02-Java并发机制的底层实现原理

Java代码在编译后会变成Java字节码,字节码被类加载起加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行, Java中所使用的并发机制依赖于JVM的实现和CPU的指令. 一.volatile的应用 在多处理器开发中保证共享变量的 "可见性", 可见性的意思是: 当一个线程修改一个共享变量时,另外一个线程能够读到这个修改的值.  如果 volatile变量修饰符使用恰当的话, 它比synchronized的使用和执行成本更低, 因为它不会引起线程上下文的切换和调度

2019年BAT面试通关宝典:数据结构+JVM+并发编程+分布式...

前言 金三银四俗称跳槽黄金季,很多同学都想趁着这段时间拿高薪,去更牛逼的公司工作,认识更多大牛,提升自己的职场竞争力. 那怎样才能通过BAT面试官的考核?怎样成为一名Offer收割机? 收割Offer有一个最直接的公示:Offer=硬实力+软实力+好的心态,三者缺一不可. 一.硬实力 这里说的硬实力,也就是技术上的真实积累. 怎么来体现你的技术实力?总的分为:技术深度和技术广度这两方面. 技术广度通俗的讲,就是你熟悉该技术点的使用以及基本原理.一般面试官在面试首轮会问很多技术点,来考核你是否能正