Java并发编程:Thread类

简介

Thread类可以用来创建多线程,并指定要多线程执行的业务逻辑(重写run方法)

创建多线程

启动多线程总共有3步:

  • 创建线程(new Thread类及其子类)
  • 指定任务(Thread的run方法或Runnable的run方法中指定要执行的任务)
  • 启动线程(Thread的start方法)

线程的生命周期

  • 新建(New)/初始状态状态:新建一个线程对象。
  • 就绪/可运行(Runnable)状态:线程对象创建后,其他线程(一般是main方法的主线程)调用该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
  • 运行状态(Running):就绪状态的线程获得了CPU使用权,并执行程序代码。
  • 阻塞状态(Blocked):线程因为某种原因放弃CPU的使用权,暂时停止运行。直到线程再次进入就绪状态,才有机会转到运行状态。阻塞的情况分为三种:
    • 等待阻塞:运行的线程执行wait方法,JVM会把该线程放入等待池中。wait会释放持有的锁。
    • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    • 其他阻塞:运行的线程执行sleep或join或yield方法,或者发出了I/O请求,JVM会把该线程置为阻塞状态。当sleep的状态超时、join等待线程终止或超时、以及I/O处理完毕时,线程重新转入就绪状态。这种方式不会释放持有的锁。
  • 死亡状态(Dead):线程执行完成,或者因为异常退出run方法,该线程结束生命周期。

start方法与run方法

  • run()方法是Thread类(或子类)中的一个普通方法,如果直接调用run方法,只是用主线程去执行了普通方法
  • start()方法是native方法,调用之后会启动1个线程(让线程变成就绪状态,等待 CPU 调度后执行),该线程会去执行run方法。

所以启动多线程一定要调用start方法,如果直接调用run方法,只是用主线程去执行了普通的run方法。

调用run方法和start方法对比

java 复制代码
public class PrintStoryExtends extends Thread{
    String text;
    long interval;

    public PrintStoryExtends(String text, long interval){
        this.text = text;
        this.interval = interval;
    }

