安琪拉教百里守约学并发编程之多线程基础

《安琪拉与面试官二三事》系列文章
一个HashMap能跟面试官扯上半个小时
一个synchronized跟面试官扯了半个小时

《安琪拉教鲁班学算法》系列文章

安琪拉教鲁班学算法之动态规划

安琪拉教鲁班学算法之BFS和DFS

安琪拉教鲁班学算法之堆排序
《安琪拉教妲己学分布式》系列文章
安琪拉教妲己分布式限流
《安琪拉教百里守约学并发编程》系列文章
安琪拉教百里守约学并发编程之多线程基础

本文是来自读者群里@凯的建议,决定开一个多线程的专栏,尊重读者安琪拉是认真的,为什么是教百里守约,也是因为读者群里@百里守约是安琪拉的忠实读者,每期必读,经常草丛里定点蹲安琪拉,然后抢板凳(放大招)。这期是《安琪拉教百里守约学并发线程》系列文章第一集多线程基础。

前言

并发编程应该是Java 后端工程必备的技能,在日常开发中用的好能提升系统吞吐量,提升业务逻辑执行效率,提高系统的响应性,简化程序结构,当然这把青龙偃月刀也不是随随便便就能耍的好,需要些内力。先放一张Java 并发工具包JUC的知识脑图,后面 Wx公众号【安琪拉的博客】《安琪拉教百里守约学并发线程》会按以下思维脑图详细介绍 JUC 的各部分组件实际使用场景以及组件特性:

开场

百里守约:安琪拉,你熟悉线程(Thread)吗?和进程(Process)有什么区别?

安琪拉:熟悉啊!一个应用就是一个进程,一个进程可以包含多个线程,从操作系统层面看,同一个进程中的线程共享该进程的资源,例如内存空间和文件句柄。Linux 操作系统中线程是轻量级进程。

百里守约:在Java 中怎么创建一个线程呢?

安琪拉:线程的创建有2 种方式,如下,很多网上的文章还写了通过线程池的方式创建,其本质也是这二种中的一种:

  1. 继承 Thread 类;
  2. 实现 Runnable 接口;

百里守约:能不能用实际的代码举例说一下?

安琪拉:可以,如下所示:

public static void main(String[] args) {
  new Seller("笔").start();
  new Thread(new Seller02("书")).start();
}

//第一种方式 继承 Thread
public static class Seller extends Thread{

  String product;

  public Seller(String product){
    this.product = product;
  }

  @Override
  public void run() {
    System.out.println("继承 Thread类 卖 " + product);
  }
}

//第二种方式 实现 Runnable
public static class Seller02 implements Runnable{

  String product;

  public Seller02(String product){
    this.product = product;
  }

  @Override
  public void run() {
    System.out.println("实现 Runnable接口 卖 " + product);
  }
}

百里守约:如果我直接使用 new Seller("笔").run() 执行和start() 有什么区别?

安琪拉start() 方法是native 方法,JVM 会另起一个线程执行,而直接执行run() 方法是本地线程执行,我们可以使用示例程序对比一下,如下:

public static void main(String[] args) {
  new Seller("笔").run(); //没有另起一个线程
  new Seller("笔").start(); //在新线程中执行 run 函数
}

//第一种方式 继承 Thread
public static class Seller extends Thread{

  String product;

  public Seller(String product){
    this.product = product;
  }

  @Override
  public void run() {
    System.out.println(String.format("当前线程: %s 卖%s", Thread.currentThread().getName(), product));
  }
}

看下控制台输出如下:

当前线程: main 卖笔
当前线程: Thread-1 卖笔

因为调用start() 方法后,JVM 会新建一个线程来执行run() 方法内容。

百里守约:我理解了,Thread 对象是Java 中普通的对象,和其他对象一样,只是在调用 start() 这个native 方法时变得不一样了,JVM 会根据Thread 对象来创建线程。

