• 中文
    • English
  • 注册
  • 查看作者
    • 一. 并发编程线程基础

      一.  线程和进程

      1.  什么是进程

      进程是代码在数据集合上的一次运行活动,是系统除CPU外进行资源分配和调度的基本单位(CPU资源是直接分配到线程的,线程是CPU分配的基本单位),进程包括线程。

      2.  什么是线程

      线程是进程的一个实体,一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源

      3.  在Java中,启动main函数就相当于启动了一个JVM进程,main函数所在的线程称为主线程

      4.  一个进程中的线程共享该进程的堆和方法区的资源,堆是一个进程中最大的一块内存,主要存放使用new操作创建的对象实例。方法区用来存放JVM加载的类、常量、及静态变量等信息。

      5.  每个线程有自己的程序计数器和栈区域,程序计数器用于记录当前线程要执行的指令地址,栈用来存储该线程的局部变量,该局部变量无法被其他线程访问。

      二.  线程创建与运行

      创建线程一共有三种方式

      方式一:继承Thread类

      public class ThreadTest {
          public static class MyThread extends Thread{
              @Override
              public void run() {
                  System.out.println("I am a child thread");
              }
          }
      
          public static void main(String[] args) {
              //1.  创建线程
              MyThread myThread = new MyThread();
              //2.  启动线程
              myThread.start();
          }
      }

      需要注意的是,当第一步创建了Thread对象后,线程并没有被执行,直到调用了start方法,该线程才被启动执行,调用start方法后,该线程处于就绪状态(已经获得了除CPU外其他资源),当该线程获取了CPU资源后才真正处于运行状态,执行完run方法,线程处于终止状态。

      方式二:实现Runnable接口

      public class ThreadTest {
      
          public static void main(String[] args) {
              //1.  创建线程
              RunnableTask task = new RunnableTask();
              //2.  启动线程
              new Thread(task).start();
              new Thread(task).start();
          }
      
          public static class RunnableTask implements Runnable {
      
              @Override
              public void run() {
                  System.out.println("I am a child thread");
              }
          }
      }

      前两种方式任务都没有返回值,可以FutureTask的方式来获取任务的返回值

      方式三:FutureTask

      public class ThreadTest {
      
          public static void main(String[] args) {
              //1. 创建异步任务:使用FutrueTask对象作为任务,创建了一个进程
              FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
              //2. 启动线程
              new Thread(futureTask).start();
              try {
              //3. 等待任务执行完毕,并返回结果
                  String result = futureTask.get();
                  System.out.println(result);//输出:zhangjia
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
          //创建任务类,类似Runnable
          public static class CallerTask implements Callable<String> {
      
              @Override
              public String call() throws Exception {
                  return "ZhangJia";
              }
          }
      }

      三种方式的优缺点:

      • 继承Thread类

        • 优点:在run()方法中可以直接使用this获取当前线程,方便传参,可以在子类里添加成员变量,通过set方法设置参数或者通过构造方法传递

        • 缺点:java不支持多继承,任务和代码没有分离,当多个线程执行一样的任务时需要多份任务代码

      • 实现Runnable接口

        • 优点:可以继承其他类,多个线程可以共用一份代码

        • 缺点:没有返回值,只能使用主线程里面被声明为final的变量

      • FutureTask:

        • 优点:可以拿到任务的返回值

      三.  线程的通知与等待

      在讨论线程的通知与等待之前,想起之前面试的时候,面试官一直在问各种多线程相关的问题,其中有一个问题印象深刻:你用过Object类中的哪些方法?菜鸡的我只说出了equals,toString,getClass,hashCode之类常见的方法,其实面试官想要的答案是wait、notify、notifyAll,接下来我们就从这几个函数入手,来讲解一下线程的通知与等待

      1. wait()函数

      一个线程可以通过调用一个共享变量的wait()方法将自己挂起,直到发生以下事情之一才返回:

      • 其他线程调用了该共享对象的notify() 方法或者notifyAll() 方法

      • 其他线程(或者该线程自己调用interrupt)调用了该线程的interrupt()方法导致该线程抛出InterruptedException异常返回

      另外如果调用wait方法的线程事先没有获取该独享的监视器锁,那么会抛出IllegalMonitorStateException异常,可以通过下面两种方法来获取该对象的监视器锁来避免该异常

      //方法一
      synchronized (共享变量){
          //防止虚假唤醒
          while(条件不满足) {
              共享变量.wait(); 
          }
      }
      //方法二
      synchronized void add(int a, int b){
          //doSomething
      }

      需要注意的是,如果一个线程持有多个共享变量的锁,那么调用wait方法后只会释放当前共享变量的锁,举个例子来理解一下这句话:

      package io.zhangjia.threads.test.Test;
      
      import org.apache.ibatis.annotations.Param;
      
      /**
       * @Author : ZhangJia
       * @Date : 2019/12/4 21:45
       * @Description : 
       */
      public class Test2 {
          private static volatile Object a = new Object();
          //volatile关键字能够保证代码的有序性
          private static volatile Object b = new Object();
      
          public static void main(String[] args) {
              new Thread(new ThreadA()).start();
              new Thread(new ThreadB()).start();
          }
      
          public static class ThreadA implements Runnable {
      
              @Override
              public void run() {
                  try {
                      synchronized (a) {
                          System.out.println("线程A获得了共享变量a的监视器锁");
      
                          synchronized (b) {
                              System.out.println("线程A获得了共享变量b的监视器锁");
      
                              System.out.println("线程A阻塞,并释放获取到的共享变量a的锁");
                              a.wait();
      
                          }
                      }
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
      
      
          public static class ThreadB implements Runnable {
      
              @Override
              public void run() {
                  try {
                      Thread.sleep(1000);
                      synchronized (a) {
                          System.out.println("线程B获取到共享变量a的锁啦");
                          System.out.println("我是线程B,我一直在尝试获取共享变量b的锁,就是没获取到");
      
                          synchronized (b) {
                              System.out.println("线程B获取到共享变量b的锁啦");
                              System.out.println("线程B阻塞,并释放获取到的共享变量a的锁");
                              a.wait();
                          }
                      }
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
      
              }
          }
      }
      输出:
      线程A获得了共享变量a的监视器锁
      线程A获得了共享变量b的监视器锁
      线程A阻塞,并释放获取到的共享变量a的锁
      线程B获取到共享变量a的锁啦
      我是线程B,我一直在尝试获取共享变量b的锁,就是没获取到

      上例中,线程A一开始拥有变量a和变量b的锁,通过调用a的wait方法后,只有a的锁被释放了,b的锁并没有被释放掉,所以线程B无法获取到b变量的锁。

      2. wait(long tiemout)函数

      timeout意为超时参数,单位是毫秒(ms),比如wait(1000)意为在1秒内如果没被其他线程调用notify()或者notifyAll()方法唤醒,还是会因为超时返回。

      3.  wait(long timeout,int nanos)函数

      timeout单位是毫秒,nanos单位是纳秒,1毫秒 = 1000 微秒 = 1000 000 纳秒 ,该函数的主要作用是为了能够更加精确的控制等待时间

      4. notify()函数

      该方法用户唤醒被挂起的线程。注意阻塞和挂起的区别,阻塞是被动的,挂起是主动的。

      另外如果一个共享变量上有多个线程被挂起了,那么具体唤醒哪个等待线程是随机的,被唤醒的线程依旧需要先获取共享对象的锁才可以返回。

      notify函数和wait函数类似,如果当前线程没有获取到共享变量的监视器锁就调用notify方法,也会抛出IllegalMonitorStateException异常。

      5. notifyAll()函数

      当一个线程通过共享变量调用notifyAll()函数时,该方法会唤醒在此之前所有通过该共享变量调用了wait系列函数的线程。

      举一个例子来理解:

      package io.zhangjia.threads.test.Test;
      
      /**
       * @Author : ZhangJia
       * @Date : 2019/12/15 19:30
       * @Description : 
       */
      public class NotifyTest {
          private static volatile Object a = new Object();
      
          public static void main(String[] args) throws InterruptedException {
              Thread threadA = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      synchronized (a) {
                          System.out.println("线程A获得了共享变量a的监视器锁");
                          try {
                              System.out.println("线程A开始等待");
                              a.wait();
                              System.out.println("线程A结束等待");
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
      
                      }
                  }
              });
      
              Thread threadB = new Thread(new Runnable() {
                  @Override
                  public void run() {
                     synchronized (a) {
                         System.out.println("线程B获得了共享变量a的监视器锁");
                         try {
                             System.out.println("线程B开始等待");
                             a.wait();
                             System.out.println("线程B结束等待");
                         } catch (InterruptedException e) {
                             e.printStackTrace();
                         }
                     }
                  }
              });
      
              Thread threadC = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      synchronized (a) {
                          System.out.println("线程c开始唤醒");
                          a.notify();
                      }
                  }
              });
      
              threadA.start();
              threadB.start();
              Thread.sleep(1000); //主线程先休眠1s,为了让线程A和线程B都执行完wait方法后再调线程c的notify方法
              threadC.start();
      
              //等待线程结束
              threadA.join();
              threadB.join();
              threadC.join();
              System.out.println("主线程结束");
          }
      }
      //输出:
      线程A获得了共享变量a的监视器锁
      线程A开始等待
      线程B获得了共享变量a的监视器锁
      线程B开始等待
      线程c开始唤醒
      线程A结束等待

      通过上例可以看到,当线程A和线程B分别通过a变量调用wait方法将自身挂起后,线程c调用notify方法,随机唤醒了一个线程,如果将线程c中的 a.notify();修改为 a.notifyAll();则结果为:

      线程A获得了共享变量a的监视器锁
      线程A开始等待
      线程B获得了共享变量a的监视器锁
      线程B开始等待
      线程c开始唤醒
      线程B结束等待
      线程A结束等待
      主线程结束

      四.  等待线程执行终止的Join方法

      未完待续…

      最近一次更新:19.12.15

      山东省·日照市
    • 0
    • 0
    • 0
    • 1.1k
    • zjmarina

      请登录之后再进行评论

      登录

      赞助本站

      • 支付宝
      • 微信
      • QQ

      感谢一直支持本站的所有人!

      单栏布局 侧栏位置: