Java中线程的创建与使用、Thread类的常用方法

1、什么是进程与线程

1.1 含义

1.1.1 进程

进程是指正在运行的程序的实例。在操作系统中,一个进程代表了一个正在执行的程序,它包括了程序的代码、数据以及程序执行时所需要的系统资源。

最直观的就是我们任务管理器:

任务管理器中的每一个应用程序都是一个进程。

1.1.2 线程

线程是进程中的一个执行单元。一个进程可以包含多个线程,每个线程都拥有独立的执行路径,可以独立执行特定的任务,一个进程中的所有线程都共享这个进程的资源。

比如下面的这个例子。 进程:餐厅的整个经营过程可以看作一个进程。它包括了所有的资源和活动,例如厨房、餐厅大厅、服务员、厨师、顾客等。 线程:在餐厅中,服务员可以看作是一个线程。有很多个服务员,他们分别负责接待顾客、记录订单、传递菜单、上菜等任务。服务员是餐厅进程中的一个执行单元,他与其他服务员共享同一份资源,如餐厅的桌子、厨房、食材等。多个服务员可以并行执行任务,提高效率。

1.1.3 线程与进程的区别

进程的诞生就是为了实现程序的并发执行。所谓并发,就是在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。

线程的诞生目的是为了减少程序在并发执行时所付出的时空开销,使操作系统具有更好的并发性。

  • 地址空间:线程共享本进程的地址空间,而进程之间是独立的地址空间。
  • 资源:线程共享本进程的资源如内存、I/O、CPU等,而进程之间的资源是独立的。
  • 健壮性:多进程要比多线程健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。
  • 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序出口,执行开销大。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,执行开销小。
  • 可并发性:两者均可并发执行。
  • 切换时:进程切换时,消耗的资源大,效率低。所以涉及到频繁的切换时,使用线程要好于进程。
  • 其他:进程是操作系统资源分配的基本单位,线程是操作系统调度的基本单位。

1.2 PCB与TCB

进程控制块 PCB(Process Control Block),它是用来描述和控制进程的运行的一个数据结构。它是进程存在的唯一标志,也是操作系统管理进程的重要依据。

TCB(Thread Control Block)是线程控制块的缩写,是操作系统中用来描述和控制线程运行的一种数据结构。

它们的关系:

PCB中存储了进程的标识符、状态、优先级、寄存器、程序计数器、堆栈指针、资源清单、同步和通信机制等信息。PCB是进程存在的唯一标志,也是实现进程切换和调度的基础。

TCB与PCB是差不多的,TCB中也存储了线程的标识符、状态、优先级、寄存器、程序计数器、堆栈指针、信号屏蔽等信息。TCB是线程存在的唯一标志,也是实现线程切换和调度的基础。

2.创建线程的几种方式

线程是操作系统中的概念。操作系统内核实现了线程这样的机制,并且对用户层提供了一些 API 供用户使用。Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装。

2.1 继承 Thread 类

(1)继承 Thread 来创建一个线程类。

class MyThread extends Thread{
    //重写run方法,run 表示这个线程需要执行的任务
    @Override
    public void run() {
        System.out.println("这个线程需要执行的任务");
    }
}

(2)创建 MyThread 实例。

//创建一个 MyThread 实例
MyThread myThread = new MyThread();

(3)调用 start() 方法,线程开始执行。

//start 方法表示这个线程开始执行,注意,这里不是调用 run()方法
myThread.start();

2.2 实现 Runnable 接口

(1)实现 Runnable 接口。

//通过 实现 Runnable 接口来创建一个线程
class MyRunnable implements Runnable{

    //需要重写run方法
    @Override
    public void run() {
        System.out.println("这个线程需要执行的任务");
    }
}

(2)创建一个 Thread 实例。

//创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为参数。
Thread thread = new Thread(new MyRunnable());

(3)调用 start() 方法,线程开始执行。

//线程开始运行
thread.start();

2.3 其它变形

  • 匿名内部类创建 Thread 子类对象。
