Java多线程基础知识-读书笔记

575 阅读17分钟

前言

最近作者小白主要在阅读《Java并发程序设计》一书,书中讲述了很多关于高并发相关的知识。小白把个人认为比较重要的部分按照个人理解作以记录!文章中会存在很多表述不准确的地方,请大家谅解并在评论区一同探讨!

大学毕业

什么是多线程?

想要了解多线程,必须先了解进程!进程源于操作系统中的概念,它是计算机中程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。线程可以理解为轻量级的线程,但不同于进程的是线程所使用资源源于进程。

因此,进程可以说是线程的容器,单个进程内会包含1个或多个线程,多个线程被分配进程上的资源后执行相应的指令,最后结束其罪恶的一生!

多线程状态扭转?

线程状态表述了线程是否已经创建、是否可以执行、是否已经执行结束等问题。

当进程了解到当前线程处于什么状态,那么进程就知道是不是该给线程分配内存资源、是不是该给线程分配CPU、是不是该从该线程上回收已经分配的所有资源等。

正是为解决上述问题,合理的定义线程状态以及合理地扭转线程状态,成为设计线程的核心问题!

线程包含的状态以及扭转过程如下图所示:

线程状态机

需要注意的是线程状态变换并不是闭环,也就是说已经处于TERMINATED状态的线程再也不会回到NEW状态!

多线程状态基本已经明确,JDK官方团队如此专业,必然也为多线程定义线程状态枚举:

 public class Thread implements Runnable{
     //...
     public enum State 
         NEW,
         RUNNABLE,
         BLOCKED,
         WAITING,
         TIMED_WAITING,
         TERMINATED;
     }
 }

线程状态解释:

  • NEW: 表示线程已经创建但执行的状态。
  • RUNNABLE: 表示线程可执行的线程状态。此时包含两种状态,一种是线程正在JVM中执行,而另一种是该线程正在等待操作系统中的其他资源(CPU时间片)。
  • BLOCKED: 表示线程阻塞状态, 线程持续暂停执行直到拿到锁之后才会执行同步代码块。
  • WAITING: 表示线程处于无限等待状态,除非线程被唤醒,否则线程一直处于无限等待状态。
  • TIME_WAITING: 表示线程处于有限状态,本状态不同于无线等待的是,处于本状态继续执行的条件除被唤醒外还包含超时。

线程状态已经基本了解了,那线程相关的API必然是用来操作线程从某以状态转换至其他状态的操作了咯.

常见的线程操作与状态转换如下表所示:

线程操作起始状态终点状态
Thread.startNEWRUNNABLE
Thread.stopRUNNABLETERMINATED
Thread.waitRUNNABLEWAITING
Thread.notifyWAITING/TIME_WAITINGRUNNABLE
Thread.wait(timeout)RUNNABLTIME_WAITING

创建线程方式到底有几种?

相信很多小伙伴在面试中被问到这个问题时一定会回答两种: 实现Runnable接口和继承Thread类! 作者小白也常常是这样回答的,所以至今还在这里恶补多线程知识!

排开线程池创建线程的方式外,其实有三种方式:

  • 实现Runnable接口
  • 继承Thread类
  • 实现Callable接口

其中 实现Runnable接口和实现Callable接口 两种方式可以归为一类,而继承Thread类方式属于另依赖.

继承Thread类

 public class MyThread extends Thread {
     @Override
     public void run() {
         //...
     }
     public static void main(String[] args) {
         MyThread thread = new MyThread();
         //开启多线程且线程状态转变为Runnable
         thread.start();
          
     }
 }

RUNNABLE状态线程获取CPU时间片后将执行run()方法下的代码,因此需要继承Thread类的同时需要重写run()方法以实现自己的任务。此外,若按如下方式执行并不会开启多线程,而仅仅是在当前线程中调用执行run()方法!

   public static void main(String[] args) {
         MyThread thread = new MyThread();
         thread.run();
     }

实现Runnable接口

 public class MyRunnable implements Runnable {
     @Override
     public void run() {
         //...
     }
 ​
     public static void main(String[] args) {
         //执行任务
         MyRunnable runnable = new MyRunnable();
         //多线程驱动
         Thread thread = new Thread(runnable);
         //开启多线程
         thread.start();
     }
 } 

