[03][01][01] 并发编程的基本认识

进程的概念

进程的本质是一个正在执行的程序,程序运行时系统会创建一个进程,并且给每个进程分配独立的内存地址空间保证每个进程地址不会相互干扰。同时,在 CPU 对进程做时间片的切换时,保证进程切换过程中仍然要从进程切换之前运行的位置出开始执行。所以进程通常还会包括程序计数器、堆栈指针,如果内存中可以同时存放足够多的程序,那 CPU 的利用率可以接近 100%

线程的概念

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务

线程解决的问题

  • 在多核 CPU 中,利用多线程可以实现真正意义上的并行执行
  • 在一个进程中,会存在多个同时执行的任务,如果其中一个任务被阻塞,将会引起不依赖该任务的任务也被阻塞。通过对不同任务创建不同的线程去处理,可以提升程序处理的实时性
  • 线程可以认为是轻量级的进程,所以线程的创建、销毁比进程更快

线程的创建

  • 继承 Thread 类
  • 实现 Runnable 接口
  • Callable/Future带返回值的线程
  • ThreadPool

线程可以合理的利用多核心 CPU 资源,提高程序的吞吐量

继承 Thread 类

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

public class ThreadDemo extends Thread {

    @Override
    public void run() {
        int sum = 0;

        for (int i = 0; i <= 100; i++) {
            sum += i;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("计算结果为" + sum);
    }

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();

        threadDemo.start();
        System.out.println("启动线程" + threadDemo.getClass().getSimpleName());
    }
}

实现 Runnable 接口

如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个 Runnable 接口

public class RunnableDemo implements Runnable{

    @Override
    public void run() {
        int sum = 0;

        for (int i = 0; i <= 100; i++) {
            sum += i;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("计算结果为" + sum);
    }

    public static void main(String[] args) {
        RunnableDemo runnableDemo = new RunnableDemo();

        new Thread(runnableDemo).start();
        System.out.println("启动线程" + runnableDemo.getClass().getSimpleName());
    }
}

Callable/Future带返回值的线程

有业务场景需要让一步执行的线程在执行完成以后,提供一个返回值给到当前的主线程,主线程需要依赖这个值进行后续的逻辑处理,那么这个时候,就需要用到带返回值的线程了。Java 中提供了这样的实现方式

public class CallableDemo implements Callable<String> {
    /**
     * 处理业务流程
     */
    @Override
    public String call() throws Exception {
        int a = 2;

        int b = 2;

        System.out.println(a + b);
        return "执行结果:" + (a + b);
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);

        CallableDemo callableDemo = new CallableDemo();

        Future<String> future = executorService.submit(callableDemo);

        System.out.println(future.get());
        executorService.shutdown();
    }
}

ThreadPool


多线程的实际应用

在工作中应该很少有场景能够应用多线程了,因为基于业务开发来说,很多使用异步的场景我们都通过分布式消息队列来做了。但并不是说多线程就不会被用到,你们如果有看一些框架的源码,会发现线程的使用无处不在

在做文件跑批场景中,每天会有一些比如收益文件、对账文件,我们会有一个定时任务去拿到数据然后通过线程去处理

zookeeper 源码的异步责任链模式

时序图

外界请求PrevProcessorSaveProcessorPrintProcessor处理request将请求做预处理将请求交给保存请求类处理保存请求将请求做打印处理打印请求外界请求PrevProcessorSaveProcessorPrintProcessor

代码实现

  • 请求类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Request {

    private String name;
}
  • 请求处理接口
/**
 * 责任链模式的请求处理接口
 */
public interface IRequestProcessor {

    void process(Request request);
}
  • 请求预处理类
/**
 * 预处理
 */
public class PrevProcessor extends Thread implements IRequestProcessor {
    //阻塞队列
    LinkedBlockingQueue<Request> requests = new LinkedBlockingQueue<>();