    public void run(){
        try{
            System.out.println("执行这段代码的线程名字是:" + Thread.currentThread().getName());
            printStory(text, interval);
            System.out.println(Thread.currentThread().getName() + "执行结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    protected void printStory(String text, long interval) throws InterruptedException {
        for (char ch : text.toCharArray()){
            Thread.sleep(interval);
            System.out.print(ch);
        }
        System.out.println("----");
    }
}

//main函数所在类
public class PrintStoryAppMain {
    public static final String text = "今天又是阳光明媚的一天,某小胖7点钟起床洗漱完成,奔向地铁站去往浦东图书馆,在龙阳路换乘时,发现今天有好多人啊," +
            "还都是背着小书包的,某小胖心里想:从地铁站就开始卷了嘛,等下一下车要奔跑哇。";

    public static void main(String[] args) {
        System.out.println("程序执行开始,执行线程的名字叫做:" + Thread.currentThread().getName());
        for (int i = 1; i <= 2; i++){
            Thread thread = new PrintStoryExtends(text, 200 * i);
            thread.start();;
        }
        System.out.println("启动线程结束,名字叫做:" + Thread.currentThread().getName());
    }
}

在main方法中new PrintStoryExtends之后,调用start方法,会创建一个线程(一定要调用start方法,才会创建线程,如果直接调用run方法,只是用主线程去执行了普通的run方法),该线程执行run方法中的业务逻辑。

  • 直接thread.run()的执行结果
css 复制代码
程序执行开始,执行线程的名字叫做:main
执行这段代码的线程名字是:main
今天又是阳光明媚的一天,某小胖7点钟起床洗漱完成,奔向地铁站去往浦东图书馆,在龙阳路换乘时,发现今天有好多人啊,还都是背着小书包的,某小胖心里想:从地铁站就开始卷了嘛,等下一下车要奔跑哇。----
main执行结束

执行这段代码的线程名字是:main
今天又是阳光明媚的一天,某小胖7点钟起床洗漱完成,奔向地铁站去往浦东图书馆,在龙阳路换乘时,发现今天有好多人啊,还都是背着小书包的,某小胖心里想:从地铁站就开始卷了嘛,等下一下车要奔跑哇。----
main执行结束
启动线程结束,名字叫做:main

Process finished with exit code 0
  • 调用thread.start()的执行结果
css 复制代码
程序执行开始,执行线程的名字叫做:main
启动线程结束,名字叫做:main
执行这段代码的线程名字是:Thread-0
执行这段代码的线程名字是:Thread-1
今今天又天是阳又光明是媚的阳一天光,某明小胖媚7点的钟起一床洗天漱完,成,某奔向小地铁胖站去7往浦点东图钟书馆起,床在龙洗阳路漱换乘完时,成发现,今天奔有好向多人地啊,铁还都站是背去着小往书包浦的,东某小图胖心书里想馆:从,地铁在站就龙开始阳卷了路嘛,换等下乘一下时车要,奔跑发哇现。----
Thread-0执行结束
今天有好多人啊,还都是背着小书包的,某小胖心里想:从地铁站就开始卷了嘛,等下一下车要奔跑哇。----
Thread-1执行结束

start方法详细分析

在Thread 类的顶部,有个native的registerNatives本地方法,该方法主要的作用就是注册一些本地方法供 Thread 类使用,如start0(),stop0() 等等,可以说,所有操作本地线程的本地方法都是由它注册的 . 这个方法放在一个static语句块中,这就表明,当该类被加载到JVM中的时候,它就会被调用,进而注册相应的本地方法。

csharp 复制代码
private static native void registerNatives();    

  static {
    registerNatives();
  }
  
}  

本地方法registerNatives是定义在Thread.c文件中的。Thread.c是个很小的文件,定义了各个操作系统平台都要用到的关于线程的公用数据和操作

arduino 复制代码
JNIEXPORT void JNICALL
    Java_Java_lang_Thread_registerNatives (JNIEnv *env, jclass cls){
      (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
    }
    
    static JNINativeMethod methods[] = {
       ......
{"start0", "()V",(void *)&JVM_StartThread},
       {"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
      ......
};

到此,可以容易的看出Java线程调用start的方法,实际上会调用到JVM_StartThread方法,那这个方法又是怎样的逻辑呢。实际上,我们需要的是(或者说 Java 表现行为)该方法最终要调用Java线程的run方法,事实的确如此。在jvm.cpp中,有如下代码段:

scss 复制代码
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
      ......
     native_thread = new JavaThread(&thread_entry, sz);
     ......

这里JVM_ENTRY是一个宏,用来定义JVM_StartThread 函数,可以看到函数内创建了真正的平台相关的本地线程,其线程函数是 thread_entry,代码如下所示。

scss 复制代码
static void thread_entry(JavaThread* thread, TRAPS) {
  HandleMark hm(THREAD);
   Handle obj(THREAD, thread->threadObj());
   JavaValue result(T_VOID);
   JavaCalls::call_virtual(&result,obj,
   KlassHandle(THREAD,SystemDictionary::Thread_klass()),
   vmSymbolHandles::run_method_name(),
   vmSymbolHandles::void_method_signature(),THREAD);
}

可以看到调用了vmSymbolHandles::run_method_name方法,这是在 vmSymbols.hpp用宏定义的:

kotlin 复制代码
class vmSymbolHandles: AllStatic {
  ...         template(run_method_name,"run")
  ...
}

至于run_method_name是如何声明定义的,因为涉及到很繁琐的代码细节,本文不做赘述。感兴趣的读者可以自行查看JVM的源代码。

综上所述,Java线程的创建调用过程如上图所示,首先 , Java线程的start方法会创建一个本地线程(通过调用JVM_StartThread),该线程的线程函数是定义在jvm.cpp中的thread_entry,由其再进一步调用run方法。

可以看到Java线程的run方法和普通方法其实没有本质区别,直接调用run方法不会报错,但是却是在当前线程执行,而不会创建一个新的线程。

yield()方法(没在业务代码中见到过)

源码

java 复制代码
public static native void yield();

作用

Thread类的静态方法,调用后能让当前线程释放CPU的时间片,进入就绪状态(而非阻塞状态),让与该线程有同等优先级的其他线等待线程获取CPU执行权,也有可能当前线程又重新获得CPU时间片进入运行状态,调用yield方法不会使线程释放它所持有的对象的同步锁。

代码实战

  • 线程的优先级相同
csharp 复制代码
public class YieldMethod {
    public static void main(String[] args) {
        Runnable runnable = () -> {
            for (int i = 0; i <= 100; i++) {
                System.out.println(Thread.currentThread().getName() + "_____" + i);
                if (i % 20 == 0) {
                    // 每执行完20个让出CPU
                    Thread.yield();
                }
            }
        };
        new Thread(runnable, "栈长").start();
        new Thread(runnable, "小蜜").start();
    }
}

上述代码有以下两种可能的结果:

结果1:栈长让出了 CPU 资源,小蜜成功上位。

lua 复制代码
栈长-----29

栈长-----30

小蜜-----26

栈长-----31

结果2:栈长让出了 CPU 资源,栈长继续运行。

lua 复制代码
栈长-----28

栈长-----29

栈长-----30

栈长-----31
  • 线程的优先级不同
java 复制代码
public class YieldMethod {
    public static void main(String[] args) {
        Runnable runnable = () -> {
            for (int i = 0; i <= 100; i++) {
                System.out.println(Thread.currentThread().getName() + "_____" + i);
                if (i % 20 == 0) {
                    // 每执行完20个让出CPU
                    Thread.yield();
                }
            }
        };

        Thread thread1 = new Thread(runnable, "栈长");
        thread1.setPriority(Thread.MIN_PRIORITY);

        Thread thread2 = new Thread(runnable, "小蜜");
        thread2.setPriority(Thread.MAX_PRIORITY);

        thread1.start();
        thread2.start();

    }
}

因为给小蜜加了最高优先权,栈长加了最低优先权,即使栈长先启动,那小蜜还是有很大的概率比栈长先会输出完的。 实际执行结果如下:

java 复制代码
栈长_____0
小蜜_____0
栈长_____1
栈长_____2
栈长_____3
栈长_____4
栈长_____5
小蜜_____1
...
小蜜_____22

yield 和 sleep 的异同

1)yield, sleep 都能暂停当前线程,sleep 可以指定具体休眠的时间,而 yield 则依赖 CPU 的时间片划分。

2)yield, sleep 两个在暂停过程中,如已经持有锁,则都不会释放锁资源。

3)yield 不能被中断,而 sleep 则可以接受中断。

