美文网首页
从线程到并发编程

从线程到并发编程

作者: felixu | 来源:发表于2019-01-10 15:32 被阅读0次

    何为线程

    说起线程,还是得从进程说起。那么进程是什么呢?现代操作系统在运行一个程序时,会为其创建一个进程。比如你电脑上打开个QQ或者是启动一个Java程序,操作系统都会为其创建一个进程。而线程是操作系统的最小调度单元,一个进程中可以有多个线程。OS调度会让多个线程之间高速切换,让我们以为是多个线程在同时执行。

    线程的创建与销毁

    线程的创建

    那么怎么去创建一个线程呢。在Java中我们有且仅有一种(至于为啥说只有一种,而网上说有三种,那是因为其他两种并非创建线程,而是创建了线程的执行单元,最后还是交由Thread去执行)方式来创建线程,那就是new Thread(),但是我们却可以有以下三种方式来使用:

    1. 继承Thread类,重写run方法。

      public class ThreadDemo1 extends Thread {
      
          @Override
          public void run() {
              System.out.println("extends thread run");
          }
      
          public static void main(String[] args) {
              ThreadDemo1 thread1 = new ThreadDemo1();
              ThreadDemo1 thread2 = new ThreadDemo1();
              thread1.start();
              thread2.start();
          }
      }
      
    2. 实现Runnable接口,重写run方法。

      public class ThreadDemo2 implements Runnable{
      
          @Override
          public void run() {
              System.out.println(Thread.currentThread().getName() + " implements runnable run");
          }
      
          public static void main(String[] args) {
              new Thread(new ThreadDemo2(), "thread1").start();
              new Thread(new ThreadDemo2(), "thread2").start();
          }
      }
      
    3. 实现Callable接口,重写call方法,实现带返回值的线程。

      public class ThreadDemo3 implements Callable<String> {
      
          public static void main(String[] args) throws ExecutionException, InterruptedException {
              ExecutorService executorService = newFixedThreadPool(1);
              ThreadDemo3 thread = new ThreadDemo3();
              Future<String> future = executorService.submit(thread);
              System.out.println(future.get());
              executorService.shutdown();
          }
      
          @Override
          public String call() throws Exception {
              System.out.println(Thread.currentThread().getName() + " implements callable");
              return Thread.currentThread().getName();
          }
      }
      

    终止线程

    1. interrupt中断标志

      前面看完了如何创建一个线程,那么又怎么去终止一个线程呢。以前的Thread类中有个stop方法可以用来终止线程,而现在已经被标记过期了,其实也不建议使用stop方法来终止线程,为什么呢!因为我想用过Linux系统的都知道kill -9吧,stop方法与其类似,stop方法会强制杀死线程,而不管线程中的任务是否执行完毕,那么我们如何更加优雅的去终止一个线程呢。

      这里Thread类为我们提供了一个interrupt方法。

      当我们需要终止一个线程,可以调用它的interrupt方法,相当于告诉这个线程你可以终止了,而不是暴力的杀死该线程,线程会自行中断,我们可以使用isInterrupted方法来判断线程是否已经终止了,我们可以用下面的代码来加以验证:

      public class InterruptDemo {
          private static int i;
          public static void main(String[] args) throws InterruptedException {
              Thread thread = new Thread(() -> {
                  while(!Thread.currentThread().isInterrupted()){
                      i++;
                  }
                  System.out.println("result: " + i);
              }, "interrupt-test");
              thread.start();
              TimeUnit.SECONDS.sleep(2);
              thread.interrupt();
          }
      }
      

      如果interrupt方法无法终止线程,那么这个线程将会是死循环,而无法结束。这里使用interrupt以一种更加安全中断线程。

    2. volatile共享变量作为中断标志

      这里先不介绍volatile的内存语义以及原理,它可以解决共享变量的内存可见性问题,使其他线程可以及时看到被volatile变量修饰的共享变量的变更,所以我们也可以使用volatile来达到中断线程的目的。

      public class VolatileDemo {
      
          private volatile static boolean flag = false;
        
          public static void main(String[] args) throws InterruptedException {
              Thread thread = new Thread(() -> {
                  long i = 0L;
                  while (!flag) {
                      i++;
                  }
                  System.out.println(i);
              }, "volatile-demo");
              thread.start();
              System.out.println("volatile-demo is start");
              Thread.sleep(1000);
              flag = true;
          }
      }
      

      比如上面示例中的代码,我们可以控制在特定的地方,改变共享变量,来达到让线程退出。

    线程复位

    • interrupted

      前面说了使用interrupt可以告诉线程可以中断了,线程同时也提供了另外一个方法即Thread.interrupted()可以将已经设置过中断标志的线程进行复位。

      public class InterruptDemo {
          public static void main(String[] args) throws InterruptedException {
              Thread thread = new Thread(() -> {
                  while (true) {
                      boolean isInterrupted = Thread.currentThread().isInterrupted();
                      if(isInterrupted){
                          System.out.println("before: " + isInterrupted);
                          Thread.interrupted(); // 对线程进行复位,中断标识为false
                          System.out.println("after: " + Thread.currentThread().isInterrupted());
                      }
                  }
              }, "InterruptDemo");
              thread.start();
              TimeUnit.SECONDS.sleep(1);
              thread.interrupt(); // 设置中断标识为true
          }
      }
      

      输出结果:

      before: true
      after: false
      

      通过demo可以看到线程确实是先被设置了中断标识,后又被复位。

    • 异常复位

      除了使用interupted来设置中断复位,还有一种情况,就是对抛出InterruptedException异常的方法,在 InterruptedExceptio抛出之前,JVM会先把线程的中断标识位清除,然后才会抛出 InterruptedException,这个时候如果调用isInterrupted方法,将会返回false,例如:

      public class InterruptDemo {
          public static void main(String[] args) throws InterruptedException {
              Thread thread = new Thread(() -> {
                  while (true) {
                      try {
                          Thread.sleep(10000);
                      } catch (InterruptedException e) {
                          // 抛出InterruptedException会将复位标识设置为false
                          e.printStackTrace();
      
                      }
                  }
              }, "InterruptDemo");
              thread.start();
              TimeUnit.SECONDS.sleep(1);
              thread.interrupt(); // 设置中断标志为true
              TimeUnit.SECONDS.sleep(1);
              System.out.println(thread.isInterrupted()); 
          }
      }
      

      输出结果:

      java.lang.InterruptedException: sleep interrupted
        at java.lang.Thread.sleep(Native Method)
        at top.felixu.chapter1.lifecycle.InterruptDemo.lambda$main$0(InterruptDemo.java:48)
        at java.lang.Thread.run(Thread.java:748)
      false
      
      

      通过例子可以看到,在抛出异常之后,isInterrupted确实是又变成了false

    为什么要并发编程

    单线程有时候也可以解决问题啊,那么我们为什么还要并发编程呢,很大程度上是因为更好的利用CPU资源,提升我们系统的性能。根据摩尔定律(当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买到的电脑性能,将每隔18-24个月翻一倍以上。这一定律揭示了信息技术进步的速度。)推算,不久就会有超强的计算能力,然而,事情并未像预料的那样发展。2004年,Intel宣布4GHz芯片的计划推迟到2005年,然后在2004年秋季,Intel宣布彻底取消4GHz的计划。现在虽然有4GHz的芯片但频率极限已逼近,而且近10年停留在4GHz,也就是摩尔定律应该是失效了。既然单核CPU的计算能力短期无法提升了,而我们伟大的硬件科学家们可不会承认自己的理论是错的,那既然一个CPU搞不定了,那么就多个嘛,多核CPU在此时应运而生。单线程毕竟只可能跑在一个核心上,浪费了CPU的资源,从而催生了并发编程,并发编程是为了发挥出多核CPU的计算能力,提升性能。

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

    并发编程总结起来说大致有以下优点:

    • 充分利用CPU,提高计算能力。

    • 方便对业务的拆分。比如一个购物流程,我们可以拆分成下单,减库存等,利用多线程来加快响应。

    • 对于需要阻塞的场景,可以异步处理,来减少阻塞。

    • 对于执行性能,可以通过多线程并行计算。

    并发编程有哪些问题

    看起来好像多线程确实很好,那么我们就可以尽量多的去开线程了嘛。也并不是这样的,多线程的性能也受多方面因素所影响:

    • 时间片的切换

      时间片是CPU分配给线程执行的时间,即便是单核CPU也是可以通过时间片的切换使多个线程切换执行,让我们觉得是多个线程在同时执行,因为时间片的切换是非常快的,我们感觉不到的。每次切换线程是需要时间的,而且切换的时候需要保存当前线程的状态,以便切换回来的时候可以继续执行。所以当线程较多的时候,切换时间片所带来的消耗也同样可观。那么有没有什么姿势可以解决这个问题呢,是有的:

      • 无锁并发编程:多线程在竞争锁时会引起上下文的切换,可以使用对数据Hash取模分段的思想来避免使用锁。
      • CAS算法:可以使用Atomic包中相关原子操作,来避免使用锁。
      • 使用最少线程:根据业务需求创建线程数,过多的创建线程会造成线程闲置和资源浪费。
      • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
    • 死锁

      为了保证多线程的正确性,很多时候,我们都会使用锁,它是一个很好用的工具,然而在一些时候,不正确的姿势会造成死锁问题,进而引发系统不可用。下面我们就来看一个死锁案例:

      public class DeadLockDemo {
      
          public static void main(String[] args) {
              new DeadLockDemo().deadLock();
          }
      
          private void deadLock() {
              Object o1 = new Object();
              Object o2 = new Object();
              Thread one = new Thread(() -> {
                  synchronized (o1) {
                      try {
                          Thread.sleep(2000);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      synchronized (o2) {
                          System.out.println(Thread.currentThread().getName());
                      }
                  }
              }, "thread-one");
      
              Thread two = new Thread(() -> {
                  synchronized (o2) {
                      synchronized (o1) {
                          System.out.println(Thread.currentThread().getName());
                      }
                  }
              }, "thread-two");
      
              one.start();
              two.start();
          }
      }
      

      运行之后便会发现程序无法终止了,那么究竟发生了什么呢?我们通过jps命令来查看一下当前JavaPID

      $ jps
      1483 DeadLockDemo
      

      可以看到当前的程序PID1483(每个人的都不一样,得自己执行哦),接下来我们使用jstack命令dump出当前程序的线程信息,看一下究竟发生了什么。

       jstack 1483
      . . . . . .省略部分信息
      "thread-two" #12 prio=5 os_prio=31 tid=0x00007fbba9956800 nid=0x5603 waiting for monitor entry [0x0000700011058000]
         java.lang.Thread.State: BLOCKED (on object monitor)
              at top.felixu.section1.deadlock.DeadLockDemo.lambda$deadLock$1(DeadLockDemo.java:32)
              - waiting to lock <0x000000076ada81b8> (a java.lang.Object)
              - locked <0x000000076ada81c8> (a java.lang.Object)
              at top.felixu.section1.deadlock.DeadLockDemo$$Lambda$2/381259350.run(Unknown Source)
              at java.lang.Thread.run(Thread.java:748)
      
      "thread-one" #11 prio=5 os_prio=31 tid=0x00007fbba8033800 nid=0xa803 waiting for monitor entry [0x0000700010f55000]
         java.lang.Thread.State: BLOCKED (on object monitor)
              at top.felixu.section1.deadlock.DeadLockDemo.lambda$deadLock$0(DeadLockDemo.java:24)
              - waiting to lock <0x000000076ada81c8> (a java.lang.Object)
              - locked <0x000000076ada81b8> (a java.lang.Object)
              at top.felixu.section1.deadlock.DeadLockDemo$$Lambda$1/1607521710.run(Unknown Source)
              at java.lang.Thread.run(Thread.java:748)
      . . . . . .省略部分信息
      Found one Java-level deadlock:
      =============================
      "thread-two":
        waiting to lock monitor 0x00007fbba9006eb8 (object 0x000000076ada81b8, a java.lang.Object),
        which is held by "thread-one"
      "thread-one":
        waiting to lock monitor 0x00007fbba90082a8 (object 0x000000076ada81c8, a java.lang.Object),
        which is held by "thread-two"
      
      Java stack information for the threads listed above:
      ===================================================
      "thread-two":
              at top.felixu.section1.deadlock.DeadLockDemo.lambda$deadLock$1(DeadLockDemo.java:32)
              - waiting to lock <0x000000076ada81b8> (a java.lang.Object)
              - locked <0x000000076ada81c8> (a java.lang.Object)
              at top.felixu.section1.deadlock.DeadLockDemo$$Lambda$2/381259350.run(Unknown Source)
              at java.lang.Thread.run(Thread.java:748)
      "thread-one":
              at top.felixu.section1.deadlock.DeadLockDemo.lambda$deadLock$0(DeadLockDemo.java:24)
              - waiting to lock <0x000000076ada81c8> (a java.lang.Object)
              - locked <0x000000076ada81b8> (a java.lang.Object)
              at top.felixu.section1.deadlock.DeadLockDemo$$Lambda$1/1607521710.run(Unknown Source)
              at java.lang.Thread.run(Thread.java:748)
      
      Found 1 deadlock.
      
      

      从上面来看,两个线程都是阻塞状态,都在等待别的线程释放锁,但是永远都等不到,从而形成了死锁。那么平常开发过程中尽量按以下操作来避免不必要的死锁(当然有时候不注意还是会莫名死锁,得dump信息加以分析才能找出问题的):

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

      程序跑在服务器上,必然受到服务器等方面的限制。

      • 硬件资源限制:一般指磁盘读写速度、带宽、CPU性能等方面
      • 软件资源限制:一般指数据库连接数、Socket连接数等方面

    所以,如何合理的使用线程需要我们在实践中具体去分析。

    结语

    并发编程一直是个难点,也是在面试中不可避免被问到的知识点,后面会更多讨论其他方面的知识点,也需要更多的动手实践,才能体会其中的一些深层意义。

    参考自《Java并发编程的艺术》

    相关文章

      网友评论

          本文标题:从线程到并发编程

          本文链接:https://www.haomeiwen.com/subject/zqearqtx.html