安琪拉:你说的非常对,new Thread() 创建Thread 对象时,JVM 还没有实际创造线程,调用start() 方法后JVM 才会通过 pthread_create 方法(Linux系统)创建线程。因此一定要将Thread 对象和真实的线程区分开。

百里守约:那JVM 又是如何创建线程的呢?

安琪拉:你这个问的有点深了,我可以大致讲讲,因为今天是基础篇,因此不展开聊,想深入了解的可以关注【安琪拉的博客】公众号,有源代码层的详细的讲解,先丢个源代码地址:Hotspot1.8 jvm.cpp,推荐先将本篇文章整体看完,然后回过头来再看实现原理。担心很多同学没学过c++,或者源码太多无从下嘴,后面会出一期JVM 创建线程源代码解析。

今天先丢个大致原理:Java 种新建的Thread 对象只是操作系统线程运行的载体,Thread类的作用主要有二点:

  • Thread 对象内的属性提供了创建新线程时所需要的线程描述信息,例如线程名、线程id、线程组、是否为守护线程;
  • Thread 对象内的方法提供了Java 程序可以跟操作系统线程打交道的手段,例如wait、sleep、join、interrupt等。

前面说到JVM new Thread对象时其实还没有真实创建线程,调用start() 方法时才开始正式创建。

百里守约:那线程是怎么从创建到执行,最后销毁的啊?

安琪拉:那你就要看Java 中线程的生命周期了,如下图所示:

线程状态转换图

在 Thread 类中有个State 枚举类型标识线程状态,如下。

public static enum State {
  NEW,
  RUNNABLE,
  BLOCKED,
  WAITING,
  TIMED_WAITING,
  TERMINATED;
}

同时可以使用Thread.currentThread().getState()获取当前线程的状态。

解释一下每种状态:

  • New: 刚创建而未启动的线程就是这个状态。由于一个线程只能被启动一次,因此一个线程只可能有一次在这个状态。
  • Runnable:如上图,这个状态实际是个复合状态,包含二个子状态:Ready 和 Running。Ready是就绪状态,可以被JVM 线程调度器(Scheduler) 进行调度,如果是单核CPU,同一时刻只有一个线程处于Running 状态,可能有多个线程处于 Ready 状态,Running 表示当前线程正在被CPU 执行,在Java 中就是Thread 对象只 run() 方法正在被执行。当 yield() 方法被调用,或者线程时间片被用完,线程就会从 Running 状态转为 Ready 状态。另外有个小姿势点,CPU 的一个时间片时间是多久呢? 这个展开来讲又可以单独写篇文章,这里只说一个结论:CPU时间片和主机时钟频率有关系,一般是10 ~ 20 ms。
  • Blocked:一个线程发生一个阻塞式I/0 (文件读写I/O, 网络读写I/O)时,或者试图获取其他线程持有的锁时,线程会进入此状态,例如:获取别的线程已经持有的 synchronized 修饰的对象锁。如果大家对synchronized 关键字感兴趣,可以看我这篇文章 一个synchronized跟面试官扯了半个小时,建议看完这篇再回过头看,顺便还可以点个赞。在Blocked 状态的线程不会占用CPU 资源,但是程序如果出现大量处于这个状态的线程,需要警惕了,可以考虑优化一下程序性能。
  • Waiting: 一个线程执行了Object.wait( )、 Thread.join( ) 、LockSupport.park( ) 后会进入这个状态,这个状态是处于无限等待状态,没有指定等待时间,可以和Timed_Waiting 对比,Timed_Waiting是有等待时间的。这个状态的线程如果要恢复到Runnable 状态需要通过别的线程调用Object.notify( )、Object.notifyAll( )、LockSupport.unpark( thread )。
  • Timed_Waiting: 带时间限制的Waiting。
  • Terminated: 已经执行结束的线程处于此状态。Thread 的 run( ) 方法执行结束,或者由于异常而提前终止都会让线程处于这个状态。

百里守约:你刚才上面讲了wait( )、sleep( )、join( )、yield( ) 、notify()、notifyAll( ) 都是做什么的?什么区别?