"实现Runnable接口的类只能当作一个可以在线程中执行的任务,不是真正意义上的线程,因此最后还是需要通过Thread来调用。可理解为,任务是通过线程驱动从而执行的!"----cyc2018 技术面试基础知识

为了更好地理解上述这段话,就不得不翻翻ThreadRunnable的源码了:

 // Runnable 接口代码
 public interface Runnable {
     public abstract void run();
 }
 //Thread 类核心关注点
 public class Thread implements Runnable {
      //...
     private Runnable target;
     public synchronized void start() {
         if (threadStatus != 0)
             throw new IllegalThreadStateException();
         group.add(this);
         boolean started = false;
         try {
             start0();
             started = true;
         } finally {
             try {
                 if (!started) {
                     group.threadStartFailed(this);
                 }
             } catch (Throwable ignore) {
             }
         }
     }
     //本地方法 
     private native void start0();
     //重写run()方法
     @Override
     public void run() {
         //判断私有属性target是否为空
         if (target != null) {
             //调用target方法
             target.run();
         }
     }
 }

小白首先观察的是Thread.start()方法,该方法调用了本地方法Thread.start0() 。其中本地方法Thread.start0()的作用是向操作系统申请线程并在该线程获取CPU时间片后执行Thread.run()方法下的指令。

那么此时就存在这么个问题了,对于 继承Thread类的MyThread类的来说就具有两个职责了,一个继自Thread类的申请操作系统线程资源,另一个是定义需要该线程执行的任务。

由于这点违背了单一职责原则,JDK官方团队必然对此不会视而不见der。因此,在Thread类源码中可以看到策略模式 的身影。 各位看官还是对照着上面的简要源码,判别一下到底是不是将职责分离了哟!

因此,cycy2018 老大说 Runnable 实现类仅是任务而Thread是线程驱动真是十分准备呢!

实现Callable接口

Callable接口与Runnable接口相比,Callable可以有返回值,返回值通过FutureTask进行封装。

 public class MyCallable implements Callable<Integer> {
     @Override
     public Integer call() throws Exception {
         //...
         return 0;
     }
 ​
     public static void main(String[] args) throws ExecutionException, InterruptedException {
         //任务
         MyCallable callable = new MyCallable();
         //任务适配器
         FutureTask<Integer> futureTask = new FutureTask<>(callable);
         //多线程驱动
         Thread thread = new Thread(futureTask);
         thread.start();
         //获取结果
         Integer res = futureTask.get();
     }
 }

Callable实现类是具有返回值的任务,而Thread依旧相当于线程驱动,而FutureTask类更像是一个适配器。由于Thread类中属性target必须是Runnable类或子类,因此FutureTask实现了Runnable接口。而我们又希望执行的是Callable类中call()并具有返回值,因此FutureTask包含Callable实例,并重写run()方法调用call()方法。

 //Callable接口
 public interface Callable<V> {
     V call() throws Exception;
 }
 //FutureTask 类
 public class FutureTask<V> implements RunnableFuture<V> {
     //...
     private Callable<V> callable;
     public void run() {
         if (state != NEW ||
             !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                          null, Thread.currentThread()))
             return;
         try {
             Callable<V> c = callable;
             if (c != null && state == NEW) {
                 V result;
                 boolean ran;
                 try {
                     //调用call()方法
                     result = c.call();
                     ran = true;
                 } catch (Throwable ex) {
                     result = null;
                     ran = false;
                     setException(ex);
                 }
                 if (ran)
                     set(result);
             }
         } finally {
             runner = null;
             int s = state;
             if (s >= INTERRUPTING)
                 handlePossibleCancellationInterrupt(s);
         }
     }
 }

小结

关于如何创建多线程这块暂时记录这么多,对于到底采用实现接口还是继承Thread中哪种方式这个问题,《Java高并发程序设计》中回答是接口!该书作者认为Java仅支持单继承,因此继承是一种宝贵资源,既然实现接口可以实现相同功能又何必浪费资源!而小白我的观点是,JDK官队推荐我们如何使用,那就怎么用!

