高并发编程从入门到精通(三)

面试中最常被虐的地方必定有并发编程这块知识点,不管你是刚刚入门的大四萌新仍是2-3年经验的CRUD怪,也就是说这类问题你最起码会被问3年,何不花时间死磕到底。消除恐惧最好的办法就是面对他,奥利给!(这一系列是本人学习过程当中的笔记和总结,并提供调试代码供你们玩耍java

上章回顾

1.java线程生命周期和java线程状态都有哪些?git

2.java线程生命周期之间是如何转换的?github

3.Thread.start()都作了哪些事情?面试

请自行回顾以上问题,若是还有疑问的自行回顾上一章哦~编程

本章提要

本章学习完成,你将会掌握Thread经常使用API接口的使用,包括sleep、yield和join,而且会详细解析join源码和用法。同时配合上一章的start()方法,本章还会介绍一下应该如何去关闭一个线程。鉴于interrupt字段内容较多,咱们放到下一章讲哦。(老规矩,熟悉这块的同窗能够选择直接关注点赞👍完成本章学习哦!)bash

本章代码下载并发

1、Thread经常使用API详解

本节开头先打个预防针,针对每个API会用和精通是两个水准哦,这里咱们的目标是彻底吃透,因此章节内容会比较干,可是我会加油写的有代入感,你们一块儿加油~👏app


(1) sleep

sleep一共有两个重载方法ide

  • public static native void sleep(long millis) throws InterruptedException
  • public static void sleep(long millis, int nanos) throws InterruptedException

因为这两个实现精度不一样,内部调用的都是同一个方法,因此咱们这里就挑public static void sleep(long millis, int nanos) throws InterruptedException来看下函数

/**
     * Causes the currently executing thread to sleep (temporarily cease
     * execution) for the specified number of milliseconds plus the specified
     * number of nanoseconds, subject to the precision and accuracy of system
     * timers and schedulers. The thread does not lose ownership of any
     * monitors.
     *
     * @param  millis
     *         the length of time to sleep in milliseconds
     *
     * @param  nanos
     *         {@code 0-999999} additional nanoseconds to sleep
     *
     * @throws  IllegalArgumentException
     *          if the value of {@code millis} is negative, or the value of
     *          {@code nanos} is not in the range {@code 0-999999}
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */
    public static void sleep(long millis, int nanos)
    throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }

        sleep(millis);
    }
复制代码

官方的描述是这样的,使线程暂时中止执行,在指定的毫秒数上再加上指定的纳秒数,可是线程不会失去监视器。这里的关键是不会失去持有的监视器,上一章咱们讲过这时线程处于BLOCKED阶段。

if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
复制代码

当millis<0或者nanos不在0-999999范围中的时候就会抛出IllegalArgumentException

@throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
复制代码

sleep可被中断方法打断,可是会抛出InterruptedException异常。

好啦到这里咱们介绍完了这个API了,是否是感受很简单呢?哈哈光这样可不行,实践是检验真理的惟一标准下面咱们来验证一下sleep以后对象监视锁到底有没有释放。

别犯困啦,划重点啦

