• 聊聊并发编程——线程


    1. 目录

      进程与线程

      1.1 进程(process):

      1.2 线程(thread):

      同步、并发、并行

      2.1 线程同步:

      2.2 线程并发:

      线程的实现方式

      3.1 继承Thread class

      3.2 实现Runable

      3.3 实现Callable

      线程的六种状态

      守护线程(Deamon Thread)

      启动和终止线程

      6.1线程的初始化

      6.2启动线程

      6.3线程中断

      6.4 终止线程

      6.5过期的方法

      线程间常用的调度方法​编辑

      线程间通信

      8.1volatile和synchronized关键字

      8.2等待/通知机制

      8.3管道输⼊/输出流

      8.4使⽤Thread.join()

      8.5使用ThreadLocal


      进程与线程

      1.1 进程(process):

      进程是程序的一次执行过程,是动态的概念。有生命周期,随着程序终止而销毁。比如QQ、音乐播放器、腾讯视频等等。

      注意:进程A与进程B之间内存独立不共享。

      1.2 线程(thread):

      线程是进程中的实际运作的单位,是进程的一条流水线,是程序的实际执行者,是最小的执行单位。通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程。线程是CPU调度和执行的最小单位。

      注意: 1.在java中,线程A与线程B堆内存和方法区内存共享,栈内存独立(一个线程一个栈)。

      2.很多多线程都是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器,如果是模拟出来的多线程,即一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。

    2. 同步、并发、并行

      2.1 线程同步

      多个线程访问同一个对象,利用对象等待池(等待机制)形成队列,挨个使用。保证多线程对共享资源的安全访问。

      2.2 线程并发:

      同一时间段内发生,不是同时发生。系统仅有一个CPU,把CPU的运行时间划分为若干个时间段,再将时间段分配给各个线程。在一个线程在其时间段执行时,其余线程处于挂起状。这种方式我们称之为并发(Concurrent)。

      2.3 线程并行:

      同一时刻同时发生。系统拥有一个以上CPU时,则存在多个线程时可并行执行。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行。这种方式我们称之为并行(Parallel)。

    3. 线程的实现方式

      3.1 继承Thread class
      /*
      * 1.自定义线程类继承Thread class
      * 2.重写run方法编写线程执行体
      * 3.创建线程对象,调用start()方法启动
      * 注意:线程对象实例化之后,调用start()进入就绪状态,并不会立即执行,需要CPU调度
      */
      public class ThreadNote extends Thread {
          @Override
          public void run() {
              for (int i = 0; i < 10000; i++) {
                  System.out.println(Thread.currentThread().getName() + "我在记笔记" + i);
              }
          }
      ​
          public static void main(String[] args) {
              ThreadNote threadNote = new ThreadNote();
              threadNote.start();
              for (int i = 0; i < 10000; i++) {
                  System.out.println(Thread.currentThread().getName() + i);
              }
      ​
          }
      }

      CPU调度处理,主线程和自定义线程执行顺序随机。

      3.2 实现Runable
      /*
      * 1.自定义线程类实现Runable接口
      * 2.重写run方法编写线程执行体
      * 3.创建线程对象,Runable没有start()的实现,通过Thread类代理自定义线程调用start()方法启动
      * 注意:由于java的单继承机制的限制,相对于extends更倾向于implement的方式实现线程
      */
      public class ThreadCode implements Runnable {
          @Override
          public void run() {
              for (int i = 0; i < 1000; i++) {
                  System.out.println(Thread.currentThread().getName() + "我在写代码" + i);
              }
          }
      ​
          public static void main(String[] args) {
              ThreadCode threadCode = new ThreadCode();
              new Thread(threadCode).start();
              for (int i = 0; i < 1000; i++) {
                  System.out.println(Thread.currentThread().getName() + i);
              }
          }
      }

      CPU调度处理,主线程和自定义线程执行顺序随机。

      3.3 实现Callable
      /*
      * 1.自定义线程类实现Callable接口
      * 2.重写call方法编写线程执行体,相当于run方法,只不过有返回值
      * 3.创建线程对象,需要调用FutureTask的构造器创建出futureTask对象,然后通过Thread类代理task调用start()方法启动
      * 注意:调用futureTask的get方法获取线程返回值是会导致“当前线程阻塞”
      */
      public class ThreadListen implements Callable {
          @Override
          public Integer call() throws Exception {
              // 模拟执行
              System.out.println("call method begin");
              for (int i = 0; i < 10; i++) {
                  Thread.sleep(1000);
                  System.out.println("倒计时" + (10 - i) + "秒!");
              }
              System.out.println("call method end!");
              int a = 100;
              int b = 200;
              return a + b; //自动装箱(300结果变成Integer)
          }
      ​
          public static void main(String[] args) throws ExecutionException, InterruptedException {
              ThreadListen threadListen = new ThreadListen();
              FutureTask futureTask = new FutureTask(threadListen);
              new Thread(futureTask).start();
              System.out.println("线程计算结果:" + futureTask.get()); // 调用futureTask的get方法获取返回值是会导致“当前线程阻塞”的
              System.out.println("main线程输出Hello World!"); // 被阻塞的主线程只会在futureTask获取结果之后执行
      ​
          }
      }

      优点:可以获取到线程的执行结果。

      缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。

    4. 线程的六种状态

      六种状态包括:NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED。

      线程有完整的生命周期,其中状态也不是一直固定的,会随着代码的执行在不同状态之间切换,如下图所示:

    5. 守护线程(Deamon Thread)

      1. Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这 意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。

      2. Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。

      3. Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行。

      public class DaemonThread {
          public static void main(String[] args) {
              Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");
              thread.setDaemon(true);
              thread.start();
          }
          static class DaemonRunner implements Runnable {
              @Override
              public void run() {
                  try {
                      SleepUtils.second(100);
                  } finally {
                      System.out.println("DaemonThread finally run.");
                  }
              }
          }
      }

      因为Java虚拟机中没有非Daemon线程,虚拟机需要退出。Java虚拟机中的所有Daemon线程都需要立即终止,因此DaemonRunner立即终止,但是DaemonRunner中的finally块并没有执行。

    6. 启动和终止线程

      6.1线程的初始化

      java.lang.Thread中对线程进行初始化的部分源码

      private void init(ThreadGroup g, Runnable target, String name,
                            long stackSize, AccessControlContext acc) {
              if (name == null) {
                  throw new NullPointerException("name cannot be null");
              }
              // 当前线程就是该线程的父线程
              Thread parent = currentThread();
              this.group = g;
              // 将daemon、priority属性设置为父线程的对应属性
              this.daemon = parent.isDaemon();
              this.priority = parent.getPriority();
              this.target = target;
              setPriority(priority);
              // 将父线程的inheritableThreadLocals复制过来
              if (parent.inheritableThreadLocals != null)
                  this.inheritableThreadLocals =
                          ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
              // 分配一个线程ID
              tid = nextThreadID();
          }

      一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的 ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个能够运行的线程对象就初始化好了,在堆内存中等待着运行。

      6.2启动线程

      线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用 start()方法的线程。为什么调⽤start()⽅法时会执⾏run()⽅法,那怎么不直接调⽤run()⽅ 法?

      这是因为start()方法会启动一个新的线程来执行run()方法中的代码,而直接调用run()方法只会在当前线程中执行run()方法的内容,不会创建新的线程。这两者之间的区别非常重要,有以下几个原因:

      1. 多线程执行: 调用start()方法会启动一个新的线程,使得多个线程可以并行执行。每个线程都有自己的执行堆栈和上下文,可以独立运行。如果直接调用run()方法,它将在当前线程的上下文中执行,没有实现多线程的效果。

      2. 并发性: 使用start()方法启动线程可以实现并发性,多个线程可以同时运行,提高程序的执行效率。直接调用run()方法将导致串行执行,一个线程执行完后,下一个线程才能执行,不会实现并发。

      3. 线程生命周期管理: 使用start()方法启动线程后,Java虚拟机会自动管理线程的生命周期,包括创建、运行、休眠、等待和终止。直接调用run()方法则不会启动新线程,你需要自己管理线程的生命周期,包括线程的启动和结束,容易出错。

      4. 线程安全性: 在多线程环境中,使用start()方法启动线程可以更好地确保线程安全性。每个线程都有自己的栈空间,可以避免多线程之间的竞态条件。直接调用run()方法可能导致多线程共享相同的栈空间,容易引发线程安全问题。

      因此,一般情况下,如果希望创建并发执行的线程,应该使用start()方法来启动线程。只有在特殊情况下,才会直接调用run()方法,比如在单线程应用程序中或需要在当前线程中执行某些代码块的情况下。

      6.3线程中断

      中断可以理解为是线程的一个标识位属性,线程通过调用IsInterrupted()来判断是否被中断,可以调用静态方法Thread.interrupted()对当前线程的中断标识进行复位。如果线程已经处于终结状态,即使该线程被中断过,也会返回false。

      许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位 清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。

      public class ThreadInterrupted {
          public static void main(String[] args) throws InterruptedException {
              Thread sleepThread = new Thread(new SleepThread(),"SleepThread");
              sleepThread.setDaemon(true);
      ​
              Thread busyThread= new Thread(new BusyThread(), "BusyThread");
              busyThread.setDaemon(true);
      ​
              sleepThread.start();
              busyThread.start();
      ​
              TimeUnit.SECONDS.sleep(5);
              System.out.println("休眠5秒");
              sleepThread.interrupt();
              busyThread.interrupt();
              System.out.println("sleepThread interrupted is " + sleepThread.isInterrupted());
              System.out.println("busyThread interrupted is " + busyThread.isInterrupted());
          }
      ​
          static class SleepThread implements Runnable {
              @Override
              public void run() {
                  while (true) {
                      SleepUtils.second(10);
                  }
              }
          }
          static class BusyThread implements Runnable {
              @Override
              public void run() {
                  while (true) {
                      System.out.println("忙碌中...");
                  }
              }
          }
      }
      public class SleepUtils {
          public static final void second(long seconds) {
              try {
                  TimeUnit.SECONDS.sleep(seconds);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }

      从结果可以看出,抛出InterruptedException的线程SleepThread,其中断标识位被清除了, 而一直忙碌运作的线程BusyThread,中断标识位没有被清除。

      6.4 终止线程
      1. 使用标志位: 这是一种常见的线程终止方式。在线程的执行代码中使用一个布尔型的标志位来指示线程是否应该终止。线程在执行过程中定期检查这个标志位,并在需要时退出线程。

        volatile boolean shouldTerminate = false;
        ​
        public void run() {
            while (!shouldTerminate) {
                // 线程的主要任务
            }
        }
        ​
        public void stopThread() {
            shouldTerminate = true;
        }

        这种方式需要注意线程安全性,确保在设置标志位和检查标志位时使用适当的同步。

      2. 使用Thread.interrupt() 可以使用Thread.interrupt()方法来中断线程。当调用这个方法时,线程会收到一个中断请求,并且可以在适当的时候停止执行。

        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                // 线程的主要任务
            }
        }
        
        public void stopThread() {
            thread.interrupt(); // 中断线程
        }

        这种方式需要在线程的run()方法中检查线程的中断状态。

      3. 使用stop()方法(不推荐使用): Java中的Thread.stop()方法可以立即终止线程的执行,但不建议使用它,因为它可能会导致线程处于不一致的状态并引发不可预测的问题。这个方法已被标记为不安全,通常不建议使用。

      4. 使用Thread.join() 使用Thread.join()方法可以等待另一个线程执行完毕,然后继续执行当前线程。这种方式可以用于等待其他线程的完成。

        Thread thread = new Thread(() -> {
            // 线程的任务
        });
        
        thread.start();
        thread.join(); // 等待线程执行完毕

        这种方式不会直接终止线程,而是等待线程自然结束。

      5. 使用线程池: 如果使用线程池管理线程,可以通过关闭线程池来终止所有线程。使用ExecutorService.shutdown()方法关闭线程池,或者使用ExecutorService.shutdownNow()方法立即终止线程池中的所有线程。

        ExecutorService executor = Executors.newFixedThreadPool(5);
        // 执行任务
        
        executor.shutdown(); // 关闭线程池,等待任务执行完毕
        // 或者使用 executor.shutdownNow() 来立即终止所有线程

      不同的线程终止方式适用于不同的场景和需求。在选择终止线程的方式时,需要谨慎考虑线程的状态和线程间通信,以确保线程能够被正确、安全地终止。避免不安全的线程终止方法,如直接使用Thread.stop()

      6.5过期的方法
      1. 暂停suspend():调用后线程不会释放资源(比如锁),而是占着资源进入休眠,容易死锁。

      2. 恢复resume()

      3. 停止stop():终结一个线程的时候是不保证资源正常释放的。就不给线程完成资源释放的机会。

      不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资 源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结 一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会, 因此会导致程序可能工作在不确定状态下

    7. 线程间常用的调度方法

      在多线程编程中,线程间的调度(Scheduling)是一项关键任务,用于协调和控制线程的执行顺序和优先级。以下是一些常用的线程间调度方法:

      1. join()方法: join()方法用于等待一个线程的完成。当一个线程调用另一个线程的join()方法时,它会被阻塞,直到被等待的线程执行完成。这通常用于确保某个线程在另一个线程之后执行。

      2. wait()notify()/notifyAll() 这些方法用于线程之间的协作和通信。wait()使线程进入等待状态,notify()notifyAll()用于唤醒等待的线程。它们通常与synchronized关键字一起使用,用于实现线程之间的同步。

      3. sleep()方法: Thread.sleep()方法允许线程在指定的时间内休眠。它可以用于引入延迟或控制线程的执行速度。

      4. yield()方法 :Thread类中的静态⽅法,当⼀个线程调⽤ yield ⽅法时,实际就是在暗示线程调度器当前线程请求 让出⾃⼰的CPU ,但是线程调度器可以⽆条件忽略这个暗示。 线程中断 Java 中的线程中断是⼀种线程间的协作模式,通过设置线程的中断标志并不能直接终⽌该线程的执⾏,⽽是被中断的线程根据中断状态⾃⾏处理。

      5. void interrupt() :中断线程,例如,当线程A运⾏时,线程B可以调⽤线程interrupt() ⽅法来设置线程的中断 标志为true 并⽴即返回。设置标志仅仅是设置标志, 线程A实际并没有被中断, 会继续往下执⾏。 boolean isInterrupted() ⽅法: 检测当前线程是否被中断。 boolean interrupted() ⽅法: 检测当前线程是否被中断,与 isInterrupted 不同的是,该⽅法如果发现当前 线程被中断,则会清除中断标志

    8. 线程间通信

      8.1volatile和synchronized关键字

      对象和成员变量存储在共享内存上,而每个线程本地都有一个副本。volatile修饰的变量强制要求了对该变量的访问必须从共享内存读取,对该变量的更新必须同步刷回共享内存,保证所有线程对变量访问的可见性。

      对象的锁在指令的理解维度上就是每个对象有自己的监视器,通过monitorenter和monitorexit可以获取和释放监视器,也就是获取锁和释放锁。

      任意线程对Object(Object由synchronized保护)的访问,首先要获得 Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object 的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新 尝试对监视器的获取。

      8.2等待/通知机制

      等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B 调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而 执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的 关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

      wait()、notify()以 及notifyAll()时需要注意的细节,如下。

      1)使用wait()、notify()和notifyAll()时需要先对调用对象加锁。

      2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的 等待队列。

      3)notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或 notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。

      4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll() 方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为 BLOCKED。

      5)从wait()方法返回的前提是获得了调用对象的锁。 从上述细节中可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从 wait()方法返回时能够感知到通知线程对变量做出的修改。

      8.3管道输⼊/输出流

      管道输⼊/输出流和普通的⽂件输⼊/输出流或者⽹络输⼊/输出流不同之处在于,它主要⽤于线程之间的数据传输, ⽽传输的媒介为内存。 管道输⼊/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、 PipedReader和 PipedWriter,前两种⾯向字节,⽽后两种⾯向字符。

      8.4使⽤Thread.join()

      如果⼀个线程A执⾏了thread.join()语句,其含义是:当前线程A等待thread线程终⽌之后才从thread.join()返 回。线程Thread除了提供join()⽅法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的⽅法。

      8.5使用ThreadLocal

      ThreadLocal,即线程变量,是⼀个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程 上,也就是说⼀个线程可以根据⼀个ThreadLocal对象查询到绑定在这个线程上的⼀个值。 可以通过set(T)⽅法来设置⼀个值,在当前线程下再通过get()⽅法获取到原先设置的值。

  • 相关阅读:
    一年前端|17K|极光推送5轮面经
    dd命令:用于读取、转换并输出数据
    递推递归与排列组合
    再学责任链和代理模式
    Java 基于微信小程序的快递柜小程序
    Java安全之CC6
    决策树原理以及在sklearn中的使用
    无人机技术,无人机动力系统知识,电机、电调、桨叶技术详解
    Linux环境基础开发工具使用(上)
    函数基础学习01
  • 原文地址:https://blog.csdn.net/Elaine2391/article/details/133252119