【并发编程】并发编程中你需要知道的基础概念

本博客系列是学习并发编程过程中的记录总结。由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅。

并发编程系列博客传送门



多线程是Java编程中一块非常重要的内容,其中涉及到很多概念。这些概念我们平时经常挂在嘴上,但是真的要让你介绍下这些概念,你可能还真的讲不清楚。这篇博客就总结下多线程编程中经常用到的概念,理解这些概念能帮助我们更好地掌握多线程编程。

进程(Process)与线程(Thread)

进程和线程是最常提到的概念了。在linux中,线程与进程最大的区别就是是否共享同一块地址空间,而且共享同一块地址空间的那一组线程将显现相同的PID号。下面介绍下两者的概念:

  • 进程是系统资源分配的最小单元,可以简单地理解为系统中运行的一个程序就是一个进程。
  • 线程是CPU调度的最小单元,是进程中的一个个执行流程。
  • 一个进程至少包含一个线程,可以包含多个线程,这些线程共享这个进程的资源。同时每个线程都拥有独立的运行栈和程序计数器,线程切换开销小。
  • 多进程指的是操作系统同时运行多个程序,如当前操作系统中同时运行着QQ、IE、微信等程序。
  • 多线程指的是同一进程中同时运行多个线程,如迅雷运行时,可以开启多个线程,同时进行多个文件的下载。

谈到线程和进程,又势必会涉及到线程号和进程号的概念。下面列举了各个ID的概念。

  • pid: 进程ID。
  • tgid: 线程组ID,也就是线程组leader的进程ID,等于pid。
  • lwp: 线程ID。在用户态的命令(比如ps)中常用的显示方式。
  • tid: 线程ID,等于lwp。tid在系统提供的接口函数中更常用,比如syscall(SYS_gettid)和syscall(__NR_gettid)。

并行(Parallel)、并发(Concurrent)

  • 并发:是指多个线程任务在同一个CPU上快速地轮换执行,由于切换的速度非常快,给人的感觉就是这些线程任务是在同时进行的,但其实并发只是一种逻辑上的同时进行;
  • 并行:是指多个线程任务在不同CPU上同时进行,是真正意义上的同时执行。

下面贴上一张图来解释下这两个概念:

上图中的咖啡就可以看成是CPU,上面的只有一个咖啡机,相当于只有一个CPU。想喝咖啡的人只有等前面的人制作完咖啡才能制作自己的开发,也就是同一时间只能有一个人在制作咖啡,这是一种并发模式。下面的图中有两个咖啡机,相当于有两个CPU,同一时刻可以有两个人同时制作咖啡,是一种并行模式。

我们发现并行编程中,很重要的一个特点是系统具有多核CPU。要是系统是单核的,也就谈不上什么并行编程了。

线程安全

这个概念可能是在多线程编程中提及最多的一个概念了。在面试过程中,我试着问过几个面试者,但是几乎没人能将这个概念解释的很好的。

关于这个概念,我觉得好多人都有一个误区,包括我自己一开始也是这样的。我一开始认为线程安全讲的是某个共享变量线程安全,其实我们所说的线程安全是指某段代码或者是某个方法是线程安全的。线程安全的准确定义应该是这样的:

如果线程的随机调度顺序不影响某段代码的最后执行结果,那么我们认为这段代码是线程安全的

为了保证代码的线程安全,Java中推出了很多好用的工具类或者关键字,比如volatile、synchronized、ThreadLocal、锁、并发集合、线程池和CAS机制等。这些工具并不是在每个场景下都能满足我们多线程编程的需求,并不是在每个场景下都有很高的效率,需要我们程序员根据具体的场景来选择最适合的技术,这也许就是我们程序员存在的价值所在。(我一直觉得如果有一个技术能很好的解决大多数场景下的问题,那么这个领域肯定是可以做成机器自动化的。那么对于这个领域就不太需要有多少人参与了。)

死锁

线程1占用了锁A,等待锁B,线程2占用了锁B,等待锁A,这种情况下就造成了死锁。在死锁状态下,相关的代码将不能再提供服务。

private void deadLock() {
      Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (lock1) {
              try {
                Thread.currentThread().sleep(2000);
              } catch (InterruptedException e) {
                e.printStackTrace();
              }
              synchronized (lock2) {
                System.out.println("1");
              }
            }
        }
      });
      Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (lock2) {
              synchronized (lock1) {
                System.out.println("2");
              }
            }
        }
      });
      t1.start();
      t2.start();
    }

这段代码只是演示死锁的场景,在现实中你可能不会写出这样的代码。但是,在一些更为复杂的场景中,你可能会遇到这样的问题,比如t1拿到锁之后,因为一些异常情况没有释放锁(死循环)。又或者是t1拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉。

如果你怀疑代码中有线程出现了死锁,你可以dump线程,然后查看线程状态有没有Blocked的线程(java.lang.Thread.State: BLOCKED)


"Thread-2" prio=5 tid=7fc0458d1000 nid=0x116c1c000 waiting for monitor entry [116c1b000]
    java.lang.Thread.State: BLOCKED (on object monitor)
     at com.ifeve.book.forkjoin.DeadLockDemo$2.run(DeadLockDemo.java:42)
     - waiting to lock <7fb2f3ec0> (a java.lang.String)
     - locked <7fb2f3ef8> (a java.lang.String)
     at java.lang.Thread.run(Thread.java:695)

"Thread-1" prio=5 tid=7fc0430f6800 nid=0x116b19000 waiting for monitor entry [116b18000]
    java.lang.Thread.State: BLOCKED (on object monitor)
     at com.ifeve.book.forkjoin.DeadLockDemo$1.run(DeadLockDemo.java:31)
     - waiting to lock <7fb2f3ef8> (a java.lang.String)
     - locked <7fb2f3ec0> (a java.lang.String)
     at java.lang.Thread.run(Thread.java:695)

避免死锁的几个方式:

  • 尽量不要一个线程同时占用多个锁;
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

饥饿

饥饿是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。比如它的线程优先级可能太低,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。

在自然界中,母鸟给雏鸟喂食时很容易出现这种情况:由于雏鸟很多,食物有限,雏鸟之间的食物竞争可能非常厉害,经常抢不到食物的雏鸟有可能会被饿死。线程的饥饿非常类似这种情况。

此外,某一个线程一直占着关键资源不放,导致其他需要这个资源的线程无法正常执行,这种情况也是饥饿的一种。与死锁相比,饥饿还是有可能在未来一段时间内解决的(比如,高优先级的线程已经完成任务,不再疯狂执行)。

活锁

活锁是一种非常有趣的情况。不知道大家是否遇到过这么一种场景,当你要坐电梯下楼时,电梯到了,门开了,这时你正准备出去。但很不巧的是,门外一个人挡着你的去路,他想进来。于是,你很礼貌地靠左走,避让对方。同时,对方也非常礼貌地靠右走,希望避让你。结果,你们俩就又撞上了。于是乎,你们都意识到了问题,希望尽快避让对方,你立即向右边走,同时,他立即向左边走。结果,又撞上了!不过介于人类的智能,我相信这个动作重复两三次后,你应该可以顺利解决这个问题。因为这个时候,大家都会本能地对视,进行交流,保证这种情况不再发生。

但如果这种情况发生在两个线程之间可能就不会那么幸运了。如果线程的智力不够,且都秉承着“谦让”的原则,主动将资源释放给他人使用,那么就会导致资源不断地在两个线程间跳动,而没有一个线程可以同时拿到所有资源正常执行。这种情况就是活锁。

同步(Synchronous)和异步(Asynchronous)

这边讨论的同步和异步指的是同步方法和异步方法。

同步方法是指调用这个方法后,调用方必须等到这个方法执行完成之后才能继续往下执行。
异步方法是指调用这个方法后会立马返回,调用方能立马往下继续执行。被调用的异步方法其实是由另外的线程进行执行的,如果这个异步方法有返回值的话可以通过某种通知的方式告知调用方。

实现异步方法的方式:

  • 回调函数模式:一个方法被调用后立马返回,调用结果通过回调函数返回给调用方;
  • MQ(发布/订阅):请求方将请求发送到MQ,请求处理方监听MQ处理这些请求,并将请求处理结果也返回给某个MQ,调用方监听这个Queue获取处理结果;
  • 多线程处理模式:系统创建其他线程处理调用请求,比如Spring中的@Async注解标注的方法就是这种方法。

临界区

涉及读写共享资源的代码片段叫“临界区”。

比如下面代码中,1处和2处就是一个代码临界区。

private static class BankAccount{
        String accountName;
        double balance;

        public BankAccount(String accountName,double balance){
            this.accountName = accountName;
            this.balance = balance;
        }

        public synchronized   double deposit(double amount){
            balance = balance + amount; //1
            return balance;
        }

        public synchronized  double  withdraw(double amount){
            balance = balance - amount; //2
            return balance;
        }

    }

多线程编程的优势和挑战

使用并发编程的目的是让程序运行的更快(更大限度的使用CPU资源,让程序运行更快),但是在进行并发编程的过程也会遇到一些挑战。

PS:多线程并发编程可以让我们最大限度的使用系统的CPU资源,以达到让程序运行更快的目的(不是所有情况下多线程都更快)。但是一个硬币具有两面性,引入多线程编程会给我们带来其他的问题,比如说线程的上下文切换问题、共享变量的线程安全问题、线程间通信问题、线程死锁问题和硬件资源对多线程的影响等问题。其实研究多线程并发编程就是在研究这对矛盾体,怎么在享受多线程并发编程给我们带来便利的同时又能避开多线程带来的坑。JDK中给我们提供很多多线程相关的类

参考

  • http://blog.chinaunix.net/uid-31404751-id-5753869.html
  • https://blog.csdn.net/hanchao5272/article/details/79513153
  • 《实战Java高并发程序设计》

原文地址:https://www.cnblogs.com/54chensongxia/p/11935433.html

时间: 2024-07-29 15:04:52