Thread thread1 = new Thread(){
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Thread 子类对象");
    }
};
  • 匿名内部类创建 Runnable 子类对象。
Thread thread2 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Runnable 子类对象");
    }
});
  • lambda 表达式创建 Runnable 子类对象。
Thread thread3 = new Thread(()-> {
    System.out.println("使用匿名类创建 Thread 子类对象");
});

3.Thread 类的常见方法

3.1 Thread 的常见构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用 Runnable 对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名
Thread thread = new Thread("小明"){
    @Override
    public void run(){
        while(true){
        	System.out.println("我是小明");
    	}   
    }
};

这个线程的名字为“小明”,如何查看这个线程的名字呢?在Java开发工具包(JDK) 中,可以使用jconsole来监视线程。

  1. 打开你的 jdk 路径,我这里是C:\Program Files\Java\jdk1.8.0_31\bin,打开该路径下的jconsole工具。
  2. 运行上面的代码,然后根据如下操作:

可以看到有一个叫“小明”的线程,这个就是我们创建的线程。

3.2 获取 Thread 属性的方法

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复。
  • 名称是线程的名称。
  • 状态表示线程当前所处的一个情况,比如阻塞、运行等。
  • 优先级高的线程更容易被调度到。
  • 关于后台线程:后台线程(Daemon Thread)是在程序运行过程中在后台提供服务的线程。与前台线程(也称为用户线程)相对,后台线程不会阻止程序的终止。当所有的前台线程(用户线程)结束时,后台线程会自动被终止,即使它们尚未执行完毕。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了。

3.3 启动线程的方法 start()

前文已经演示过了,之前我们已经看到了如何通过重写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。重写 run 方法是提供给线程要做的事情的指令清单。而调用 start() 方法,线程才能真正独立去执行,调用 start 方法, 才真的在操作系统的底层创建出一个线程。

3.4 线程睡眠 sleep()

方法 说明
public static void sleep(long millis) throws InterruptedException 休眠当前线程 millis 毫秒
public static void sleep(long millis, int nanos) throws InterruptedException 可以更高精度的休眠

如下代码:

public static void main(String[] args) throws InterruptedException {
    long begin = System.currentTimeMillis();
    Thread.sleep(3000);//睡眠当前线程
    long end = System.currentTimeMillis();
    System.out.println(end - begin);
}

结果:

它的一些更详细内容后文介绍。

3.5 中断线程

这里的中断不是马上就中断的意思,具体的我们看下面的案例。

这里有两个线程,通过一个线程来中断另一个线程,这里先不使用Thread的方法。

public class Main {
    
    public static volatile boolean flag = true;
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(){
            @Override
            public void run(){
                // flag 是中断标记
                while(flag){
                    try {
                        Thread.sleep(500);//睡眠
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Hi");
                }
            }
        };
        //开启线程
        thread.start();

        //中断线程
        Thread.sleep(3000); //睡眠 3 秒
        flag = false;//更改标记
        System.out.println("开始中断线程");
    }
}

(volatile的作用这里不作介绍,后面会持续更新。)

结果:

你会发现,这里的中断就是改变标记位的值,并且这里是有延迟的,只有sleep睡眠结束的时候才能结束线程。

所以就可以使用Thread提供的方法,即: isInterrupted()或者interrupted(),它们的优点就是能立刻唤醒睡眠的线程,看下面的案例。

方法 说明
public void interrupt() 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知, 否则设置标志位
public static boolean interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted() 判断对象关联的线程的标志位是否设置,调用后不清除标志位

这里的标志位的详细介绍在后面。

public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(){
            @Override
            public void run(){
                // flag 是中断标记
                while(!Thread.currentThread().isInterrupted()){
                    try {
                        Thread.sleep(500);//随眠
                    } catch (InterruptedException e) {
                        e.printStackTrace();//打印报错信息
                    }
                    System.out.println("Hi");
                }
            }
        };
        //开启线程
        thread.start();

        Thread.sleep(3000); //睡眠 3 秒
        thread.interrupt();//中断 thread 线程,把标记位置为true。
        System.out.println("开始中断线程");
    }