/**
   * 建立一个独占锁
   */
  private static final Lock lock = new ReentrantLock();

  public static void main(String[] args) {

    new Thread(new Runnable() {
      @Override
      public void run() {
        lock.lock();
        System.out.println("我是" + Thread.currentThread().getName() + ",lock在我手中");
        try {
          Thread.sleep(3000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          lock.unlock();
          System.out.println(Thread.currentThread().getName() + "不须要lock了");
        }

      }
    }, "一号线程").start();

    new Thread(new Runnable() {
      @Override
      public void run() {
        lock.lock();
        System.out.println("我是" + Thread.currentThread().getName() + ",lock在我手中");
        try {
          Thread.sleep(3000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          lock.unlock();
          System.out.println(Thread.currentThread().getName() + "不须要lock了");
        }

      }
    }, "二号线程").start();

  }
复制代码

输出结果:

我是一号线程,lock在我手中
一号线程不须要lock了
我是二号线程,lock在我手中
二号线程不须要lock了
复制代码

同窗们能够用各类姿式来run咱们的代码,不关你是坐着run,躺着run仍是倒立run,结果始终是连续的,不会出现一号线程和二号线程交替打印的情景。这就证实了sleep确实不会释放其获取的监视锁,可是他会放弃CPU执行权。实践也实践完了,可是每次都要计算毫秒也着实费劲,有没有什么好的办法呢?


⚠️会玩的都这么写

假如如今有需求要求咱们让线程sleep1小时28分19秒33毫秒咱们要咋办?手脚快的同窗可能已经掏出了祖传的计算器滴滴滴地操做起来了。可是咱们通常不这么作,JDK1.5为咱们新增了一个TimeUnit枚举类,请你们收起心爱的计算器,其实咱们能够这么写

//使用TimeUnit枚举类
          TimeUnit.HOURS.sleep(1);
          TimeUnit.MINUTES.sleep(28);
          TimeUnit.SECONDS.sleep(19);
          TimeUnit.MILLISECONDS.sleep(33);
复制代码

这样咱们的代码更加优雅,可读性会更强

写累了,锻炼下身体,给同窗们挖个坑。咱们已经知道millis的范围是大于等于0,sleep(1000)咱们知道是什么意思,那么sleep(0)会有做用吗?

答案是会起做用的,这是咱们须要记住的关于 sleep的第二个点, sleep(0)的做用是“触发操做系统马上从新进行一次CPU竞争,竞争结果多是当前线程继续获取到CPU的执行权,也有多是别的线程获取到了当前线程的执行权。

两个点但愿你们能够牢记

1.sleep不会释放mointor lock。

2.sleep的做用是触发操做系统马上从新进行一次CPU竞争。

(2) yield

仍是老套路,咱们先来看API接口描述是怎么定义这个接口的

/**
     * A hint to the scheduler that the current thread is willing to yield
     * its current use of a processor. The scheduler is free to ignore this
     * hint.
     *
     * <p> Yield is a heuristic attempt to improve relative progression
     * between threads that would otherwise over-utilise a CPU. Its use
     * should be combined with detailed profiling and benchmarking to
     * ensure that it actually has the desired effect.
     *
     * <p> It is rarely appropriate to use this method. It may be useful
     * for debugging or testing purposes, where it may help to reproduce
     * bugs due to race conditions. It may also be useful when designing
     * concurrency control constructs such as the ones in the
     * {@link java.util.concurrent.locks} package.
     */
    public static native void yield();
复制代码

这个方法的描述是这样的提示调度程序,当前线程愿意放弃CPU执行权。调度程序能够无条件忽略这个提示,打个比方就是说,A暗恋B,A说我愿意怎么怎么样,B能够接受A,可是也能够彻底无条件的忽略A,,嗯嗯额~大概就是这么个场景,卑微A。

API接口描述中也明确说明了,这个接口不经常使用,可能用于调试或测试的目的,可能用于重现因为竞争条件而致使的bug,还有就是在java.util.concurrent.locks包中有用到这个API,总的来讲就是在实际生产开发过程当中是不用的。可是它又不像stop同样已经被废弃不推荐使用,讲这个API的目的是应由于它很容易和sleep混淆。

1.调用yield并生效以后线程会从RUNNING阶段转变为RUNNABLE,固然被无条件忽略的状况除外。而sleep则是进入BLOCKED阶段,并且是几乎百分百会进入。

2.sleep会致使线程暂停,可是不会消耗CPU时间片,yield一旦生效就会发生线程上下文切换,会带来必定的开销。

3.sleep能够被另外一个线程调用interrupt中断,而yield就不会,yield得等到CPU轮询给到执行权的时候才会再次被唤醒,也就是从RUNNABLE阶段编程RUNNING阶段。

光说不练假把式,虽然不经常使用,可是是驴子是马总归仍是要溜一溜。

private static class MyYield implements Runnable {

    @Override
    public void run() {
      for (int i = 0; i < 5; i++) {
        if (i % 5 == 0) {
          System.out.println(Thread.currentThread().getName()+"线程,yield 它出现了");
//          Thread.yield();

        }
      }

      System.out.println(Thread.currentThread().getName()+"结束了");
    }
  }

  public static void main(String[] args) {
    Thread t1 = new Thread(new MyYield());
    t1.start();

    Thread t2 = new Thread(new MyYield());
    t2.start();

    Thread t3 = new Thread(new MyYield());
    t3.start();
  }
复制代码

屡次执行,按到最多的输出是连续的,相似下面这种输出结果:

Thread-0线程,yield 它出现了
Thread-0结束了
Thread-1线程,yield 它出现了
Thread-1结束了
Thread-2线程,yield 它出现了
Thread-2结束了
复制代码

如今咱们把注释打开,发现输出结果变了

Thread-0线程,yield 它出现了
Thread-1线程,yield 它出现了
Thread-2线程,yield 它出现了
Thread-0结束了
Thread-1结束了
Thread-2结束了
复制代码

那是应为在调用到yield到时候当前线程让出了执行权,因此等到你们都出现了以后,你们再分别结束了

(3) join

在本小节中咱们介绍一下joinAPI

  • public final void join() throws InterruptedException
  • public final synchronized void join(long millis)
  • public final synchronized void join(long millis, int nanos) 和sleepAPI十分相像,可是join除了两个设置超时等待时间的API外,还额外提供了一个不设置超时时间的方法,可是经过追踪第一个API咱们发现内部其实调用的就是第二个API的join(0),设置纳秒的内部调用也是第二个API。因此咱们这边就拿第二个API来说解。
/**
    //设置一段时间等待当前线程结束,若是超时还未返回就会一直等待
     * Waits at most {@code millis} milliseconds for this thread to
     * die. A timeout of {@code 0} means to wait forever.
     *
     这个方法调用的前提就是当前线程仍是处于alive状态的
     * <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.
     *
     * @param  millis
     *         the time to wait in milliseconds
     *
     //超时时间为负数
     * @throws  IllegalArgumentException
     *          if the value of {@code millis} is negative
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */
    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) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
复制代码

经过该方法咱们能够看到他的逻辑是经过当前运行机器的时间,判断线程是否isAlive来决定是否须要继续等待,而且内部咱们能够看到调用的是wait()方法,直到delay<=0的时刻,就会跳出当前循环,从而结束中断。

又要给同窗们讲一个悲伤的故事了

public static void main(String[] args) throws InterruptedException {

    Thread t1 = new Thread(() -> {
      System.out.println("周末都要加班,终于回家了,洗个手吃饭了");
      try {
        TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.print("洗完手,");
    });

    Thread t2 = new Thread(() -> {
      try {
        TimeUnit.SECONDS.sleep(2);
//        t1.join();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.print("拿起筷子,");
    });

    t1.start();
    t2.start();
//    t2.join();
    System.out.println("我要吃饭了");

  }
复制代码

输出:

周末都要加班,终于回家了,洗个手吃饭了
我要吃饭了
拿起筷子,洗完手,
复制代码

显然这个结果不是咱们想要的结果,也可是不排除加班加的已经意识模糊,手抓饭了,这里咱们仍是但愿按照正常习惯来执行。咱们把注释打开

输出:

周末都要加班,终于回家了,洗个手吃饭了
洗完手,拿起筷子,我要吃饭了
复制代码

这个才是咱们须要的结果。

相信同窗们经过这个例子已经大概了解join的做用了,没错join是可让程序能按照必定的次序来完成咱们想完成的工做,他的工做原理就是阻塞当前调用join的线程,让新join进来的线程优先执行。

2、线程该如何关闭

线程关闭大体上能够分为三种状况

1.线程正常关闭

2.线程异常退出

3.进程假死

这里咱们着重讲一下线程正常关闭的状况,也是实际开发生产中经常使用方法。

1.线程生命周期正常结束

这个没什么好说的,就是线程逻辑单元执行完成而后本身正常结束。

2.捕获中断信号关闭线程。

早期JDK中还提供有一个stop函数用于关闭销毁线程,可是后来发现会存在monitor锁没法释放的问题,会致使死锁,因此如今强烈建议你们不要用这个方式。这里咱们使用捕获线程中断的方式来结束线程。

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      System.out.println("我要自测代码啦~~");
      while (!Thread.currentThread().isInterrupted()) {
        System.out.println("目前来看是好好的");
      }
      System.out.println("代码中断中止了");
    });

    t1.start();
    TimeUnit.SECONDS.sleep(1);
    t1.interrupt();

  }
