Java并发 之 深入理解Java线程

线程生命周期

操作系统层面

线程生命周期在操作系统层面有五个状态,分别是:初始化状态、可运行状态、运行状态、休眠状态和终止状态。如下图所示:

image.png

  • 初始化状态:此时线程已经创建,但是还不允许分配CPU执行。这个状态属于编程语言特有的,也就是这里的创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有被创建。
  • 可运行状态:线程已经可以分配CPU执行,此时真正的操作系统线程已经被成功创建,所以可以分配CPU资源。
  • 运行状态:当有空闲的CPU时,操作系统会将其分配给一个处于可运行状态的线程,此时被分配到CPU的线程状态就由可运行状态转换成运行状态。
  • 休眠状态:运行状态的线程如果调用一个阻塞的API(例如阻塞方式读取文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放CPU使用权,休眠状态的线程永远没有机会获得CPU使用权。只有当等待的事件出现了,线程才会从休眠状态转换为可运行状态,才能获得CPU使用权。
  • 终止状态:线程执行完毕或者遇到异常退出,线程就会进入终止状态,终止状态的线程不能切换到其他任何状态,进入终止状态就意味着线程的生命周期结束了。

Java层面

从Thread以下源码可以得知,Java语言中线程的生命周期共有六种状态,分别是NEW(初始化状态)、RUNNABLE(可运行状态+运行状态)、BLOCKED(阻塞状态)、WAITING(无时限等待状态)、TIMED_WAITING(有时限等待状态)、TERMINATED(终止状态) image.png

对于操作系统层面,Java线程中的BLOCKED、WAITING、TIMED_WAITING都属于休眠状态,也就是说只要Java线程处于这三种状态之一,这个线程就没有使用CUP的权限。

Java 线程六种状态之间的切换可以参考下图: image.png

Java 线程实现的几种方式

Java 中实现线程的方式有以下几种:

使用Thread类或继承Thread 类:

//创建线程对象 
Thread t = new Thread() { 
    public void run() { 
    // 要执行的任务 
    } 
}; 
//启动线程
t.start();
复制代码

实现Runnable接口配合Thread类使用:

//把线程(Thread)和 任务(要执行的代码-Runnable)分开
Runnable runnable = new Runnable() { 
    public void run(){ 
    // 要执行的任务 
    } 
}; 
// 创建线程对象 
Thread t = new Thread( runnable ); 
// 启动线程
t.start();
复制代码

实现有返回值的Callable接口

class CallableTask implements Callable<Integer{     
    @Override     
    public Integer call() throws Exception {         
        return new Random().nextInt();     
    } 
}
//使用FutureTask创建线程对象
Callable<Integer> callableTask = new CallableTask();
FutureTask<String> oneTask = new FutureTask<String>(callableTask);
Thread t = new Thread(oneTask);
//启动线程
t.start();
复制代码

使用lambda表达式

new Thread(() -> 
    // 线程任务代码
).start();
复制代码

注:本质上Java实现线程只有一种方式,都是通过New Thread()创建线程,调用Thread#start启动线程

Java 线程实现原理

Java线程属于内核级线程

Java线程的实现在JDK1.2之后-是基于操作系统原生线程模型来实现的。Sun JDK的Windows版本和Linux版本都使用一对一的线程模型实现,一条java线程就映射到一条轻量级进程之中。 image.png

  • 内核级线程(Kernel Level Thread ,KLT):它们是依赖于内核的,即无论是用户进程中的线程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。
  • 用户级线程(User Level Thread,ULT):操作系统内核不知道应用线程的存在。

目前Java官方不支持协程,但可以依赖第三方框架 kilim/quasar 来实现

协程是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特性。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。而协程的调用和子程序不同。协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。如下伪代码,假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能随时中断,去执行A,最终输出结果可能是:12xy3x, 而不是123xyz

def A(): 
    print '1' 
    print '2' 
    print '3' 
def B(): 
    print 'x'
    print 'y'
    print 'z'
    
A()
B()
复制代码

协程的优势

协程的特点在于是一个线程执行,和多线程比,协程具有以下优势:

  • 线程的切换由操作系统调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
  • 线程的默认stack大小是1M,而协程更轻量,接近1k。因此可以在相同的内存中开启更多的协程。
  • 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

注意: 协程适用于被阻塞的,且需要大量并发的场景(网络io)。不适合大量计算的场景。

Java 线程调度机制

线程的调度是指系统为线程分配CPU处理器使用权的过程,主要调度方式分两种,分别是协同式线程调度抢占式线程调度

  • 协同式线程调度

线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。最大好处是实现简单,而且切换操作对线程自己是可知的,没有什么线程同步问题。缺点是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。

  • 抢占式线程调度

每个线程由系统来分配执行时间,线程的切换不由线程本身来决定。Java中,Thread.yield()可以让出执行时间,但无法获取执行时间。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。

Java线程调度就是抢占式调度: 如果希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,就可以通过设置线程优先级来完成。Java语言一共有10个级别的线程优先级(Thread.MIN_PRIORITY 至 Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。

Thread 常用几种方法

sleep 方法

  • 线程调用sleep方法,会使当前线程从Runnable状态进入到TIMED_WAITING状态,不会释放对象锁
  • 正在睡眠的线程如果被其他线程使用interrupt方法打断,这时sleep方法会抛出InterruptedException,并且会清除中断标志。
  • 睡眠结束后的线程未必会立刻得到CPU时间时间片执行。
  • 调用sleep方法时如果传入参数0,则跟yield方法相同。

yield 方法

  • 线程调用yield方法会释放CPU资源,让当前线程从运行状态(Running)进入到可运行状态(Runnable),让优先级更高(至少相同)的线程获得执行机会,但是yield不会释放对象锁
  • 假设当前进程只有main线程,当调用yield之后,main线程会继续执行,因为没有比它优先级更高的线程。
  • 具体实现依赖于操作系统的任务调度。

join 方法

等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。 代码示例:

public class ThreadJoinDemo {

   public static void main(String[] args) throws InterruptedException {
      Thread thread = new Thread(() -> {
         System.out.println("t begin");

         try {
            Thread.sleep(5000);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         System.out.println("t finished");
      });

      long start = System.currentTimeMillis();
      thread.start();
      //thread 调用join, 此时main方法需要等待thread 运行结束才继续往下执行
      thread.join();
      
      System.out.println("执行时间:" + (System.currentTimeMillis() - start));
      System.out.println("Main finished");

   }
}
复制代码

stop 方法

stop方法已经被jdk弃用,原因是stop方法过于暴力,强行把执行到一半的线程终止。从以下示例代码可以知道,未执行完的线程调用stop会释放锁,因为使用stop可能会造成数据不一致的情况。

public class ThreadStopDemo {

   private static Object lock = new Object();

   public static void main(String[] args) throws InterruptedException {
      Thread thread = new Thread( ()->{
         synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "获取锁");

            try {
               Thread.sleep(60000);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "执行完成");
         }
      });

      thread.start();
      Thread.sleep(2000);
      //停止thread,释放锁
      thread.stop();

      new Thread(() -> {
         System.out.println(Thread.currentThread().getName() + "等待锁");
         synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "获取锁");
         }
      }).start();
   }
}
复制代码

