Java多线程并发

😝Java中的多线程和并发😝

Posted by 华仔 on March 12, 2019

Java并发知识库

Java并发知识库

Java线程实现/创建方式

  • 继承Thread类 Thread类本质上是实现了Runnable接口的一个示例,代表一个线程的实例。启动线程的唯一方法就是Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。

    public class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("MyThread.run()");
        }
    }
    class Test {
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
            myThread.start();
        }
    }
    
  • 实现Runnable接口

    public class MyThread implements Runnable {
      @Override
      public void run() {
          System.out.println("MyThread.run()");
      }
    }
    class Test {
      public static void main(String[] args) {
          //启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例
          MyThread myThread = new MyThread();
          Thread thread = new Thread(myThread);
          //事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run()
          thread.start();
      }
    }
    
  • **ExecutorService、Callable、Future有返回值线程**

    有返回值的任务必须实现Callable接口,类似的,无返回值的任务必须实现Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中的有返回结果的多线程了。

    public class MyThread {
      public static void main(String[] args) throws ExecutionException, InterruptedException {
          //任务数量
          int taskSize = 10;
          //创建一个线程池
          ExecutorService pool = Executors.newFixedThreadPool(taskSize);
          //创建多个有返回值的任务
          List<Future> list = new ArrayList<>();
          for (int i = 0; i < taskSize; i++) {
              Callable c = new MyCallable(i+"");
              //执行任务并获取Future对象
              Future f = pool.submit(c);
              list.add(f);
          }
          //关闭线程
          pool.shutdown();
          //获取所有并发任务的运行结果
          for (Future future : list) {
              //从Future对象上获取任务的返回值,并输出到控制台
              System.out.println("res:"+ future.get().toString());
          }
      }
    }
    class MyCallable implements Callable {
      private String param;
      MyCallable(String param) {
          this.param = param;
      }
      @Override
      public Object call() {
          System.out.println("执行了MyCallable的call()");
          return param;
      }
    }
    
  • 基于线程池的方式

    线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存策略,也就是使用线程池。

    ```java   public class MyThread {
    public static void main(String[] args) {
        //创建线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        while (true) {
            threadPool.execute(()->{
                //提交多个线程任务,并执行
                System.out.println(Thread.currentThread().getName() + " is running...");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }   }
    ```
    

4种线程池

​ Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

class Executor

  • newCachedThreadPool 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

  • newFixedThreadPool

    创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数nThreads线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

  • newScheduledThreadPool

    创建一个线程池,它可安排在给定延迟后运行命令后者定期地执行。

    public class MyThread {
      public static void main(String[] args) {
          System.out.println("程序跑起来了");
          ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
          scheduledThreadPool.schedule(() -> {
              System.out.println("延迟三秒");
          }, 3, TimeUnit.SECONDS);
          scheduledThreadPool.scheduleAtFixedRate(() -> {
              System.out.println("延迟一秒后每三秒执行一次");
          }, 1, 3, TimeUnit.SECONDS);
      }
    }
    
  • newSingleThreadExecutor

    Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来代替原有的线程继续执行下去

线程生命周期(状态)

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

  • 新建状态(New)

    当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。

  • 就绪状态(Runnable)

    当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

  • 运行状态(Running)

    如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。

  • 阻塞状态(Blocked)

    阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cput timeslice转到运行(running)状态。阻塞的情况分为三种:

    • 等待阻塞(o.wait->等待队列):
      • 运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waiting queue)中。
    • 同步阻塞(lock->锁池):
      • 运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把线程放入锁池(lock pool)中。
    • 其他阻塞(sleep/join):
      • 运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出I/O请求时,JVM会把该线程置位阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行状态(runnable)状态。
  • 线程死亡(Dead)

    线程会以下面三种方式结束,结束后就是死亡状态。

    1. 正常结束:run()或call()方法执行完成,线程正常结束。
    2. 异常结束:线程抛出一个未捕获的Exception或Error。
    3. 调用stop:直接调用该线程的stop()方法来结束该线程–该方法通常容易导致死锁,不推荐使用。

    线程执行流程