【并发编程】并发编程中你需要知道的基础概念的相关文章

Python 中的协程 (3) 基础概念

1 进程和线程 进程 Process:一个正在运行的应用程序在操作系统中被视为一个进程,进程可以包括一个或多个线程.线程是操作系统分配处理器时间的基本单元,在进程中可以有多个线程同时执行代码.进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式). 线程 Thread: 线程是进程中的基本执行单元,是操作系统分配CPU时间的基本单位,一个进程可以包含若干个线程,在进程入口执行的第一个线程被视为这个进程的主线程.线程主要是由CPU寄存器.调用栈和线程本地存储器(Threa

编程学习记录4:OOP的一些基础概念

面向对象编程(OOP, Object Oritented Programing) OOP的一些相关单词 单词 说明 class 类.种类 public 公有的 private 私有的 object 对象.物体 encapsulation 封装.包装 attribute 属性.特征 method 方法 member 成员 constructor 构造方法.构造器 OOP的一些相关概念 1.类:具有相同属性和行为的一组对象的集合(学生). 2.对象:具有明确定义状态和行为的实体(学生张三). 3.属

C#并行编程-并发集合

原文:C#并行编程-并发集合 菜鸟学习并行编程,参考<C#并行编程高级教程.PDF>,如有错误,欢迎指正. 背景 基于任务的程序设计.命令式数据并行和任务并行都要求能够支持并发更新的数组.列表和集合. 在.NET Framework 4 以前,为了让共享的数组.列表和集合能够被多个线程更新,需要添加复杂的代码来同步这些更新操作. 如您需要编写一个并行循环,这个循环以无序的方式向一个共享集合中添加元素,那么必须加入一个同步机制来保证这是一个线程安全的集合. System.Collenctions

基于UDP协议的socket套接字编程 基于socketserver实现并发的socket编程

基于UDP协议 的socket套接字编程 1.UDP套接字简单示例 1.1服务端 import socket server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 建立一个UDP协议的服务器 server.bind(("127.0.0.1",8080)) while True: data,addr = server.recvfrom(1024) server.sendto(data.upper(),addr) server

Java并发编程(02):线程核心机制,基础概念扩展

本文源码:GitHub·点这里 || GitEE·点这里 一.线程基本机制 1.概念描述 并发编程的特点是:可以将程序划分为多个分离且独立运行的任务,通过线程来驱动这些独立的任务执行,从而提升整体的效率.下面提供一个基础的演示案例. 2.应用案例 场景:假设有一个容器集合,需要拿出容器中的每个元素,进行加工处理,一般情况下直接遍历就好,如果数据偏大,可以根据线程数量对集合切割,每个线程处理一部分数据,这样处理时间就会减少很多. public class ExtendThread01 { publ

python之高性能网络编程并发框架eventlet实例

http://blog.csdn.net/mingzznet/article/details/38388299 前言: 虽然 eventlet 封装成了非常类似标准线程库的形式,但线程和eventlet在实际并发执行流程仍然有明显区别.在没有出现 I/O 阻塞时,除非显式声明,否则当前正在执行的 eventlet 永远不会把 cpu 交给其他的 eventlet,而标准线程则是无论是否出现阻塞,总是由所有线程一起争夺运行资源.所有 eventlet 对 I/O 阻塞无关的大运算量耗时操作基本没有

为什么函数式编程在Java中很危险?

摘要:函数式编程这个不温不火的语言由来已久.有人说,这一年它会很火,尽管它很难,这也正是你需要学习的理由.那么,为什么函数式编程在Java中很危险呢?也许这个疑问普遍存在于很多程序员的脑中,作者Elliotte对此发表了一些见解,我们一起来看看他是怎么说的. 在我的日常工作中,我身边的开发者大多是毕业于CS编程顶级院校比如MIT.CMU以及Chicago,他们初次涉及的语言是Haskell.Scheme及Lisp.他们认为函数式编程是一种自然的.直观的.美丽的且高效的编程样式.但奇怪的是,我和我

编程算法 - 数组中的逆序对 代码(C)

数组中的逆序对 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 题目: 在数组中的两个数字如果前面一个数字大于后面的数字, 则这两个数字组成一个逆序对. 输入一个数组, 求出这个数组中的逆序对的总数. 使用归并排序的方法, 辅助空间一个排序的数组, 依次比较前面较大的数字, 算出整体的逆序对数, 不用逐个比较. 时间复杂度: O(nlogn) 代码: /* * main.cpp * * Created on: 2014.6.12 * Author:

编程算法 - 数组中只出现一次的数字 代码(C)

数组中只出现一次的数字 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 题目: 一个整型数组里除了两个数字以外, 其他的数字都出现了两次. 请写程序找出这两个只出现一次的数字. 如果从头到尾依次异或数组中的每一个数字, 那么最终的结果刚好是那个只出现一次的数字. 根据结果数组二进制某一位为1, 以此分组, 为1的一组, 为0的一组, 再重新进行异或. 最后得出两个结果. 时间复杂度O(n). 代码: /* * main.cpp * * Create