美文网首页Java
java并发之线程协作

java并发之线程协作

作者: 安仔夏天勤奋 | 来源:发表于2019-03-29 16:30 被阅读4次

在多线程开发中,常常在线程间进行切换或调度,那么就会出现线程协作。线程协作有几种方式如下:

  1. 阻塞/唤醒
  2. 让步
  3. 取消(中断)

先上一个简图吧

阻塞/唤醒

  • suspend()/resume()(已过时,建议不用

    在早期java中采用suspend()、resume()对线程进行阻塞与唤醒,但这种方式产生死锁的风险很大,因为线程被挂起以后不会释放锁,可能与其他线程、主线程产生死锁。

  • wait()/notify()

    wait、notify形式通过一个object作为信号,wait和notify是Object的实例方法,和线程安全、线程协作都有关系,线程安全指wait和notify方法只能在synchronized临界区内调用,wait使当前线程释放cpu和锁进入等待阻塞,notify/notifyAll唤醒因调用当前对象wait方法进入等待阻塞的线程。notify()准许阻塞的一个线程通过,notifyAll()允许所有线程通过。

      /**
       * @Author 安仔夏天勤奋
       * Create Date is  2019/3/29
       * Des  Java程序演示如何在线程中使用notify()和notifyAll()
       * notify()和notifyAll()如何通知线程,哪个线程被唤醒等。
       */
      public class TestDiff {
    
      private volatile boolean isWait = false;
      private final static TestDiff test = new TestDiff();
    
      public static void main(String args[]) throws InterruptedException {
    
          WaitThread waitTask = new WaitThread();
          NotifyThread notifyTask = new NotifyThread();
    
          Thread t1 = new Thread(waitTask, "wait_1"); 
          Thread t2 = new Thread(waitTask, "wait_2"); 
          Thread t3 = new Thread(waitTask, "wait_3");
          Thread t4 = new Thread(notifyTask,"notify_1");
    
          t1.start();
          t2.start();
          t3.start();
    
          Thread.sleep(200);
          t4.start();
    
      }
    
      //线程等待
       static class WaitThread implements Runnable{
          @Override
          public void run() {
              try {
                  test.shouldWait();
              } catch (InterruptedException ex) {
                  System.out.println(Thread.currentThread() + " 等待 异常 InterruptedException");
              }
              System.out.println(Thread.currentThread() + " 等待 finished Execution");
          }
      }
    
      //通知唤醒
      static  class NotifyThread implements Runnable{
          @Override
          public void run() {
              test.wokeUp();
              System.out.println(Thread.currentThread() + " 唤醒 finished Execution");
          }
      }
    
      /*
       * wait、notify只能synchronized方法或锁中调用
       */
      private synchronized void shouldWait() throws InterruptedException {
          while(isWait != true){
              System.out.println(Thread.currentThread()+ " 正在等待Object对象");
              wait(); //释放锁和获取唤醒
              System.out.println(Thread.currentThread() + " 被唤醒");
          }
          isWait = false;
      }
    
      /**
       * wait、notify只能synchronized方法或锁中调用
       * 线程都被锁定在“this”关键字引用的当前对象上 ,notify()或notifyAll()唤醒线程
       */
      private synchronized void wokeUp() {
          while (isWait == false){
              System.out.println(Thread.currentThread()+ " 将通知所有或一个线程等待这个对象");
              isWait = true; //使等待线程的条件为真
              notify(); //等待线程wait_1, wait_2,wait_3只能有一个被唤醒
      //            notifyAll(); // 等待线程wait_1, wait_2,wait_3 都被唤醒
              }
          }
      }
    

    结果打印

    调用notify()的打印结果
    Thread[wait_1,5,main] 正在等待Object对象
    Thread[wait_2,5,main] 正在等待Object对象
    Thread[wait_3,5,main] 正在等待Object对象
    Thread[notify_1,5,main] 将通知所有或一个线程等待这个对象
    Thread[wait_1,5,main] 被唤醒
    Thread[notify_1,5,main] 唤醒 finished Execution
    Thread[wait_1,5,main] 等待 finished Execution
    
    调用notifyAll()的打印结果
    Thread[wait_1,5,main] 正在等待Object对象
    Thread[wait_2,5,main] 正在等待Object对象
    Thread[wait_3,5,main] 正在等待Object对象
    Thread[notify_1,5,main] 将通知所有或一个线程等待这个对象
    Thread[wait_3,5,main] 被唤醒
    Thread[wait_2,5,main] 被唤醒
    Thread[wait_3,5,main] 等待 finished Execution
    Thread[wait_2,5,main] 正在等待Object对象
    Thread[wait_1,5,main] 被唤醒
    Thread[wait_1,5,main] 正在等待Object对象
    Thread[notify_1,5,main] 唤醒 finished Execution
    

    notify()与notifyAll()这两个方法区别
    Java提供了两个方法notify和notifyAll来唤醒在某些条件下等待的线程,你可以使用它们中的任何一个,但是Java中的notify和notifyAll之间存在细微差别,从上面的例子打印出的结果更能他们两者的差别。这使得它成为Java中流行的多线程面试问题之一。当你调用notify时,只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。虽然如果你调用notifyAll方法,那么等待该锁的所有线程都会被唤醒,但是在执行剩余的代码之前,所有被唤醒的线程都将争夺锁定,这就是为什么在循环上调用wait,因为如果多个线程被唤醒,那么线程是将获得锁定将首先执行,它可能会重置等待条件,这将迫使后续线程等待。因此,notify和notifyAll之间的关键区别在于notify()只会唤醒一个线程,而notifyAll方法将唤醒所有线程。

  • await()/singal()

    Condition类提供,而Condition对象由new ReentLock().newCondition()获得,与wait和notify相同,因为使用Lock锁后无法使用wait方法。

  • park()/unpark()

    LockSupport提供的park和unpark方法,提供避免死锁和竞态条件,很好地代替suspend和resume组合。park与unpark方法控制的颗粒度更加细小,能准确决定线程在某个点停止,进而避免死锁的产生。

      /**
       * @Author 安仔夏天勤奋
       * Create Date is  2019/3/29
       */
       public class TestThread {
        public static void main(String []arg){
       
          XThread xThread = new XThread();
          xThread.setName("AN");
          xThread.start();
          try {
              Thread.sleep(10);
              xThread.park();
              Thread.sleep(10000);
              xThread.unPark();
              Thread.sleep(10000);
              xThread.park();
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
      private static class XThread extends Thread{
          private boolean isPark = false;
          @Override
          public void run() {
              System.out.println("我正在运行。。。");
              while (true) {
                  if (isPark) {
                      System.out.println("Thread is Park.....");
                      LockSupport.park();
                  }
              }
          }
          public void park() {
              isPark = true;
          }
          public void unPark() {
              isPark = false;
              LockSupport.unpark(this);
              System.out.println("Thread is unpark.....");
          }
      }
    }
    

    park与unpark组合真正解耦了线程之间的同步,不再需要另外的对象变量存储状态,并且也不需要考虑同步锁。

让步 join()/yield()

  • join:线程联合

    JDK文档的定义:public final void join()throws InterruptedException: Waits for this thread to die.

    join为Thread的实例方法,用于线程联合,在线程thread1中调用线程thread2.join()方法,thread1会让出CPU使用进入阻塞直到thread2线程执行结束;如果调用thread2.join()指定时间参数,则表示thread1的最多等待时间。也可以这样融解:t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,此线程再继续;通常用于在main()主线程内,等待其它线程完成再结束main()主线程。

    下面输出不调用join()的一段代码

      /**
       * @Author 安仔夏天勤奋
       * Create Date is  2019/3/29
       */
    public static void main(String[] args){
      System.out.println("MainThread run start.");
      //启动一个子线程
      Thread threadA = new Thread(new Runnable() {
          @Override
          public void run() {
              System.out.println("threadA run start.");
              try {
                  Thread.sleep(1000);
              } catch (Exception e) {
                  e.printStackTrace();
              }
              System.out.println("threadA run finished.");
          }
      });
      threadA.start();
      System.out.println("MainThread join before.");
      System.out.println("MainThread run finished.");
    }
    

    打倒输出结果

    MainThread run start. 
    threadA run start. 
    MainThread join before .
    MainThread run finished. 
    threadA run finished.
    

    下面输出调用join()的一段代码

    /**
       * @Author 安仔夏天勤奋
       * Create Date is  2019/3/29
       */
    public static void main(String[] args){
      System.out.println("MainThread run start.");
      //启动一个子线程
      Thread threadA = new Thread(new Runnable() {
          @Override
          public void run() {
              System.out.println("threadA run start.");
              try {
                  Thread.sleep(1000);
              } catch (Exception e) {
                  e.printStackTrace();
              }
              System.out.println("threadA run finished.");
          }
      });
      threadA.start();
      System.out.println("MainThread join before.");
      try {
          threadA.join();    //调用join()
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      System.out.println("MainThread run finished.");
    }
    

    打倒输出结果

    MainThread run start. 
    threadA run start. 
    MainThread join before. 
    threadA run finished. 
    MainThread run finished.
    

    注意:如果join和synchronized同时使用时需要注意,线程thread1中调用thread2.join方法,thread1只让出CPU使用但不释放锁,这很容易死锁。

  • yield:线程切换
    yield为Thread静态方法,线程放弃继续执行,让出cpu释放锁进入可运行状态。理论上,yield意味着放手,放弃,投降。一个调用yield()方法的线程告诉虚拟机它乐意让其他线程占用自己的位置。这表明该线程没有在做一些紧急的事情。注意,这仅是一个暗示,并不能保证不会产生任何影响。

    Thread.java中yield()源码定义

    /**
      * 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. Yield is a heuristic attempt to improve relative progression between threads that would 
    otherwise over-utilize a CPU.
      * Its use should be combined with detailed profiling and benchmarking to ensure that it actually has the 
    desired effect.
      */
    public static native void yield();
    

    随意的创建了名为生产者和消费者的两个线程。生产者设定为最小优先级,消费者设定为最高优先级。在Thread.yield()注释和非注释的情况下我将分别运行该程序。没有调用yield()方法时,虽然输出有时改变,但是通常消费者行先打印出来,然后事生产者。
    上代码

      /**
       * @Author 安仔夏天勤奋
       * Create Date is  2019/3/29
       */
    public class TestYieldExample{
     public static void main(String[] args){
        Thread producer = new Producer();
        Thread consumer = new Consumer();
        producer.setPriority(Thread.MIN_PRIORITY); //Min Priority
        consumer.setPriority(Thread.MAX_PRIORITY); //Max Priority
        producer.start();
        consumer.start();
     }
    }
    
    class Producer extends Thread{
     public void run(){
        for (int i = 0; i < 5; i++){
           System.out.println("I am Producer : Produced Item " + i);
           Thread.yield();
        }
     }
    }
    
    class Consumer extends Thread{
     public void run(){
        for (int i = 0; i < 5; i++){
           System.out.println("I am Consumer : Consumed Item " + i);
           Thread.yield();
        }
     }
    }
    

    打倒输出结果

    没有调用yield()方法情况下的输出
     I am Consumer : Consumed Item 0
     I am Consumer : Consumed Item 1
     I am Consumer : Consumed Item 2
     I am Consumer : Consumed Item 3
     I am Consumer : Consumed Item 4
     I am Producer : Produced Item 0
     I am Producer : Produced Item 1
     I am Producer : Produced Item 2
     I am Producer : Produced Item 3
     I am Producer : Produced Item 4
    
    在调用yield()方法情况下的输出
     I am Producer : Produced Item 0
     I am Consumer : Consumed Item 0
     I am Producer : Produced Item 1
     I am Consumer : Consumed Item 1
     I am Producer : Produced Item 2
     I am Consumer : Consumed Item 2
     I am Producer : Produced Item 3
     I am Consumer : Consumed Item 3
     I am Producer : Produced Item 4
     I am Consumer : Consumed Item 4
    

    注意:执行yield只是让线程暂停一下,让系统重新调度,大多数情况,yield后系统会继续选择当前线程执行(所以不好验证)

取消(中断)

每个线程都有一个标志位,标志当前线程是否中断,Thread类中有获取当前线程中断状态及设置当前线程为中断状态的方法:

  • interrupted:Thread的类方法,获取当前线程的中断状态,并重置当前线程为非中断状态。
  • isInterrupted:Thread的实例方法,获取当前线程的中断状态。
  • interrupt:Thread的实例方法,设置当前线程为中断状态;(只是单纯的设置线程的中断标志,至于线程中断后做什么在线程驱动的任务中可以通过捕获异常或获取中断状态后自己定义)。
 /**
   * @Author 安仔夏天勤奋
   * Create Date is  2019/3/29
   */
public class TestInterrupt{
  public class InterruptRunnableDemo extends Thread {
    @Override
    //这种设计比较好,当调用阻塞操作时,会因为抛出异常退出,当不调用阻塞操作时,会因为检查中断状态而退出
    public void run(){
        try{
            while(!Thread.interrupted()){
               // 循环代码              
            }
            System.out.println("Exit normal");
        }catch(Exception e){
            System.out.println("interrupted");
        }
    }
  public static void main(String[] args) throws InterruptedException {
            Thread thread = new InterruptRunnableDemo();
            thread.start();
            Thread.sleep(1000);
            thread.interrupt();
  }
}

注意

  1. 如果线程因wait/sleep/join方法进入等待阻塞,或因调用Lock对象的lockInterrupity/tryLock(time)进入同步阻塞状态,其他线程中调用interrupt方法会导致阻塞线程中抛出InterruptException异常;
  2. 阻塞状态的线程抛出InterruptException时会重置中断标志(标志位false);
  3. 类方法interrupted获取中断状态后会清除中断状态,实例方法isInterrupted()只是获取中断状态;
  4. 除使用interrupt方法中断线程外,还有2种方式中止线程执行,a. 退出标志使线程正常退出(线程通信);b.使用stop()方法强行终止线程(不推荐,可能发生不可预料的结果)

相关文章

网友评论

    本文标题:java并发之线程协作

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