终止线程4种方式

  • 正常运行结束

    • 程序运行结束,线程自动结束。
  • 使用退出标志退出线程

    一般run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出,代码示例:

    public class ThreadSafe extends Thread {
        public volatile boolean exit = false;
        public void run() {
            while(!exit) {
                //do something
            }
        }
    }
    

    定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false。在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值。

  • Interrupt方法结束线程

    使用interrupt()方法来中断线程有两种情况:

    1. 线程处于阻塞状态:使用了sleep,同步锁的wait,socket中的receiver/accept等方法时,会使线程处于阻塞状态。当调用线程interrupt()方法时,会抛出InterruptedException异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用interrupt方法线程就会结束,,实际上是错的,一定要先捕获interruptedException异常之后通过break来跳出循环,才能正常结束run方法

    2. 线程未处于阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置位true,和使用自定义的标志来控制循环时一样的道理。

      public class ThreadSafe extends Thread {
          @Override
          public void run() {
              while (!isInterrupted()) { //非阻塞过程中通过判断中断标志来退出
                  try {
                      Thread.sleep(5 * 1000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                      break; //捕获到异常之后,执行break跳出循环
                  }
              }
          }
      }
      
  • stop方法终止线程(线程不安全)

    程序中可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就像突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeath的错误,并会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐试用 stop方法来终止线程。

sleep和wait区别

在Java中每个对象都有两个池:锁池(monitor)和等待池

锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

  1. 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
  2. sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。
  3. 在调用sleep()方法的过程中,线程不会释放对象锁。
  4. 而调用wait()方法的时候,线程会放弃对象锁,进入此对象的等待池,只有针对此对象调用notify()方法或notifyAll()后本线程才进入对象锁池准备获取对象锁进入运行状态。
class WaitThread implements Runnable {
    @Override
    public void run() {
        synchronized (D.class) {
            System.out.println(Thread.currentThread().getName() + " into wait");
            try {
                D.class.wait(); //使该线程进入该对象的锁池,并释放该对象的锁
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" wait over");
        }
    }
}
class NotifyThread implements Runnable {
    @Override
    public void run() {
        synchronized (D.class) {
            System.out.println(Thread.currentThread().getName() + " into notify");
            D.class.notify(); //随机使该对象的等待池中的某一个线程进入锁池
//            D.class.notifyAll(); //使该对象的等待池
            System.out.println(Thread.currentThread().getName() + " notify over");
        }
    }
}
public class D {
    public static void main(String[] args) throws InterruptedException {
//        new Thread(new WaitThread()).start();
//        new Thread(new WaitThread()).start();
//        new Thread(new WaitThread()).start();
//        new Thread(new WaitThread()).start();
//        Thread.sleep(2000);
//        new Thread(new NotifyThread()).start();
//        new Thread(new NotifyThread()).start();
//        new Thread(new NotifyThread()).start();
//        new Thread(new NotifyThread()).start();
//        new Thread(new NotifyThread()).start();
        new Thread(new NotifyThread()).start();
    }
}

start与run区别

  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码。
  2. 可以调用Thread类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。
  3. 方法run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行run函数当中的代码。Run方法运行结束,此线程终止。然后CPU再调度其他线程。

Java后台线程

  1. 定义:守护线程–也称”服务线程“,它是后台线程,它有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
  2. 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
  3. 设置:通过setDeamon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在线程对象创建之前用线程对象的setDeamon方法。
  4. 在Deamon线程中产生的新线程也是Deamon的。
  5. 线程则是JVM级别的,以Tomcat为例,如果你在Web应用中启动一个线程,这个线程的生命周期并不会和Web应用程序保持同步。也就是说,即使你停止了Web应用,这个线程依旧是活跃的。
  6. example:垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
  7. 生命周期:守护进程(Deamon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死“。当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则JVM不会退出。