复制代码

输出:

我要自测代码啦~~
...
目前来看是好好的
目前来看是好好的
代码中断中止了
复制代码

能够看到,咱们经过判断当前线程的isInterrupted()状态来捕获线程是否已经被中断,从而能够来控制线程正常关闭。同理,若是咱们在线程内部已经执行来某中断方法,好比sleep就能够经过捕获中断异常来退出sleep状态,从而也能让线程正常结束。

3.设置开关关闭

因为interrupt颇有可能被擦除,或者整个逻辑单元中并有调用中断方法,这样咱们上一种方法就不适用了,这里咱们使用volatile关键字来设置一个开关,控制线程的正常退出。

private static class MyInterrupted extends Thread {

    private volatile boolean close = false;

    @Override
    public void run() {
      System.out.println("我要开始自测代码啦~~");
      while (!close) {
        System.out.println("目前来看好好的");
      }
      System.out.println("close已经变成了" + close + ",代码正常关闭了");
    }

    public void closed() {
      this.close = true;
    }
  }

  public static void main(String[] args) throws InterruptedException {
    MyInterrupted myInterrupted = new MyInterrupted();
    myInterrupted.start();
    TimeUnit.SECONDS.sleep(1);
    System.out.println("我要开始关闭线程了");
    myInterrupted.closed();
  }
复制代码

输出:

我要开始自测代码啦~~
...
目前来看好好的
目前来看好好的
目前来看好好的
我要开始关闭线程了
close已经变成了true,代码正常关闭了
复制代码

能够看到咱们调用closed方法时候把close设置为了true,从而正常关闭代码,关于volatile关键字咱们以后的章节会详细讲哦,请同窗们继续关注,和我一块儿学习😁,但愿能够同窗们帮忙关注下和点点赞👍哦~

下一章也已经出来咯,此次内容稍多,因此拖了比较久,可是内容都是妥妥的,下一章详细讲解了interrupt的执行逻辑,一步一步带同窗们调试,感兴趣的同窗记得进入下一章的学习哦~

相关文章
相关标签/搜索