乐趣区

关于java-ee:Java并发编程线程基础

1. 线程的创立

首先咱们来温习咱们学习 java 时接触的线程创立,这也是面试的时候喜爱问的,有人说两种也有人说三种四种等等,其实咱们不能去死记硬背,而应该深刻了解其中的原理,当咱们了解后就会发现所谓的创立线程本质都是一样的,在咱们面试的过程中如果咱们能从实质登程答复这样的问题,那么置信肯定是个加分项!好了咱们不多说了,开始明天的 code 之路

1.1 继承 Thread 类创立线程

**

  • 这是咱们最常见的创立线程的形式,通过继承 Thread 类来重写 run 办法,

代码如下:


/**
 * 线程类
 * url: www.i-code.online
 * @author: anonyStar
 * @time: 2020/9/24 18:55
 */
public class ThreadDemo extends Thread {
    @Override
    public void run() {
        // 线程执行内容
        while (true){
            try {TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            System.out.println("ThredDemo 线程正在执行, 线程名:"+ Thread.currentThread().getName());
        }
    }
}

测试方法:

    @Test
    public void thread01(){Thread thread = new ThreadDemo();
        thread.setName("线程 -1");
        thread.start();

        while (true){System.out.println("这是 main 主线程:" + Thread.currentThread().getName());
            try {TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }

    }

后果:

继承 Thread 的线程创立简略,启动时间接调用 start 办法,而不是间接调用 run 办法。间接调用 run 等于调用一般办法,并不是启动线程

1.2 实现 Runnable 接口创立线程

**

  • 上述形式咱们是通过继承来实现的,那么在 java 中提供了 Runnable 接口,咱们能够间接实现该接口,实现其中的 run 办法,这种形式可扩展性更高

代码如下:


/**
 * url: www.i-code.online
 * @author: anonyStar
 * @time: 2020/9/24 18:55
 */
public class RunnableDemo implements Runnable {
 
    @Override
    public void run() {
        // 线程执行内容
        while (true){
            try {TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            System.out.println("RunnableDemo 线程正在执行, 线程名:"+ Thread.currentThread().getName());
        }
    }
}

测试代码:

    @Test
    public void runnableTest(){
        // 实质还是 Thread,这里间接 new Thread 类,传入 Runnable 实现类
        Thread thread = new Thread(new RunnableDemo(),"runnable 子线程 - 1");
        // 启动线程
        thread.start();

        while (true){System.out.println("这是 main 主线程:" + Thread.currentThread().getName());
            try {TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }
    }

运行后果:

1.3 实现 Callable 接口创立线程

  • 这种形式是通过 实现 Callable 接口,实现其中的 call 办法来实现线程,然而这种线程创立的形式是依赖于 **FutureTask **包装器 来创立 Thread , 具体来看代码

代码如下:


/**
 * url: www.i-code.online
 * @author: anonyStar
 * @time: 2020/9/24 18:55
 */
public class CallableDemo implements Callable<String> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    @Override
    public String call() throws Exception {
        // 线程执行内容
        try {TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {e.printStackTrace();
        }
        System.out.println("CallableDemo 线程正在执行, 线程名:"+ Thread.currentThread().getName());

        return "CallableDemo 执行完结。。。。";
    }
}

测试代码:

    @Test
    public void callable() throws ExecutionException, InterruptedException {
        // 创立线程池
        ExecutorService service = Executors.newFixedThreadPool(1);
        // 传入 Callable 实现同时启动线程
        Future submit = service.submit(new CallableDemo());
        // 获取线程内容的返回值,便于后续逻辑
        System.out.println(submit.get());
        // 敞开线程池
        service.shutdown();
        // 主线程
        System.out.println("这是 main 主线程:" + Thread.currentThread().getName());
        try {TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }

后果:

有的时候,咱们可能须要让一步执行的线程在执行实现当前,提供一个返回值给到以后的主线程,主线程须要依赖这个值进行后续的逻辑解决,那么这个时候,就须要用到带返回值的线程了

对于线程基础知识的如果有什么问题的能够在网上查找材料学习学习!这里不再论述

2. 线程的生命周期

  • Java 线程既然可能创立,那么也势必会被销毁,所以线程是存在生命周期的,那么咱们接下来从线程的生命周期开始去理解线程。

2.1 线程的状态

2.1.1 线程六状态意识

线程一共有 6 种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED)

  • NEW:初始状态,线程被构建,然而还没有调用 start 办法
  • RUNNABLED:运行状态,JAVA 线程把操作系统中的就绪和运行两种状态对立称为“运行中”
  • BLOCKED:阻塞状态,示意线程进入期待状态, 也就是线程因为某种原因放弃了 CPU 使用权,阻塞也分为几种状况

    • 期待阻塞:运行的线程执行 wait 办法,jvm 会把以后线程放入到期待队列➢ 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其余线程锁占用了,那么 jvm 会把以后的线程放入到锁池中
    • 其余阻塞:运行的线程执行 Thread.sleep 或者 t.join 办法,或者收回了 I/O 申请时,JVM 会把以后线程设置为阻塞状态,当 sleep 完结、join 线程终止、io 处理完毕则线程复原
  • TIME_WAITING:超时期待状态,超时当前主动返回
  • TERMINATED:终止状态,示意以后线程执行结束

2.1.2 代码实操演示

  • 代码:

    public static void main(String[] args) {
        ////TIME_WAITING 通过 sleep wait(time)来进入期待超时中
        new Thread(() -> {while (true){
               // 线程执行内容
               try {TimeUnit.SECONDS.sleep(100);
               } catch (InterruptedException e) {e.printStackTrace();
               }
           }
        },"Time_Waiting").start();
        //WAITING,线程在 ThreadStatus 类锁上通过 wait 进行期待
        new Thread(() -> {while (true){synchronized (ThreadStatus.class){
                    try {ThreadStatus.class.wait();
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
            }
        },"Thread_Waiting").start();

        //synchronized 取得锁,则另一个进入阻塞状态 blocked
        new Thread(() -> {while (true){synchronized(Object.class){
                    try {TimeUnit.SECONDS.sleep(100);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
            }
        },"Object_blocked_1").start();
        new Thread(() -> {while (true){synchronized(Object.class){
                    try {TimeUnit.SECONDS.sleep(100);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
            }
        },"Object_blocked_2").start();}

启动一个线程前,最好为这个线程设置线程名称,因为这样在应用 jstack 分析程序或者进行问题排查时,就会给开发人员提供一些提醒

2.1.3 线程的状态堆栈

➢ 运行该示例,关上终端或者命令提示符,键入“jps”,(JDK1.5 提供的一个显示以后所有 java 过程 pid 的命令)

➢ 依据上一步骤取得的 pid,持续输出 jstack pid(jstack 是 java 虚拟机自带的一种堆栈跟踪工具。jstack 用于打印出给定的 java 过程 ID core file 或近程调试服务的 Java 堆栈信息)

3. 线程的深刻解析

3.1 线程的启动原理

  • 后面咱们通过一些案例演示了线程的启动,也就是调用 start() 办法去启动一个线程,当 run 办法中的代码执行结束当前,线程的生命周期也将终止。调用 start 办法的语义是以后线程通知 JVM,启动调用 start 办法的线程。
  • 咱们开始学习线程时很大的纳闷就是 启动一个线程是应用 start 办法,而不是间接调用 run 办法,这里咱们首先简略看一下 start 办法的定义,在 Thread 类中
    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            // 线程调用的外围办法,这是一个本地办法,native 
            start0();
            started = true;
        } finally {
            try {if (!started) {group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
    
    // 线程调用的 native 办法
    private native void start0();
  • 这里咱们能看到 start 办法中调用了 native 办法 start0来启动线程,这个办法是在 Thread 类中的动态代码块中注册的 , 这里间接调用了一个 native 办法 registerNatives
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {registerNatives();
    }
  • 因为 registerNatives 办法是本地办法,咱们要看其实现源码则必须去下载 jdk 源码,对于 jdk 及虚拟机 hotspot 的源码下载能够去 openJDK 官网下载,参考:
  • 咱们能够本地查看源码或者间接去 http://hg.openjdk.java.net/jdk8u/jdk8u60/jdk/file/935758609767/src/share/native/java/lang/Thread.c 查看 Thread 类对应的本地办法 .c 文件,

  • 如上图,咱们本地下载 jdk 工程,找到 src->share->native->java->lang->Thread.c 文件

  • 下面是 Thread.c 中所有代码,咱们能够看到调用了 RegisterNatives 同时能够看到 method 汇合中的映射,在调用本地办法 start0 时,理论调用了 JVM_StartThread,它本身是由 c/c++ 实现的,这里须要在 虚拟机源码中去查看,咱们应用的都是 hostpot 虚拟机,这个能够去 openJDK 官网下载,上述介绍了不再多说
  • 咱们看到 JVM_StartThread 的定义是在 jvm.h 源码中,而 jvm.h 的实现则在虚拟机 hotspot 中,咱们关上 hotspot 源码,找到 src -> share -> vm -> prims ->jvm.cpp 文件,在 2955 行,能够间接检索 JVM_StartThread , 办法代码如下:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;

  bool throw_illegal_thread_state = false;

  {MutexLocker mu(Threads_lock);

    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {throw_illegal_thread_state = true;} else {
      // We could also check the stillborn flag to see if this thread was already stopped, but
      // for historical reasons we let the thread detect that itself when it starts running
      // <1> : 获取以后过程中线程的数量
      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));

      size_t sz = size > 0 ? (size_t) size : 0;

      // <2> : 真正调用创立线程的办法
      native_thread = new JavaThread(&thread_entry, sz);
      if (native_thread->osthread() != NULL) {
        // Note: the current thread is not being used within "prepare".
        native_thread->prepare(jthread);
      }
    }
  }

  if (throw_illegal_thread_state) {THROW(vmSymbols::java_lang_IllegalThreadStateException());
  }

  assert(native_thread != NULL, "Starting null thread?");

  if (native_thread->osthread() == NULL) {
    // No one should hold a reference to the 'native_thread'.
    delete native_thread;
    if (JvmtiExport::should_post_resource_exhausted()) {
      JvmtiExport::post_resource_exhausted(
        JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS,
        "unable to create new native thread");
    }
    THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
              "unable to create new native thread");
  }

  // <3> 启动线程
  Thread::start(native_thread);

JVM_END

JVM_ENTRY 是用来定义 JVM_StartThread 函数的,在这个函数外面创立了一个真正和平台无关的本地线程, 上述标记 <2> 处

  • 为了进一步线程创立,咱们在进入 new JavaThread(&thread_entry, sz) 中查看一下具体实现过程,在 thread.cpp 文件 1566 行处定义了 new 的办法

  • 对于上述代码咱们能够看到最终调用了 os::create_thread(this, thr_type, stack_sz); 来实现线程的创立,对于这个办法不同平台有不同的实现,这里不再赘述,

  • 下面都是创立过程,之后再调用   Thread::start(native_thread); 在 JVM_StartThread 中调用,该办法的实现在 Thread.cpp

start 办法中有一个函数调用:os::start_thread(thread);,调用平台启动线程的办法,最终会调用 Thread.cpp 文件中的 JavaThread::run() 办法

3.2 线程的终止

3.2.1 通过标记位来终止线程

  • 失常咱们线程内的货色都是循环执行的,那么咱们理论需要中必定也存在想在其余线程来进行以后线程的须要,这是后咱们能够通过标记位来实现,所谓的标记为其实就是 volatile 润饰的变量,着由它的可见性个性决定的,如下代码就是根据 volatile 来实现标记位进行线程

    // 定义标记为 应用 volatile 润饰
    private static volatile  boolean mark = false;

    @Test
    public void markTest(){new Thread(() -> {
            // 判断标记位来确定是否持续进行
            while (!mark){
                try {TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println("线程执行内容中...");
            }
        }).start();

        System.out.println("这是主线程走起...");
        try {TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {e.printStackTrace();
        }
        //10 秒后将标记为设置 true 对线程可见。用 volatile 润饰
        mark = true;
        System.out.println("标记位批改为:"+mark);
    }

3.2.2 通过 stop 来终止线程

  • 咱们通过查看 Thread 类或者 JDK API 能够看到对于线程的进行提供了 stop() , supend() , resume() 等办法,然而咱们能够看到这些办法都被标记了 @Deprecated 也就是过期的,
  • 尽管这几个办法都能够用来进行一个正在运行的线程,然而这些办法都是不平安的,都曾经被摈弃应用,所以在咱们开发中咱们要防止应用这些办法,对于这些办法为什么被摈弃以及导致的问题 JDK 文档中较为具体的形容《Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?》
  • 在其中有这样的形容:

  • 总的来说就是:

    • 调用 stop() 办法会立即进行 run() 办法中残余的全副工作,包含在 catchfinally 等语句中的内容,并抛出 ThreadDeath 异样(通常状况下此异样不须要显示的捕捉),因而可能会导致一些工作的得不到实现,如文件,数据库等的敞开。
    • 调用 stop() 办法会立刻开释该线程所持有的所有的锁,导致数据得不到同步,呈现数据不统一的问题。

3.2.3 通过 interrupt 来终止线程

  • 通过下面论述,咱们晓得了应用 stop 办法是不举荐的,那么咱们用什么来更好的进行线程,这里就引出了 interrupt 办法,咱们通过调用 interrupt 来中断线程
  • 当其余线程通过调用以后线程的 interrupt 办法,示意向以后线程打个招呼,通知他能够中断线程的执行了,至于什么时候中断,取决于以后线程本人
  • 线程通过查看本身是否被中断来进行相应,能够通过 isInterrupted() 来判断是否被中断。

咱们来看上面代码:

    public static void main(String[] args) {
        // 创立 interrupt-1 线程

        Thread thread = new Thread(() -> {while (true) {
                // 判断以后线程是否中断,if (Thread.currentThread().isInterrupted()) {System.out.println("线程 1 接管到中断信息,中断线程...");
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "线程正在执行...");

            }
        }, "interrupt-1");
        // 启动线程 1
        thread.start();

        // 创立 interrupt-2 线程
        new Thread(() -> {
            int i = 0;
            while (i <20){System.out.println(Thread.currentThread().getName()+"线程正在执行...");
                if (i == 8){System.out.println("设置线程中断....");
                    // 告诉线程 1 设置中断告诉
                    thread.interrupt();}
                i ++;
                try {TimeUnit.MILLISECONDS.sleep(1);
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
        },"interrupt-2").start();}

打印后果如下:

上述代码中咱们能够看到,咱们创立了 interrupt-1 线程,其中用 interrupt 来判断以后线程是否处于中断状态,如果处于中断状态那么就天然完结线程,这里的完结的具体操作由咱们开发者来决定。再创立 interrupt-2 线程,代码绝对简略不论述,当执行到某时刻时将线程 interrupt-1 设置为中断状态,也就是告诉 interrupt-1 线程。

线程中断标记复位

在上述 interrupt-1 代码中如果退出 sleep 办法,那么咱们会发现程序报出 InterruptedException 谬误,同时,线程 interrupt-1 也不会进行,这里就是因为中断标记被复位了,上面咱们来介绍一下对于中断标记复位相干的内容

  • 在线程类中提供了 Thread.interrupted 的静态方法,用来对线程中断标识的复位,在下面的代码中,咱们能够做一个小改变,对 interrupt-1 线程创立的代码批改如下:
        // 创立 interrupt-1 线程

        Thread thread = new Thread(() -> {while (true) {
                // 判断以后线程是否中断,if (Thread.currentThread().isInterrupted()) {System.out.println("线程 1 接管到中断信息,中断线程... 中断标记:" + Thread.currentThread().isInterrupted());
                    Thread.interrupted(); // // 对线程进行复位,由 true 变成 false
                    System.out.println("通过 Thread.interrupted() 复位后,中断标记:" + Thread.currentThread().isInterrupted());
                    // 再次判断是否中断,如果是则退出线程
                    if (Thread.currentThread().isInterrupted()) {break;}
                }
                System.out.println(Thread.currentThread().getName() + "线程正在执行...");

            }
        }, "interrupt-1");

上述代码中 咱们能够看到,判断以后线程是否处于中断标记为 true , 如果有其余程序告诉则为 true 此时进入 if 语句中,对其进行复位操作,之后再次判断。执行代码后咱们发现 interrupt-1 线程不会终止,而会始终执行

  • Thread.interrupted 进行线程中断标记复位是一种被动的操作行为,其实还有一种被动的复位场景,那就是下面说的当程序呈现 InterruptedException 异样时,则会将以后线程的中断标记状态复位,在抛出异样前,JVM 会将中断标记 isInterrupted 设置为 false

在程序中,线程中断复位的存在理论就是以后线程对外界中断告诉信号的一种响应,然而具体响应的内容有以后线程决定,线程不会立马进行,具体是否进行等都是由以后线程本人来决定,也就是开发者。

3.3 线程终止 interrupt 的原理

  • 首先咱们先来看一下在 Thread 中对于 interrupt 的定义:
    public void interrupt() {if (this != Thread.currentThread()) {checkAccess();  // 校验是否有权限来批改以后线程

            // thread may be blocked in an I/O operation
            synchronized (blockerLock) {
                Interruptible b = blocker;
                if (b != null) {
                    // <1> 调用 native 办法
                    interrupt0();  // set interrupt status
                    b.interrupt(this);
                    return;
                }
            }
        }

        // set interrupt status
        interrupt0();}
  • 下面代码中咱们能够看到,在 interrupt 办法中最终调用了 Native 办法 interrupt0,这里相干在线程启动时说过,不再赘述,咱们间接找到 hotspotjvm.cpp 文件中 JVM_Interrupt 办法

  • JVM_Interrupt 办法比较简单,其中咱们能够看到间接调用了 Thread.cppinterrupt 办法,咱们进入其中查看

  • 咱们能够看到这里间接调用了  os::interrupt(thread) 这里是调用了平台的办法,对于不同的平台实现是不同的,咱们这里如下所示,抉择 Linux 下的实现 os_linux.cpp 中,

在下面代码中咱们能够看到,在 1 处拿到 OSThread,之后判断如果 interruptfalse 则在 2 处调用 OSThreadset_interrupted 办法进行设置,咱们能够进入看一下其实现,发现在 osThread.hpp 中定义了一个成员变量 volatile jint _interrupted;set_interrupted 办法其实就是将 _interrupted 设置为 true,之后再通过 ParkEventunpark() 办法来唤醒线程。具体的过程在下面进行的简略的正文介绍,

本文由 AnonyStar 公布, 可转载但需申明原文出处。
企慕「优雅编码的艺术」深信游刃有余,致力扭转人生
欢送关注微信公账号:云栖简码 获取更多优质文章
更多文章关注笔者博客:云栖简码

退出移动版