sleep()方法

中断方法

stop()方法已经不推荐使用了

void interrupt()

源码

scss 复制代码
public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}
  • 向线程发送中断请求,将线程的中断状态置为true,但不会中断(停止)这个正在运行的线程,类似"set"。
  • 当线程处于阻塞状态(比如调用了sleep方法),此时再调用该线程的interrupt()方法,会抛出InterruptedException中断异常,不会将线程的中断状态再置为true。

boolean isInterrupted

源码

java 复制代码
public boolean isInterrupted() {
    return isInterrupted(false);
}

/**
 * Tests if some Thread has been interrupted.  The interrupted state
 * is reset or not based on the value of ClearInterrupted that is
 * passed.
 */
private native boolean isInterrupted(boolean ClearInterrupted);
  • 调用之后,返回线程的中断状态,调用这个方法不会改变线程的中断状态,类似"get"。

static boolean interrupted()

源码

java 复制代码
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

private native boolean isInterrupted(boolean ClearInterrupted);
  • 调用之后,返回线程的中断状态,并将线程的中断状态设置为false,类似:set+get

总结

以上三个方法不能真正将一个线程中断,只是改变线程的中断状态的值,改变状态之后,再由实际代码判断是否要停止当前线程。因此这个中断状态可以被用来当做标志位(开关),我们可以通过if或者while这种判断语句来决定线程中断状态置为true后该怎么处理。

上述三种中断方法的实际用处

比如:

java 复制代码
public class MyThread extends Thread {
    public void run() {
        while(!this.isInterrupted()) {
            System.out.println("当前线程没有被中断,继续干活");
        }
        System.out.println("当前线程被中断,停止干活");
    }
}

public class MyThreadMain {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();

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

        t.interrupt();
    }
}

线程启动,5s之后,主线程(执行main方法的线程)将线程t的中断状态设为true,线程没有立刻停止?过一会停止之后打印:。。。

执行结果

当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程没有被中断,继续干活
当前线程被中断,停止干活

当Mythread线程t处于休眠状态的时候,主线程调用interrupt方法时,会抛出InterruptedException。举例如下:

java 复制代码
public class MyThread extends Thread{
    @Override
    public void run() {
        while (!this.isInterrupted())
        {
            System.out.println("当前线程没有被中断,继续干活");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e)
            {
                this.interrupt();
            }
        }
        System.out.println("当前线程被被中断,停止干活");
    }
}

等待线程join()