如何停止线程?

停止线程不单单使线程从RUNNABLE状态转变为TERMINALED状态,其中还包括被终止的线程如何释放持有的锁等问题.

Thread.stop方法?

上古JDK中提供了停止线程的APIThread.stop(),但该方法已经被弃用。哪又为什么弃用呢?

以该书中示例演示问题所在:

 public class StopThread {
     //模拟用户
     public static StopThread.User u = new User();
 ​
     public static void main(String[] args) throws InterruptedException {
         //开启读线程
         new ReadObjectThread().start();
         while (true) {
             //间隔150ms开启和关闭写线程
             Thread t = new ChangeObjectThread();
             t.start();
             Thread.sleep(150);
             t.stop();
         }
     }
     //正常情况下id和name始终相等
     public static class User {
         private int id;
         private String name;
         //构造方法/get set方法/toString
     }
     //写线程
     public static class ChangeObjectThread extends Thread {
         @Override
         public void run() {
             while (true) {
                 synchronized (u) {
                     //随机修改成某个值
                     int v = (int) (System.currentTimeMillis() / 1000);
                     //修改ID
                     u.setId(v);
                     try {
                         //当前线程睡眠100ms
                         Thread.sleep(100);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     //修改name
                     u.setName(String.valueOf(v));
                 }
                 //让出当前线程的CPU时间片,正在运行的线程转变为就绪状态,重新竞争CPU调度
                 Thread.yield();
             }
         }
     }
     //读线程
     public static class ReadObjectThread extends Thread {
         @Override
         public void run() {
             while (true) {
                 synchronized (u) {
                     //判断用户ID和name是否一直不一致则输出
                     if (u.getId() != Integer.valueOf(u.getName())) {
                         System.out.println(u.toString());
                     }
                 }
                 //让出时间
                 Thread.yield();
             }
         }
     }
 }

示例中主要功能:写线程每次都同时修改idname为相同的值,因此按照正常逻辑应该不会有任何输出结果!

而实际的执行结果是:

 User{id=1627139301, name='1627139300'}

明明就加了锁,居然还存在数据不一致问题!

其实问题就出在Thread.stop()方法上,stop()方法执行后会释放该线程持有的所有锁资源,从而导致了数据的不一致性。

如何设计安全的Thread.stop()方法?

"安全"其实主要考虑的是如何去释放锁资源,小白认为只要等待线程执行完同步代码块释放锁后再终止线程不就OK了嘛

 public class DiyStopThread {
     //模拟用户
     public static DiyStopThread.User u = new DiyStopThread.User();
 ​
     public static void main(String[] args) throws InterruptedException {
         //开启读线程
         new DiyStopThread.ReadObjectThread().start();
         while (true) {
             ChangeObjectThread t = new DiyStopThread.ChangeObjectThread();
             t.start();
             Thread.sleep(150);
             //自定义停止线程
             t.stopMe();
         }
     }
     
     //自定义写线程
     public static class ChangeObjectThread extends Thread {
         //是否终止本线程标记位
         private volatile boolean stopMe = false;
         //修改标记位
         public void stopMe() {
             stopMe = true;
         }
         @Override
         public void run() {
             while (true) {
                 //终止本线程置位则退出循环结束线程
                 if (stopMe) {
                     System.out.println("Thread exit.");
                     break;
                 }
                 synchronized (u) {
                     int v = (int) (System.currentTimeMillis() / 1000);
                     //修改ID
                     u.setId(v);
                     try {
                         Thread.sleep(100);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     //修改name
                     u.setName(String.valueOf(v));
                 }
                 //让掉当前线程的CPU时间片,正在运行的线程转变为就绪状态,重新竞争CPU调度
                 Thread.yield();
             }
         }
     }
     //静态内部类User同上
     //静态内部类ReadObjectThread 同上
 }
 ​

自定义写线程是通过标记位来判断是否终止循环,标记位被置位就跳出循环,并释放锁资源结束线程。严格保证锁资源释放后再退出线程,简单高效地避免了数据不一致性!

JDK的中断机制

很多人比较莫名奇妙,这个Part不是再聊如何终止线程嘛?怎么聊到中断?但在解答这个问题之前,需要大家去思考这么一个问题: 被中断的线程到底处于什么状态?

想必回答不是 TERMINALED,就是WAITING。那么真正结果是什么呢?

"没有任何语言方面的需求要求一个被中断的程序应该终止。中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断 "----- Java核心卷

也就意味着被中断的线程,可转变为TERMINALED/WAITING/TIME_WAITING/BOCKED状态中的任何一种!

所以,我们可以再此利用中断机制来实现一个安全的终止线程方式。再使用之前,还是先介绍一下中断机制相关API:

  • public void Thread.interrupt(): 中断线程.
  • public boolean Thread.isInterrupted(): 判断线程是否被中断.
  • public static boolean Thread.interrupted():判断线程是否中断,并清除中断状态.

其实这里利用中断机制结束线程和上述自定义安全结束线程的方式并无太大区别,都是利用标志位实现的。

 public class InterruptStopThread {
     //模拟用户
     public static InterruptStopThread.User u = new InterruptStopThread.User();
     public static void main(String[] args) throws InterruptedException {
         //创建写线程
         new InterruptStopThread.ReadObjectThread().start();
         while (true) {
             //间隔200ms 启动写线程并关闭写线程
             ChangeObjectThread changeObjectThread = new ChangeObjectThread();
             changeObjectThread.start();
             Thread.sleep(200);
             changeObjectThread.interrupt();
 ​
         }
     }
     //中断机制实现安全的终止线程
     public static class ChangeObjectThread extends Thread {
         @Override
         public void run() {
             while (true) {
                 //判断线程是否被中断并
                 if (Thread.interrupted()) {
                     System.out.println("Thread exit.");
                     break;
                 }
                 synchronized (u) {
                     int v = (int) (System.currentTimeMillis() / 1000);
                     //修改ID
                     u.setId(v);
                     try {
                         Thread.sleep(100);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     //修改name
                     u.setName(String.valueOf(v));
                 }
                 //让掉当前线程的CPU时间片,正在运行的线程转变为就绪状态,重新竞争CPU调度
                 Thread.yield();
             }
         }
     }
     //静态内部类User同上
     //静态内部类ReadObjectThread同上
 }

但实际执行过程中将会抛出InterruptException异常,各位不要着急下定论,实际这正式中断机制不同于自定义标志位的优势所在!

假如线程处于睡眠(TIME_WAITING)状态, 自定义标志位方式必须等待线程从睡眠(TIME_WAITING)状态转为RUNNABLE后才能终止线程,但对处于WAITING/TIME_WAITING状态的线程就无能为力了。也正是自定义标志位中断线程的弊端,但对于中断机制就不同了。对处于WAITING/TIME_WAITIN状态的线程被中断后,将抛出异常InnterruptException异常,使用者自己定义处理方式即可。和自定义标志位方式,高级得不是一点半点,对吧!

该书作者也提供了很好中断机制使用示例:

     public static void SleepThreadInterrupt() throws InterruptedException {
         Thread t1 = new Thread() {
             @Override
             public void run() {
                 while (true) {
                     //判断线程是否被中断,被中断则退出循环并结束线程
                     if (Thread.currentThread().isInterrupted()) {
                         System.out.println("Interrupted");
                         break;
                     }
                     try {
                         Thread.sleep(2000);
                     } catch (InterruptedException e) {
                         System.out.println("Interrupted When Sleep");
                         //清除中断状态
                         Thread.currentThread().interrupt();
                     }
                     Thread.yield();
                 }
             }
         };
         //启动线程
         t1.start();
         //睡眠1s
         Thread.sleep(1000);
         //中断线程
         t1.interrupt();
     }

等待与唤醒

等待与唤醒主要包含RunnableWAIT/TIME_WAITING这两种线程状态之间的扭转。

Object.wait()和Object.notify()

Object类中的wait()notify() 方法?并没有搞错,Object类中确确实实是包含两方法的:

 //Object 类
 public class Object {
     //...
      public final void wait() throws InterruptedException {
          //...
      }
      public final native void notify();
 }

“当在一对象实例上调用wait()方法后,该线程将会在这个对象上等待,直到其他线程调用该对象的notify()方法为止"。很自然的是,线程等待与线程唤醒必然不可能发生在统一线程中,那么就意味着存在对象实例作为线程等待和唤醒的媒介。那么JDK官方团队认为所有对象的实例都可以是这个媒介,因此所有类的父类Object就自然而然包含了这两个方法。

wait()notify()方法到底是如何工作的呢?

小巴第一想法是在Object源码中寻找答案:

 public class Object {
     public final void wait() throws InterruptedException {
         wait(0);
     }
     //本地方法
     public final native void wait(long timeout) throws InterruptedException;
     //本地方法
     public final native void notify();
 }

然鹅源码调用了本地方法,该方法对应着JDK中的C++源码,小白这里能力有限就不介绍了!(日后再补充)

《Java高并发程序设计》作者在书中也解释了,就直接看作者是如何解释的吧:

当一个线程调用了object.wait(),那么该线程就进入了object对象的等待队列。

线程进入对象等待队列

当其他线程调用object.notify()时,将会从该队列中随机选择一个线程唤醒。注意:随机不意味着公平,因为并不是先等待的线程先被唤醒。

线程被随机唤醒

除此之外,Object类中还包含notify()方法,本方法被调用后将唤醒等待队列中的所有等待线程。

唤醒所有线程

注意:Object.wait()/Object.notify()/Object.notifyAll()方法均需要在同步代码块内调用,即synchronzied语句中。

下面简单示例演示一下如何使用wait()notify()

 public class SimpleWaitNotify {
     final static Object object = new Object();
     //wait()线程
     public static class T1 extends Thread {
         @Override
         public void run() {
             synchronized (object) {
                 try {
                     //本线程置入object对象等待队列
                     object.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         }
     }
     
     public static class T2 extends Thread {
         @Override
         public void run() {
             synchronized (object) {  
                 //唤醒object队列中随机一线程
                 object.notify();
                 try {
                     Thread.sleep(2000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         }
     }
     public static void main(String[] args) {
         Thread t1 = new T1();
         Thread t2 = new T2();
         t1.start();
         t2.start();
     }
 } 
 //执行结果
 1627226573605:T1 start!
 1627226573606:T1 wait for object
 1627226573606:T2 start!Notify One Thread
 1627226573606:T2 end!
 1627226575613:T1 end!

上面演示了不带参数的Thread.wait()方法,但实际上Thread.wait()方法还有包含超时时间的重载方法。Thread.wait(long timeout)Thread.sleep()方法一样都可以让线程等待若干时间。但wait(long timeout)方法不同于sleep()方法的是, wait()方法会释放目标对象的锁。

那么我们来梳理一下上面实例的执行过程:

T1线程开启后, T1线程获取到锁object后开始执行同步代码块。注意:当执行到object.wait()语句时,先取得object的监视器,然后使本线程从RUNNABLE状态转为WAITING状态,最后释放object的监视器并释放锁object 。之后便是T2线程的执行过程,就不再重复描述了:

wait和notify资源获取与释放

关于object 监视器可以简单理解为object队列,但实际的object监视器并不真的仅仅是队列哟!对于正常的监视器这部分的知识,小白也不大了解就日后再逐步深入!

挂起与继续执行

Thread.suspend()Thread.resume()API的功能分别将线程挂起和继续执行,似乎同Thread.stop()一样可给线程控制带来便捷操作,但实际上同Thread.stop()方法一样被弃用。而这当中核心问题就是线程被暂停的同时并不会释放任何锁资源。我们知道如果不释放线程锁资源极易导致死锁问题,这无疑增加了多线程开发的负担。

除此之外,由于指令重排导致resume()操作先于suspend()之前执行时,被挂起的线程将几乎不会再执行,并且不释放所占有的锁,从而极易导致系统处于死锁状态。 更重要的是,利用jstack查看线程状态时会发现线程居然处于RUNNABLE状态

 public class BadSuspend {
     public static Object u = new Object();
     static ChangeObjectThread t1 = new ChangeObjectThread("t1");
     static ChangeObjectThread t2 = new ChangeObjectThread("t2");
     public static class ChangeObjectThread extends Thread {
         public ChangeObjectThread(String name) {
             super.setName(name);
         }
         @Override
         public void run() {
             synchronized (u) {
                 System.out.println(" in" + getName());
                 //线程挂起
                 Thread.currentThread().suspend();
                 System.out.println();
             }
         }
     }
     public static void main(String[] args) throws InterruptedException {
         t1.start();
         Thread.sleep(1000);
         t2.start();
         t1.resume();
         //t2.resume() 方法在t2线程中断前执行可能导致线程下不停止问题
         t2.resume();
         t1.join();
         t2.join();
     }
 } 

利用jps/jstack查看Java进程PID以及线程状态:

 D:\WSharkCoder\WorkSpace\IdeaProjects\DeepLearn-Concurrent>jps
 12528 BadSuspend
 12036 Jps
 15716 Launcher
 2548
 D:\WSharkCoder\WorkSpace\IdeaProjects\DeepLearn-Concurrent>jstack -l 12528
 2021-08-08 15:32:46
 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.281-b09 mixed mode):
 ​
 "t2" #13 prio=5 os_prio=0 tid=0x000001d934518000 nid=0x2468 runnable [0x00000089e9eff000]
    java.lang.Thread.State: RUNNABLE
         at java.lang.Thread.suspend0(Native Method)
         at java.lang.Thread.suspend(Thread.java:1032)
         at base.suspendresume.BadSuspend$ChangeObjectThread.run(BadSuspend.java:21)
         - locked <0x000000076b99d138> (a java.lang.Object)
 ​

试想在项目中,巨量线程同时执行的过程中,靠着这样的线程信息提示可能得花上三五天的功夫才能对这个问题进行修复!

那么怎么实现安全的线程挂起和继续执行呢?等待和唤醒API就能实现挂起和继续执行,代码实现部分小白这里就不重复了,操作基本和上述的等待和唤醒一致。

协作/谦让

Thread.join()

协作作为日常生活人类最最最常见合作行为没有之一!多线程本身就是为增强对某些任务的处理,那么必然存在一些线程必须等待其他线程执行完其结果在处理数据的情况。那么,就必须得有线程间和合作。

线程协作常见API具有:

  • public final void join() throw InterrupedException 线程无限等待,直到目标线程执行完毕
  • public final void join(long millis) throw InterrupedException线程等待至最大时间millis,目标线程执行完毕或超出最大等待时间均可继续执行

下面演示利用子线程计算数值,父线程等待计算完毕后再输出结果集的示例:

 public class JoinThread {
     public volatile static int i = 0;
     public static class AddThread extends Thread {
         @Override
         public void run() {
             int target = 1000000000;
             while (i < target) {
                 i++;
             }
         }
     }
     public static void main(String[] args) throws InterruptedException {
         Thread t = new AddThread();
         t.start();
         //等待线程执行完毕再输出结果值
         t.join();
         System.out.println(i);
 ​
     }
 ​
 }

Thread.yeild()

相信很多仔细的小伙伴前面已经见过这个API很多遍,那么yeild()的功能又是什么呢?

yeild()主要功能是让出CPU,重新同其他线程争夺CPU资源。当某线程主任务执行完毕后就可以调用此API,给其他更大执行概率。

注意:其他线程不一定会立即执行,具体哪个线程被执行通常都是随机的。

Github代码仓库:github.com/WSharkCoder…

总结

多线程相关的知识真的相当多,白鲨这里记录的也是很少的一部分仅包括线程状态扭转与线程API,其实作者在该章节内还聊了volatile和JMM,sychronized,线程组以及线程下的集合框架等知识。关于这部分知识需要更加详细地深挖,估计已经和本篇内容差不多长,就之后再补充知识。各位看官如果觉得白鲨的博客对您有帮助的话,欢迎点赞收藏评论!

参考

《Java高并发程序设计》

Java线程同步机制