    private IRequestProcessor nextProcessor;

    private volatile boolean isFinish = false;

    public PrevProcessor() {
    }

    public PrevProcessor(IRequestProcessor nextProcessor) {
        this.nextProcessor = nextProcessor;
    }

    public void shutdown() { //对外提供关闭的方法
        isFinish = true;
    }

    @Override
    public void run() {
        //不建议这么写
        while (!isFinish) {
            try {
                //阻塞式获取数据
                Request request = requests.take();
                //真正的处理逻辑
                System.out.println("prevProcessor:" + request);
                //交给下一个责任链
                if (nextProcessor != null) {
                    nextProcessor.process(request);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void process(Request request) {
        //TODO 根据实际需求去做一些处理
        requests.add(request);
    }
}
  • 保存处理类
/**
 * 保存处理
 */
public class SaveProcessor extends Thread implements IRequestProcessor {
    //阻塞队列
    LinkedBlockingQueue<Request> requests = new LinkedBlockingQueue<>();

    private IRequestProcessor nextProcessor;

    private volatile boolean isFinish = false;

    public SaveProcessor() {
    }

    public SaveProcessor(IRequestProcessor nextProcessor) {
        this.nextProcessor = nextProcessor;
    }

    /**
     * 对外提供关闭的方法
     */
    public void shutdown() {
        isFinish = true;
    }

    @Override
    public void run() {
        //不建议这么写
        while (!isFinish) {
            try {
                //阻塞式获取数据
                Request request = requests.take();
                //真正的处理逻辑; store to mysql 。
                System.out.println("SaveProcessor:" + request);
                //交给下一个责任链
                if (nextProcessor != null) {
                    nextProcessor.process(request);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void process(Request request) {
        //TODO 根据实际需求去做一些处理
        requests.add(request);
    }
}
  • 打印处理类
/**
 * 打印处理
 */
public class PrintProcessor extends Thread implements IRequestProcessor {
    //阻塞队列
    LinkedBlockingQueue<Request> requests = new LinkedBlockingQueue<>();

    private IRequestProcessor nextProcessor;

    private volatile boolean isFinish = false;

    public PrintProcessor() {
    }

    public PrintProcessor(IRequestProcessor nextProcessor) {
        this.nextProcessor = nextProcessor;
    }

    /**
     * 对外提供关闭的方法
     */
    public void shutdown() {
        isFinish = true;
    }

    @Override

    public void run() {
        //不建议这么写
        while (!isFinish) {
            try {
                //阻塞式获取数据
                //消费者
                Request request = requests.take();
                //真正的处理逻辑
                System.out.println("PrintProcessor:" + request);
                //交给下一个责任链
                if (nextProcessor != null) {
                    nextProcessor.process(request);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void process(Request request) {
        //TODO 根据实际需求去做一些处理
        requests.add(request); //生产者
    }
}
  • 测试类
/**
 * 请求测试类
 */
public class ProcessorDemo {

    static IRequestProcessor requestProcessor;

    public void setUp() {
        PrintProcessor printProcessor = new PrintProcessor();
        printProcessor.start();

        SaveProcessor saveProcessor = new SaveProcessor(printProcessor);
        saveProcessor.start();

        requestProcessor = new PrevProcessor(saveProcessor);
        ((PrevProcessor) requestProcessor).start();
    }


    public static void main(String[] args) throws InterruptedException {
        ProcessorDemo processorDemo = new ProcessorDemo();
        processorDemo.setUp();

        Request request = new Request();
        request.setName("责任链请求测试");
        requestProcessor.process(request);

        Thread.sleep(2000L);
        System.out.println("2秒后发起新的请求");

        requestProcessor.process(new Request("请求 11"));
    }
}

输出结果
prevProcessor:Request(name=责任链请求测试)
SaveProcessor:Request(name=责任链请求测试)
PrintProcessor:Request(name=责任链请求测试)
2秒后发起新的请求
prevProcessor:Request(name=请求 11)
SaveProcessor:Request(name=请求 11)
PrintProcessor:Request(name=请求 11)

线程的生命周期

查看 Thread 类的源码,存在枚举内部类 State,定义了线程生命周期的六个状态

  • NEW
  • RUNNABLE
  • BLOCKED
  • WAITING
  • TIMED_WAITING
  • TERMINATED

NEW:初始状态,线程被构建,但是还没有调用 start() 方法
RUNNABLED:运行状态,JAVA 线程把操作系统中的就绪和运行两种状态统一称为“运行中”
BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程,因为某种原因放弃了 CPU 使用权,阻塞也分为几种情况

  • 等待阻塞:运行的线程执行 wait 方法,jvm 会把当前线程放入到等待队列
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么 jvm 会把当前的线程放入到锁池中
  • 其他阻塞:运行的线程执行 Thread.sleep 或者 t.join 方法,或者发出了 I/O 请求时,JVM 会把当前线程设置为阻塞状态,当 sleep 结束、join 线程终止、io 处理完毕则线程恢复

TIME_WAITING:超时等待状态,超时以后自动返回
TERMINATED:终止状态,表示当前线程执行完毕

线程的生命周期

调试代码

通过代码来重现线程的相关状态

public class ThreadStatusDemo {

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Time_Waiting_Thread").start();

        new Thread(() -> {
            while (true) {
                synchronized (ThreadStatusDemo.class) {
                    try {
                        ThreadStatusDemo.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "Waiting_Thread").start();

        //BLOCKED
        new Thread(new BlockedDemo(), "Blocked01_Thread").start();
        new Thread(new BlockedDemo(), "Blocked02_Thread").start();
    }


    static class BlockedDemo extends Thread {
        @Override
        public void run() {
            synchronized (BlockedDemo.class) {
                while (true) {
                    try {
                        TimeUnit.SECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

使用 jdk 自带命令 jps 和 jstack 查看运行中线程的状态

Waiting_Thread 线程与 Time_Waiting_Thread 在代码中分别调用了 wait() 和 sleep(100)方法,它们之间不存在竞争锁,线程分别状态为 WAITING,TIMED_WAITING 状态

Blocked01_Thread 与 Blocked02_Thread 对 BlockedDemo 存在锁竞争,执行 sleep(100),导致两个线程一个是 TIMED_WAITING,另一个是 BLOCKED

代码调试查看线程状态

代码调试查看线程状态

线程的启动

在编写代码中,都是通过调用start()方法去启动一个线程,当 run() 方法中的代码执行完毕以后,线程的生命周期也将终止。调用 start() 方法的语义是当前线程告诉 JVM,启动调用 start 方法的线程

学习线程的时候会比较疑惑,启动一个线程为什么是调用 start() 方法,而不是 run() 方法,这做一个简单的分析,先简单看一下 start() 方法的定义

public class Thread implements Runnable {
    ......
    static {
        registerNatives();
        EMPTY_STACK_TRACE = new StackTraceElement[0];
        SUBCLASS_IMPLEMENTATION_PERMISSION = new RuntimePermission("enableContextClassLoaderOverride");
    }
    ......
    public synchronized void start() {
        if (this.threadStatus != 0) {
            throw new IllegalThreadStateException();
        } else {
            this.group.add(this);
            boolean var1 = false;

            try {
                // 此处是start()方法的核心代码
                this.start0();
                var1 = true;
            } finally {
                try {
                    if (!var1) {
                        this.group.threadStartFailed(this);
                    }
                } catch (Throwable var8) {
                }
            }
        }
    }
    ......
    private native void start0();
    ......
}

JDK8对应的 jdk 和 hotspot 源码地址:https://github.com/huaweirookie/toalibaba/tree/master/concurrent-programming/thread-basic/src/main/resources/jdk8

调用 start() 方法实际上是调用一个 native 方法 start0() 来启动一个线程,首先 start0() 这个方法是在Thread 的静态块中来注册的,registerNatives 的 本 地 方 法 的 定 义 在 文 件 Thread.c,Thread.c 定义了各个操作系统平台要用的关于线程的公共数据和操作,以下是 Thread.c 的全部内容

文件地址信息:jdk8/jdk/src/share/native/java/lang/Thread.c

static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
    {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
    {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
    {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
    {"resume0",          "()V",        (void *)&JVM_ResumeThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
    {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};

#undef THD
#undef OBJ
#undef STE
#undef STR

JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

从这段代码可以看出, start0() 实际会执行 JVM_StartThread 方法,从名字上来看,似乎是在 JVM 层面去启动一个线程,在 JVM 层面一定会调用 Java 中定义的 run() 方法。那接下来继续去找找答案。我们找到 jvm.cpp 这个文件,这个文件需要下载 hotspot 的源码才能找到

源码地址:jdk8/hotspot/src/share/vm/prims/jvm.cpp

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  ......
  native_thread = new JavaThread(&thread_entry, sz);
  ......
}

JVM_ENTRY 是用来定义 JVM_StartThread 函数的,在这个函数里面创建了一个真正和平台有关的本地线程,继续寻找 JavaThread 的定义在 hotspot 的源码中 thread.cpp 文件中 1558 行的位置可以找到如下代码

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
  Thread()
#if INCLUDE_ALL_GCS
  , _satb_mark_queue(&_satb_mark_queue_set),
  _dirty_card_queue(&_dirty_card_queue_set)
#endif // INCLUDE_ALL_GCS
{
  if (TraceThreadEvents) {
    tty->print_cr("creating thread %p", this);
  }
  initialize();
  _jni_attach_state = _not_attaching_via_jni;
  set_entry_point(entry_point);
  // Create the native thread itself.
  // %note runtime_23
  os::ThreadType thr_type = os::java_thread;
  thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
                                                     os::java_thread;
  os::create_thread(this, thr_type, stack_sz);
  _safepoint_visible = false;
}

这个方法有两个参数:
第一个是函数名称,线程创建成功之后会根据这个函数名称调用对应的函数
第二个是当前进程内已经有的线程数量
最后我们重点关注与一下 os::create_thread,实际就是调用平台创建线程的方法来创建线程

接下来就是线程的启动,会调用 Thread.cpp 文件中的 Thread::start(Thread* thread) 方法,代码如下

void Thread::start(Thread* thread) {
  trace("start", thread);
  // Start is different from resume in that its safety is guaranteed by context or
  // being called from a Java method synchronized on the Thread object.
  if (!DisableStartThread) {
    if (thread->is_Java_thread()) {
      // Initialize the thread state to RUNNABLE before starting this thread.
      // Can not set it after the thread started because we do not know the
      // exact thread state at that time. It could be in MONITOR_WAIT or
      // in SLEEPING or some other state.
      java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),
                                          java_lang_Thread::RUNNABLE);
    }
    os::start_thread(thread);
  }
}

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

线程的终止

线程的启动过程大家都非常熟悉,但是如何终止一个线程呢?

线程的终止,并不是简单的调用 stop 命令去。虽然 api 仍然可以调用,但是和其他的线程控制方法如 suspend、resume 一样都是过期了的不建议使用,就拿 stop 来说,stop 方法在结束一个线程时并不会保证线程的资源正常释放,因此会导致程序可能出现一些不确定的状态。要优雅的去中断一个线程,在线程中提供了一个 interrupt 方法

interrupt 方法

当其他线程通过调用当前线程的 interrupt 方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。线程通过检查自身是否被中断来进行相应,可以通过 isInterrupted()来判断是否被中断。通过下面这个例子,来实现了线程终止的逻辑

public class InterruptDemo {

    private static int i;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            //默认是false  _interrupted state
            while (!Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println("i:" + i);
        });
        thread.start();

        TimeUnit.SECONDS.sleep(1);
        //把isInterrupted设置成true
        thread.interrupt();
    }
}

这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅

Thread.interrupted

上面的案例中通过 interrupt,设置了一个标识告诉线程可以终止了,线程中还提供了静态方法 Thread.interrupted() 对设置中断标识的线程复位。比如在上面的案例中,外面的线程调用 thread.interrupt() 来设置中断标识,而在线程里面又通过 Thread.interrupted 把线程的标识又进行了复位

public class InterruptDemo {

    private static int i;
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()){
                    System.out.println("before:" + Thread.currentThread().isInterrupted());

                    //对线程进行复位,由 true 变成 false
                    Thread.interrupted();

                    System.out.println("after:" + Thread.currentThread().isInterrupted());
                }
            }
        }, "interruptDemo");
        
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt();
    }
}

其他的线程复位

除了通过 Thread.interrupted 方法对线程中断标识进行复位以外,还有一种被动复位的场景,就是对抛出 InterruptedException 异常的方法,在 InterruptedException 抛出之前,JVM 会先把线程的中断标识位清除,然后才会抛出 InterruptedException,这个时候如果调用 isInterrupted 方法,将会返回 false 分别通过下面两个 demo 来演示复位的效果

为什么要复位

Thread.interrupted() 是属于当前线程的,是当前线程对外界中断信号的一个响应,表示自己已经得到了中断信号,但不会立刻中断自己,具体什么时候中断由自己决定,让外界知道在自身中断前,他的中断状态仍然是 false,这就是复位的原因

线程的终止原理

我们来看一下 thread.interrupt() 方法做了什么事情

public class Thread implements Runnable {
......
    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }
}
......

这个方法里面,调用了 interrupt0(),这个方法在前面分析 start() 方法的时候见过,是一个 native 方法,这里就不再重复贴代码了,同样我们找到 jvm.cpp 文件,找到 JVM_Interrupt 的定义

JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_Interrupt");

  // Ensure that the C++ Thread and OSThread structures aren't freed before we operate
  oop java_thread = JNIHandles::resolve_non_null(jthread);
  MutexLockerEx ml(thread->threadObj() == java_thread ? NULL : Threads_lock);
  // We need to re-resolve the java_thread, since a GC might have happened during the
  // acquire of the lock
  JavaThread* thr = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));
  if (thr != NULL) {
    Thread::interrupt(thr);
  }
JVM_END

这个方法比较简单,直接调用了 Thread::interrupt(thr) 这个方法,这个方法的定义在 Thread.cpp 文件中,代码如下

void Thread::interrupt(Thread* thread) {
  trace("interrupt", thread);
  debug_only(check_for_dangling_thread_pointer(thread);)
  os::interrupt(thread);
}

Thread::interrupt 方法调用了 os::interrupt 方法,这个是调用平台的 interrupt 方法,这个方法的实现是在 os_*.cpp 文件中,其中星号代表的是不同平台,因为 jvm 是跨平台的,所以对于不同的操作平台,线程的调度方式都是不一样的。我们以 os_linux.cpp 文件为例

void os::interrupt(Thread* thread) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");
  //获取本地线程池对象
  OSThread* osthread = thread->osthread();
  //判断本地线程对象是否为中断
  if (!osthread->interrupted()) {
    //设置中断状态为 true
    osthread->set_interrupted(true);
    // More than one thread can get here with the same value of osthread,
    // resulting in multiple notifications.  We do, however, want the store
    // to interrupted() to be visible to other threads before we execute unpark().
    //这里是内存屏障
    OrderAccess::fence();
    //_SleepEvent相当于 Thread.sleep,表示如果线程调用了 sleep 方法,则通过 unpark 唤醒
    ParkEvent * const slp = thread->_SleepEvent ;
    if (slp != NULL) slp->unpark() ;
  }

  // For JSR166. Unpark even if interrupt status already was set
  if (thread->is_Java_thread())
    ((JavaThread*)thread)->parker()->unpark();
  //_ParkEvent用于 synchronized 同步块和 object.wait(),这里相当于也是通过 unpark 进行唤醒
  ParkEvent * ev = thread->_ParkEvent ;
  if (ev != NULL) ev->unpark() ;
}

set_interrupted(true) 实际上就是调用 osThread.hpp 中的 set_interrupted()方法,在 osThread 中定义了一个成员属性 volatile jint _interrupted;

通过上面的代码分析可以知道,thread.interrupt() 方法实际就是设置一个 interrupted 状态标识为 true、并且通过 ParkEvent 的 unpark 方法来唤醒线程

  • 对于 synchronized 阻塞的线程,被唤醒以后会继续尝试获取锁,如果失败仍然可能被 park
  • 在调用 ParkEvent 的 park 方法之前,会先判断线程的中断状态,如果为 true,会清除当前线程的中断标识
  • Object.wait 、 Thread.sleep 、 Thread.join 会抛出 InterruptedException

InterruptedException

为什么 Object.wait、Thread.sleep 和 Thread.join 都会抛出 InterruptedException?

这几个方法有一个共同点,都是属于阻塞的方法

而阻塞方法的释放会取决于一些外部的事件,但是阻塞方法可能因为等不到外部的触发事件而导致无法终止,所以
它允许一个线程请求自己来停止它正在做的事情。当一个方法抛出 InterruptedException 时,它是在告诉调用者如果执行该方法的线程被中断,它会尝试停止正在做的事情并且通过抛出 InterruptedException 表示提前返回

所以,这个异常的意思是表示一个阻塞被其他线程中断了。然后,由于线程调用了 interrupt() 中断方法,那么 Object.wait、Thread.sleep 等被阻塞的线程被唤醒以后会通过 is_interrupted 方法判断中断标识的状态变化,如果发现中断标识为 true,则先清除中断标识,然后抛出 InterruptedException

需要注意的是,InterruptedException 异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,比如

  • 直接捕获异常不做任何处理
  • 将异常往外抛出
  • 停止当前线程,并打印异常信息

为了让大家能够更好的理解上面这段话,我们以 Thread.sleep 为例直接从 jdk 的源码中找到中断标识的清除以及异常抛出的方法代码找到 is_interrupted() 方法,linux 平台中的实现在 os_linux.cpp 文件中,代码如下

bool os::is_interrupted(Thread* thread, bool clear_interrupted) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");

  OSThread* osthread = thread->osthread();
  //获取线程的中断标识
  bool interrupted = osthread->interrupted();
  //如果中断标识为 true
  if (interrupted && clear_interrupted) {
    //设置中断标识为 false
    osthread->set_interrupted(false);
    // consider thread->_SleepEvent->reset() ... optional optimization
  }

  return interrupted;
}

找到 Thread.sleep 这个操作在 jdk 中的源码体现,怎么找?相信如果前面大家有认真看的话,应该能很快找到,代码在 jvm.cpp 文件中

JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
  JVMWrapper("JVM_Sleep");

  if (millis < 0) {
    THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
  }
  //判断并清除线程中断状态,如果中断状态为 true,抛出中断异常
  if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) {
    THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
  }

  // Save current thread state and restore it at the end of this block.
  // And set new thread state to SLEEPING.
  JavaThreadSleepState jtss(thread);

注意上面加了中文注释的地方的代码,先判断 is_interrupted 的状态,然后抛出一个 InterruptedException 异常。到此为止,我们就已经分析清楚了中断的整个流程

资源信息

本文设计的代码可前往下载:https://github.com/huaweirookie/toalibaba/tree/master/concurrent-programming/thread-basic

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章