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


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

并发编程系列博客传送门java


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

进程(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上运行以前须要CPU给这个线程分配时间片,当时间片运行完以后这个线程就会让出CPU资源给其余的线程运行。可是线程在将CPU资源让出以前会保存当前的任务状态以便下次得到CPU资源以后能够继续往下执行。因此线程从保存当前执行状态到再加载的过程称为一次上下文切换。

减小上下文切换的措施

  • 无锁并发编程。多线程竞争锁时,会引发上下文切换,因此多线程处理数据时,能够用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不一样的线程处理不一样段的数据。
  • CAS算法。Java的Atomic包使用CAS算法来更新数据,而不须要加锁。
  • 使用最少线程。避免建立不须要的线程,好比任务不多,可是建立了不少线程来处理,这样会形成大量线程都处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

多线程编程的优点和挑战

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

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

参考

  • http://blog.chinaunix.net/uid-31404751-id-5753869.html
  • https://blog.csdn.net/hanchao5272/article/details/79513153
  • 《实战Java高并发程序设计》
  • 《Java并发编程艺术》
相关文章
相关标签/搜索