线程是一个随机调度的过程,等待线程就是控制两个线程的结束顺序,因为线程调度是随机的,我们无法控制线程的开始顺序,但是能通过方法控制线程的结束顺序。

通常是主线程里调用该方法,t.join()。主线程等待t线程执行完成之后再执行。

代码实战

  • 主线程里没有调用join方法的情况
java 复制代码
public class MyThread extends Thread {
    public void run() {
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName + "开始运行");

        for (int i = 0; i < 20; i++) {
            System.out.println(currentThreadName +"正在运行" + String.valueOf(i));
        }
    }
}


public class MyThreadMainAsync {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();

        System.out.println("主线程开始执行");
        for (int i = 0; i < 20; i++) {
            System.out.println("主线程正在运行" + String.valueOf(i));
        }

    }
}

执行结果:

java 复制代码
主线程开始执行
Thread-0开始运行
主线程正在运行0
主线程正在运行1
主线程正在运行2
主线程正在运行3
Thread-0正在运行0
主线程正在运行4
Thread-0正在运行1
主线程正在运行5
Thread-0正在运行2
...
Thread-0正在运行10
主线程正在运行6
Thread-0正在运行11
...
Thread-0正在运行19
主线程正在运行7
...
主线程正在运行19
  • 主线程里调用了join方法

修改上述的MyThreadMain方法即可

java 复制代码
public class MyThreadMain {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();

        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程开始执行");
        for (int i = 0; i < 20; i++) {
            System.out.println("主线程正在运行 " + String.valueOf(i));
        }
    }
}

运行结果:

java 复制代码
Thread-0开始运行
...
Thread-0正在运行19
主线程开始执行
主线程正在运行 0
...
主线程正在运行 19

设为守护线程:setDaemon()方法

Java中的线程分为前台和后台线程,代码里手动创建的线程,Main线程都是前台的线程,其它的JVM自带的线程都是后台的线程,也就是守护线程。

也可以通过setDaemon来手动设置成后台线程

前台线程和后台线程的特点:

前台线程会阻止进程的结束,前台线程的工作没有做完,进程是不能结束的,后台线程不会阻止进程结束,后台工作没做完,进程是可以结束的

java 复制代码
public class MyThreadMain {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.setDaemon(true);

        t.start();

        System.out.println("主线程开始执行");
        for (int i = 0; i < 20; i++) {
            System.out.println("主线程正在运行 " + String.valueOf(i));
        }
    }
}

执行结果:

vbnet 复制代码
Thread-0正在运行10
Thread-0正在运行11
Thread-0正在运行12
Thread-0正在运行13
Thread-0正在运行14

Process finished with exit code 0

可以看到:线程t还没执行结束,进程就结束了。

参考文章

  1. Thread, Runable, Callable 还傻傻分不清?
  2. 多线程 start 和 run 方法的区别
  3. 多线程 Thread.yield 方法到底有什么用?
  4. 深入浅出线程Thread类的start()方法和run()方法
  5. 多线程(一) | 聊聊Thread和Runnable
相关推荐
lucifer3112 小时前
线程池与最佳实践
java·后端
程序员大金2 小时前
基于SSM+Vue+MySQL的酒店管理系统
前端·vue.js·后端·mysql·spring·tomcat·mybatis
程序员大金3 小时前
基于SpringBoot的旅游管理系统
java·vue.js·spring boot·后端·mysql·spring·旅游
Pandaconda3 小时前
【计算机网络 - 基础问题】每日 3 题(十)
开发语言·经验分享·笔记·后端·计算机网络·面试·职场和发展
程序员大金4 小时前
基于SpringBoot+Vue+MySQL的养老院管理系统
java·vue.js·spring boot·vscode·后端·mysql·vim
customer084 小时前
【开源免费】基于SpringBoot+Vue.JS网上购物商城(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
Ylucius4 小时前
JavaScript 与 Java 的继承有何区别?-----原型继承,单继承有何联系?
java·开发语言·前端·javascript·后端·学习
ღ᭄ꦿ࿐Never say never꧂5 小时前
微服务架构中的负载均衡与服务注册中心(Nacos)
java·spring boot·后端·spring cloud·微服务·架构·负载均衡
.生产的驴5 小时前
SpringBoot 消息队列RabbitMQ 消息确认机制确保消息发送成功和失败 生产者确认
java·javascript·spring boot·后端·rabbitmq·负载均衡·java-rabbitmq