Java 线程的中断机制

Java没有提供一种安全、直接的方法来停止线程。但是有提供了中断机制,中断机制是一种协作机制,也就是说通过中断没法直接终止另外的线程,而是需要被中断的线程自己处理。被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。

  • interrupt(): interrupt的作用是将线程的中断标志位设置为true,但是它并不会停止线程
  • isInterrupted(): interrupted的作用是判断线程的中断标志位是否为true, 该方法不会清除中断标志位
  • Thread.interrupted(): 判断当前线程的中断标志位是否为true, 并会清除中断标志位,将其重置为false

利用中断机制优雅的停止线程

public class StopThreadDemo {

   public static void main(String[] args) throws InterruptedException {
      Thread thread = new Thread(() -> {
         int count = 0;
         //中断标记为为false,线程继续执行
         while (!Thread.currentThread().isInterrupted()){
            System.out.println("count = " + count++);
         }
         //while 条件不成立,即中断标志位为true,退出线程
         System.out.println("线程停止: stop thread");
      });
      thread.start();

      Thread.sleep(500);
      //中断标志位设置为true
      thread.interrupt();
   }
}
复制代码

注意:使用中断机制时一定要注意是否存在中断标志位被清除的情况

sleep 期间线程是可以感受到中断信号的

当线程处于休眠状态时,调用了interrupt发出中断信息,线程是可以感受到中断信号的,并且会抛出一个InterruptedException 异常,同时清除中断信号,将中断标记位重置为false。这样就会导致线程调用Thread.currentThread.isInterrupted() 为false, 如上述代码改成如下代码,不在catch中重新手动添加中断信号,不做任何处理,就会屏蔽中断请求,导致线程无法正确停止。

public class StopThreadDemo {