这里解释一下!Thread.currentThread().isInterrupted(),Thread.currentThread()的作用是返回当前线程对象的引用,跟this差不多:

方法 说明
public static Thread currentThread(); 返回当前线程对象的引用

isInterrupted():如果当前线程被中断就返回true,否则返回false。

这里的中断要根据中断标志位来看,中断标志位(interrupt flag)是线程的内部状态之一,实际上是存在于线程的内部数据结构中。**具体来说,中断标志位是线程对象的一个成员变量,用于表示线程的中断状态。**这个标志位在线程创建时被初始化为false,当调用线程的interrupt()方法时,中断标志位会被设置为true,表示线程被中断。

Thread.currentThread().isInterrupted(),这里就表示当前线程是否中断,如果中断就返回true,否则false。最后在前面加上一个!,来作为while的条件,那就是中断就返回false,否则true。

最后我们来看看结果,注意,上面我们已经调用了thread.interrupt(),所以预期是sleep抛异常,然后结束循环(结束线程),

结果:

为什么这里程序没有结束呢?我们来慢慢分析,没结束显然是while循环的条件还是ture,那么Thread.currentThread().isInterrupted()的值就是false,就表示线程不是处于中断状态,What?我们不是已经调用了interrupt()方法了吗?

原因就是:当线程被阻塞时,比如调用了sleep()、join() 或 wait()方法,如果收到了中断请求,这些方法会抛出 InterruptedException 异常,并清除中断状态!,这里的清除中断状态就是把标记位置为false,表示该线程没有被中断。

如果解决这个问题呢?直接在catch中加上break就行了。

public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(){
            @Override
            public void run(){
                // flag 是中断标记
                while(!Thread.currentThread().isInterrupted()){
                    try {
                        Thread.sleep(500);//随眠
                    } catch (InterruptedException e) {
                        //这里进行善后处理

                        break;
                    }
                    System.out.println("Hi");
                }
            }
        };
        //开启线程
        thread.start();

        //中断线程
        Thread.sleep(3000); //睡眠 3 秒
        thread.interrupt();//中断 thread 线程
        System.out.println("开始中断线程");
    }

像这样做的优点有哪些呢?线程可以根据自己的逻辑来处理中断请求,比如结束循环、关闭资源或者忽略。

总结:先觉条件,调用interrupt();

  1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以InterruptedException异常的形式通知,重置中断标记。这时要不要结束线程取决于 catch 中代码的写法。可以选择忽略这个异常,也可以跳出循环结束线程。
  2. 如果没有 wait/join/sleep 等方法,
  3. Thread.interrupted() 判断当前线程的中断标志是否被设置,判断后,重置中断标志。
  4. Thread.currentThread().isInterrupted() 判断当前线程的中断标志是否被设置,不重置除中断标志。

3.6 等待线程 join()

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。

方法 描述
join() 等待该线程执行完成
join(long millis) 最多等待指定的毫秒数,如果线程在该时间内没有执行完成,当前线程将继续执行
join(long millis, int nanos) 最多等待指定的毫秒数和纳秒数,如果线程在该时间内没有执行完成,当前线程将继续执行
public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run(){
                for (int i = 0; i < 9; i++) {
                    System.out.println("thread");
                }
            }
        };

        thread.start();//开启线程

        System.out.println("join()前");
        try {
            thread.join();//先等 thread 线程执行完
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("join()后");
    }
}

如果在join()之前thread线程已经执行完了的话,join()就不会阻塞了。

这些join()方法提供了线程之间的协同执行机制,允许一个线程等待另一个线程执行完成后再继续执行。这对于需要线程执行顺序或线程之间的依赖关系的场景非常有用。

猜你喜欢

转载自blog.csdn.net/mxt51220/article/details/131361893