安琪拉:这些方法都是线程控制方法,JAVA 通过这些方法跟它创建的操作系统线程进行交互,具体如下:

  • wait:线程等待,调用该方法会让线程进入 Waiting 状态,同时很重要的一点,线程会释放对象锁,所以wait 方法一般用在同步方法或同步代码块中;

  • sleep: 线程休眠,调用该方法会让线程进入Time_Waiting 状态,调sleep 方法需要传入一个参数标识线程需要休眠的时间;

  • yield:线程让步,yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争CPU 时间片,一般来说,优先级高的线程有更大的可能性成功竞争到CPU 时间片,但不是绝对的,有的系统对优先级不敏感。

  • join:在当前线程中调用另一个线程的join 方法,则当前线程转为阻塞状态,等到另一线程执行结束,当前线程才会从阻塞状态变为就绪状态,等待CPU 的调度。写个代码一看就明白:

    public static void main(String[] args) {
      System.out.println(String.format("主线程%s 开始运行...", Thread.currentThread().getName()));
      Thread threadA = new Thread(new ThreadA());
      threadA.start();
      try {
        // 主线程 wait(0) 释放 thread 对象锁,主线程进入 waiting 状态
        threadA.join();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    
      System.out.println(String.format("主线程%s 运行结束...", Thread.currentThread().getName()));
    }
    
    private static class ThreadA implements Runnable{
    
      @Override
      public void run() {
        System.out.println(String.format("子线程%s 开始运行...", Thread.currentThread().getName()));
    
        try {
          Thread.sleep(3000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(String.format("子线程%s 准备结束运行...", Thread.currentThread().getName()));
      }
    }
    

    控制台输出如下:

    主线程main 开始运行...
    子线程Thread-0 开始运行...
    子线程Thread-0 准备结束运行...
    主线程main 运行结束...
    

    主线程调用threadA.join() 导致主线程等Thread-0 线程执行结束才开始继续执行。

    join() 函数的内部实现如下:

    public final void join() throws InterruptedException {
      join(0);
    }
    /**
         * Waits at most {@code millis} milliseconds for this thread to
         * die. A timeout of {@code 0} means to wait forever.
         *
         * <p> This implementation uses a loop of {@code this.wait} calls
         * conditioned on {@code this.isAlive}. As a thread terminates the
         * {@code this.notifyAll} method is invoked. It is recommended that
         * applications not use {@code wait}, {@code notify}, or
         * {@code notifyAll} on {@code Thread} instances.
         */
    public final synchronized void join(long millis)
      throws InterruptedException {
      long base = System.currentTimeMillis();
      long now = 0;
    
      if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
      }
    
      if (millis == 0) {
        //如果当前Thread对象关联的线程还是存活的,当前正在执行的线程进入 Waitting状态,如果当前Thread对象关联的线程执行结束,会调用notifyAll() 唤醒进入 Waitting状态的线程。
        while (isAlive()) {
          wait(0);
        }
      } else {
        while (isAlive()) {
          long delay = millis - now;
          if (delay <= 0) {
            break;
          }
          wait(delay);
          now = System.currentTimeMillis() - base;
        }
      }
    }
    
    //wait 属于 Object 对象方法
    public class Object{
      //线程进入 Time_Watting 或 Waiting 状态
      public final native void wait(long timeout) throws InterruptedException;
    }
    

    为了便于大家理解,我画了图(一言不合就上图),大家对照着代码和图看,上面代码主要有二个线程,主线程和 ThreadA 线程,主线程创建ThreadA并启动ThreadA线程,然后调用threadA.join() 会导致主线程阻塞,直到ThreadA 线程执行结束 isActive 变为 false,主线程恢复继续执行。

    join()

  • interrupt:线程中断,调用interrupt 方法中断一个线程,是希望给这个线程一个通知信号,会改变线程内部的一个中断标识位,线程本身并不会因为中断而改变状态(如阻塞、终止等)。调用interrupt 方法有二种情况:

    1. 如果当前线程正处于 Running 状态,interrupt( ) 只会改变中断标识位,不会真的中断正在运行的线程;
    2. 如果线程当前处于 Timed_Waiting 状态,interrupt( ) 会让线程抛出 InterruptedException。

    所以我们在编写多线程程序时,优雅关闭线程需要同时处理这二种情况,常规写法是:

    public static class ThreadInterrupt implements Runnable{
    
      @Override
      public void run() {
        //1. 非阻塞状态,通过检查中断标识位退出
        while(!Thread.currentThread().isInterrupted()){
          try{
            //doSomething()
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            //2. 阻塞状态,捕获中断异常,break 退出
            e.printStackTrace();
            break;
          }
        }
      }
    }
    
  • notify:notify方法和wait方法一样,也是Object 类中的方法,notify方法用于唤醒在此对象监视器上等待的单个线程,如果有多个线程在此对象监视器上等待,选择其中一个进行唤醒。另外要注意一点的是,当前线程唤醒等待线程后不会立即释放锁,而是当前线程执行结束才会释放锁,因此被唤醒的线程不是说唤醒之后立即就可以开始执行,而是要等到唤醒的线程执行结束,获得对象锁之后开始执行。上代码吧。

    public static void main(String[] args) {
      new Thread(new ThreadA()).start();
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      new Thread(new ThreadB()).start();
    }
    
    private static final Object lock = new Object();
    
    private static class ThreadA implements Runnable{
      @Override
      public void run() {
        synchronized (lock){
          System.out.println("Thread-A 进入状态 running...");
    
          try {
            System.out.println("Thread-A 进入状态 waiting...");
            lock.wait();
    
            System.out.println("Thread-A 进入状态 running...");
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
        System.out.println("Thread-A 执行完毕, 进入状态 terminated...");
      }
    }
    
    private static class ThreadB implements Runnable{
    
      @Override
      public void run() {
        synchronized (lock){
          System.out.println("Thread-B 进入状态 running...");
          try {
            System.out.println("Thread-B 进入状态 time_waiting...");
            Thread.sleep(3000);
    
            System.out.println("Thread-B 进入状态 running...");
    
            lock.notify();
            System.out.println("Thread-B 进入状态 time_waiting...");
            Thread.sleep(5000);
            System.out.println("Thread-B 进入状态 running...");
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
        System.out.println("Thread-B 执行完毕, 进入状态 terminated...");
      }
    }
    

    控制台输出:

    Thread-A 进入状态 running...
    Thread-A 进入状态 waiting...
    Thread-B 进入状态 running...
    Thread-B 进入状态 time_waiting...
    Thread-B 进入状态 running...
    Thread-B 进入状态 time_waiting...
    Thread-B 进入状态 running...
    Thread-B 执行完毕, 进入状态 terminated...
    Thread-A 进入状态 running...
    Thread-A 执行完毕, 进入状态 terminated...
    

    可以看到B 线程调用 lock.notify() 之后A 线程没有立即开始执行,而是等到B 线程执行结束后才开始执行,所以lock.notify() 唤醒 A 线程只是让 A 线程进入预备执行的状态,而不是直接进 Running 状态,B 线程调 notify 没有立即释放对象锁。

    鉴于篇幅原因,此篇也是基础篇,知识部分就到此为止,接下来是一些常规的线程面试题。

第一题:关闭线程的方式有哪几种?哪种方式最可取?(美团一面面试题)
  1. 使用退出标识位;

    public class ThreadSafe extends Thread { 
      public volatile boolean exit = false;
      public void run() { 
        while (!exit){
      		//do something 
        }
      }
    }
    
  2. 调用 interrupt 方法,这种是最可取的,但是要考虑到处理二种情况;

  3. stop 方法,这种属于强行终止,非常危险。就像直接给线程断电,调用thread.stop() 方法时,会释放子线程持有的所有锁,这种突然的释放可能会导致数据不一致,因此不推荐使用这种方式终止线程。

第二题:很多面试会问wait 和sleep 的区别?(比心一面面试题)

主要有以下3点:

  1. sleep 方法让线程进入 Timed_Waiting 状态,sleep 方法必须传入时间参数,会让当前线程挂起一段时间,过了这个时间会恢复到runnable 状态(取决于系统计时器和调度程序的精度和准确性)。而wait 方法会让当前线程进入Waiting 状态,会一直阻塞,直到别的线程调用 notify 或者 notifyAll 方法唤醒。
  2. wait 是Object 类中的方法,sleep 是Thread 类中的方法,理解这点很重要,wait方法跟对象绑定的,调用wait方法会释放wait 关联的对象锁;
  3. 如果在同步代码块,当前线程持有锁,执行到wait 方法会释放对象锁,sleep 只是单纯休眠,不会释放锁;

我们看个代码巩固一下:

public static void main(String[] args) {
  new Thread(new ThreadA()).start();
  try {
    Thread.sleep(1000);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  new Thread(new ThreadB()).start();
}

private static final Object lock = new Object();

private static class ThreadA implements Runnable{
  @Override
  public void run() {
    synchronized (lock){
      System.out.println("Thread-A 进入状态 running...");

      try {
        System.out.println("Thread-A 进入状态 waiting...");
        lock.wait();

        System.out.println("Thread-A 进入状态 running...");
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    System.out.println("Thread-A 执行完毕, 进入状态 terminated...");
  }
}

private static class ThreadB implements Runnable{

  @Override
  public void run() {
    synchronized (lock){
      System.out.println("Thread-B 进入状态 running...");
      try {
        System.out.println("Thread-B 进入状态 time_waiting...");
        Thread.sleep(3000);

        System.out.println("Thread-B 进入状态 running...");

        lock.notify();
        System.out.println("Thread-B 进入状态 time_waiting...");
        Thread.sleep(5000);
        System.out.println("Thread-B 进入状态 running...");
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    System.out.println("Thread-B 执行完毕, 进入状态 terminated...");
  }
}

这里我建议大家先不急着看控制台输出,根据自己经验猜测一下输出应该是怎样的,然后对比输出,这样对比能看是否有偏差。另外我建议大家有条件,把本篇文章的示例程序拷贝到本地,实际看下运行。

控制台输出如下:

Thread-A 进入状态 running...
Thread-A 进入状态 waiting...
Thread-B 进入状态 running...
Thread-B 进入状态 time_waiting...
Thread-B 进入状态 running...
Thread-B 进入状态 time_waiting...
Thread-B 进入状态 running...
Thread-B 执行完毕, 进入状态 terminated...
Thread-A 进入状态 running...
Thread-A 执行完毕, 进入状态 terminated...
第三题:手写一个死锁的例子?(美团二面面试题)
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

public static class DeadLockSample implements Runnable{
  Object[] locks;

  public DeadLockSample(Object lock1, Object lock2){
    locks = new Object[2];
    locks[0] = lock1;
    locks[1] = lock2;
  }

  @Override
  public void run() {
    synchronized (lock1) {
      try {
        Thread.sleep(3000);
        synchronized (lock2) {
          System.out.println(String.format("%s come in...", Thread.currentThread().getName()));
        }
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

public static void main(String[] args) {
  Thread a = new Thread(new DeadLockSample(lock1, lock2));
  Thread b = new Thread(new DeadLockSample(lock2, lock1));

  a.start();
  b.start();
}
第四题:写一个通过线程wait / notify通信的生产者消费者代码?(声网四面面试题)
static class MangoIce{
        int counter;

        public MangoIce(int counter) {
            this.counter = counter;
        }
    }

    static class Producer implements Runnable
    {
        private final List<MangoIce> barCounter;
        private final int           MAX_CAPACITY;

        public Producer(List<MangoIce> sharedQueue, int size)
        {
            this.barCounter = sharedQueue;
            this.MAX_CAPACITY = size;
        }

        @Override
        public void run()
        {
            int counter = 1;
            while (!Thread.currentThread().isInterrupted())
            {
                try
                {
                    produce(counter++);
                }
                catch (InterruptedException ex)
                {
                    ex.printStackTrace();
                    break;
                }
            }
        }

        private void produce(int i) throws InterruptedException
        {
            synchronized (barCounter)
            {
                while (barCounter.size() == MAX_CAPACITY)
                {
                    System.out.println("吧台满了,冰沙放不下 " + Thread.currentThread().getName() + " 线程等待,当前吧台冰沙数: " + barCounter.size());
                    barCounter.wait();
                }

                Thread.sleep(1000);
                barCounter.add(new MangoIce(i));
                System.out.println("生产第: " + i + "杯冰沙...");
                barCounter.notifyAll();
            }
        }
    }

    static class Consumer implements Runnable
    {
        private final List<MangoIce> barCounter;

        public Consumer(List<MangoIce> sharedQueue)
        {
            this.barCounter = sharedQueue;
        }

        @Override
        public void run()
        {
            while (!Thread.currentThread().isInterrupted())
            {
                try
                {
                    consume();
                } catch (InterruptedException ex)
                {
                    ex.printStackTrace();
                    break;
                }
            }
        }

        private void consume() throws InterruptedException
        {
            synchronized (barCounter)
            {
                while (barCounter.isEmpty())
                {
                    System.out.println("吧台空的,没有冰沙 " + Thread.currentThread().getName() + " 消费者线程等待,当前吧台冰沙数: " + barCounter.size());
                    barCounter.wait();
                }
                Thread.sleep(1000);
                MangoIce i = barCounter.remove(0);
                System.out.println("消费第: " + i.counter + "杯冰沙...");
                barCounter.notifyAll();
            }
        }
    }

    public static void main(String[] args)
    {
        List<MangoIce> taskQueue = new ArrayList<>();
        int MAX_CAPACITY = 5;
        Thread tProducer = new Thread(new Producer(taskQueue, MAX_CAPACITY), "生产者");
        Thread tConsumer = new Thread(new Consumer(taskQueue), "消费者");
        tProducer.start();
        tConsumer.start();
    }

控制台输出

生产第: 1杯冰沙...
生产第: 2杯冰沙...
生产第: 3杯冰沙...
生产第: 4杯冰沙...
生产第: 5杯冰沙...
吧台满了,冰沙放不下 生产者 线程等待,当前吧台冰沙数: 5
消费第: 1杯冰沙...
消费第: 2杯冰沙...
消费第: 3杯冰沙...
消费第: 4杯冰沙...
消费第: 5杯冰沙...
吧台空的,没有冰沙 消费者 消费者线程等待,当前吧台冰沙数: 0
生产第: 6杯冰沙...
生产第: 7杯冰沙...
生产第: 8杯冰沙...
生产第: 9杯冰沙...
生产第: 10杯冰沙...
吧台满了,冰沙放不下 生产者 线程等待,当前吧台冰沙数: 5
消费第: 6杯冰沙...
消费第: 7杯冰沙...

后面几期会分别讲以下内容,顺序还没定,大概会按照读者群的反馈来。

  • 线程上下文切换、JAVA锁
  • 线程池实战、Fork / Join
  • 并发工具 CyclicBarrier、CountDownLatch、Semaphore的实际使用场景
  • synchronized、volatile、Atomic*** 涉及的原子性、内存可见性和指令重排序原理
  • AQS、ReentrantLock原理、以及和synchronized区别、CAS原理
  • 阻塞队列、线程调度
    欢迎关注Wx 公众号【安琪拉的博客】查看后续内容更新

参考:How to work with wait(), notify() and notifyAll() in Java?

原创文章 18 获赞 769 访问量 8万+

猜你喜欢

转载自blog.csdn.net/zhengwangzw/article/details/105625514
今日推荐