   public static void main(String[] args) throws InterruptedException {
      Thread thread = new Thread(() -> {
         int count = 0;
         try {
            Thread.sleep(3000);
         } catch (InterruptedException e) {
            e.printStackTrace();
            //重新设置线程中断状态为true,防止线程无法正常退出 
            //Thread.currentThread().interrupt();
         }
         //中断标记为为false,线程继续执行
         while (!Thread.currentThread().isInterrupted()){
            System.out.println("count = " + count++);
         }
         //while 条件不成立,即中断标志位为true,退出线程
         System.out.println("线程停止: stop thread");
      });
      thread.start();

      Thread.sleep(500);
      //中断标志位设置为true
      thread.interrupt();
   }
}
复制代码

sleep可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位 wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位

Java 线程间的通信

volatile

volatile有两大特性,即可见性和有序性,其中有序性是禁止指令重排序,而可见性就是用来让线程之间进行通信的。

public class VolatileDemo {

   public static volatile boolean flag = true;
   //public static boolean flag = true;

   public static void main(String[] args) throws InterruptedException {
      new Thread(() -> {
         while (true){
            if (flag){
               System.out.println("turn on");
               flag = false;
            }
         }
      }).start();
      
      Thread.sleep(1);
      
      if (!flag){
         System.out.println("turn off");
         flag = true;
      }
   }
}
复制代码

等待唤醒(等待通知)机制

  • 等待唤醒机制可以使用 wait 和 notify 方法实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待对了进行等待,直到被notify唤醒。
public class WaitNotifyDemo {
   private static Object lock = new Object();
   private static boolean flag = true;

   public static void main(String[] args) {
      new Thread(() -> {
         synchronized (lock) {
            System.out.println("Thread1 get lock");
            while (flag){
               try {
                  System.out.println("Start wait...");
                  lock.wait(); // 进入阻塞等待队列,等待唤醒,并且会释放锁
                  System.out.println("Notified...");
               }catch (InterruptedException e){
                  e.printStackTrace();
               }
            }
            System.out.println("Wait End...");
         }
      }).start();

      new Thread(() -> {
         if (flag){
            synchronized (lock){
               System.out.println("Thread2 get lock");
               if (flag){
                  lock.notify();
                  System.out.println("Notify lock...");
                  flag = false;
               }
            }
         }
      }).start();
   }
}
复制代码
  • LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用LockSupport.park()则等待“许可”,调用unpark则为指定线程提供“许可”。使用它可以在任何场合使用线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一致的。
package thread;

import java.util.concurrent.locks.LockSupport;

public class LockSupportDemo {

   public static void main(String[] args) throws InterruptedException {
      Thread thread = new Thread(() -> {
         while (true){
//          try {
//             Thread.sleep(50);
//          } catch (InterruptedException e) {
//             e.printStackTrace();
//          }
            System.out.println("ParkThread Start...");
            LockSupport.park();//有许可则可以消耗掉许可继续执行,没有则需要等待许可,进入阻塞队列
            System.out.println("ParkThread 获得许可");
         }
      });
      thread.start();
      System.out.println("1唤醒thread");
      LockSupport.unpark(thread);

      System.out.println("2唤醒thread");
      LockSupport.unpark(thread);
      //由程序执行结果可知:
      //LockSupport.park 和 unpark 需要成对配合使用,但是park 和 unpark 的执行顺序没有限制,也就是说提前调用了unpark, 再调用park, 线程依然可以获得许可
      //如果连续多次调用unpark, 之后再连续调用park,那么只有第一次park获得许可
      //但是 unpark 需要再thread.srart()之后调用才能起作用
//    thread.start();
   }
}
复制代码

管道输入输出流:

管道输入输出流和普通的文件输入输出流或者网络输入输出流的不同之处在于: 管道主要是用于线程之间的数据传输,它的传输媒介为内存。
管道输入输出流主要包括以下4中具体实现:

  • PipedOutputStream 和 PipedInputStream 面向字节
  • PipedReader 和 PipedWriter 面向字符
public class PipedDemo {

   public static void main(String[] args) throws IOException {
      PipedWriter out = new PipedWriter();
      PipedReader in = new PipedReader();
      //输入流和输出流需要进行连接,否则在使用时会抛出IOException
      in.connect(out);

      Printer printer = new Printer(in);
      Thread printerThread = new Thread(printer, "ThreadName-Print");
      printerThread.start();
      System.out.println("System start, you can input what you want to print in the console.");
      while (true){
         out.write(System.in.read());
      }
   }
}

class Printer implements Runnable{
   private PipedReader in;

   public Printer(PipedReader in){
      this.in = in;
   }
   @Override
   public void run() {
      try {
         while (true){
            int receivedFromIn = in.read();
            if (((char) receivedFromIn) != '\n'){
               System.out.println("Printer received data: " + (char) receivedFromIn);
            }
         }
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}
复制代码

Thread.join

join可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现其实是基于等待通知机制的

Guess you like

Origin juejin.im/post/7068108059989508104