Java多线程初阶(一)(图片+源码+超详细)

线程的概念参照以往的这篇文章

目录

 1.创建线程

1.1 继承Thread类

1.2 实现Runnable接口

 eg:常用的简写方式

2.Thread类中的常用API

3. start方法和run方法

4. 继承Thread类启动新线程的逻辑

5. 实现Runnable接口启动新线程的逻辑 

6. 线程相关API

6.1 中断一个线程(包含注意事项)

6.2 等待一个线程 - join

6.3 获取当前线程的引用

6.3 休眠当前线程


 1.创建线程

1.1 继承Thread类

创建并启动线程的第一种方法是让我们的自定义类继承java.lang.Thread类,覆写其中的run方法,然后调用Thread类的start方法即可启动线程。

这里为什么调用的是start方法而不是run方法我们再接下来分析源码后试着理解下,现在先来看下新线程的创建和启动。

Java多线程初阶(一)(图片+源码+超详细)_第1张图片

 在实际的开发过程中,由于Java单继承机制,我们很少直接使用继承Thread的方法创建和启动线程,接下来看下第二种创建线程的方式。

1.2 实现Runnable接口

这是实现多线程的第二种方法,让我们的自定义线程类实现Runnable并且实现接口中唯一的一个run方法,然后将创建Thread对象,将我们的Runnable实例对象通过构造方法传入,接着调用Thread类的start方法即可启动线程。这个过程如下图所示:

Java多线程初阶(一)(图片+源码+超详细)_第2张图片

这种创建新线程的方式解决了单继承的缺陷,同时降低了程序的耦合度,是一种在开发中经常倍使用的创建新线程的方式。 

 eg:常用的简写方式

在创建新线程时,通常会使用匿名内部类或者lambda表达式的形式来简化书写。例如:

  • 使用匿名内部类的方式创建新线程
  • 使用lambda表达式的方式创建新线程

以下是使用这几种方式创建新线程的写法(备忘):

public class Main {
    public static void main(String[] args) {
        //使用匿名内部类继承Thread类实现多线程
        Thread threadAnonymous = new Thread("threadAnonymous-0") {
            @Override
            public void run() {
                System.out.println("这是使用匿名内部类继承Thread类启动的新线程" + this.getName());
            }
        };
        //使用匿名内部类实现Runnable接口创建的新线程
        Thread threadImplIter = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("这是使用匿名内部类实现Runnable接口启动的新线程" + Thread.currentThread().getName());
            }
        }, "threadImpIter-1");

        //使用lambda表达式实现的多线程
        Thread threadLambda = new Thread(() -> {
            //直接书写run方法的逻辑
            System.out.println("这是使用lambda启动的新线程" + Thread.currentThread().getName());
        },"threadLambda-2");
        threadAnonymous.start();
        threadImplIter.start();
        threadLambda.start();
    }
}

2.Thread类中的常用API

Thread类常用的构造方法:

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());    //传入Runnable接口实例
Thread t3 = new Thread("这是我的名字");      //指定新创建的线程的名字
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");    //传入Runnable接口实例,并指定新线程的名字
Thread类中的常用的获取属性的方法:
属性 方法
线程 ID getId()
线程名称 getName()
线程状态 getState()
线程优先级 getPriority()
是否后台进程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupt()

关于上表中线程的一些属性说明:

  • 优先级(priority)高的线程理论上来说更容易被调度到,但是实际情况是不能确定的
  • 关于线程是否存活,简单来说就是run方法是否执行结束
  • 关于后台进程,就是在所有的非后台进程执行结束后,这个后台进行就会强制被结束

3. start方法和run方法

现在我们来理解为什么新线程的执行逻辑是我们覆写的run方法,而启动新线程却要调用start方法来启动❓

1.调用被覆写的 run方法只是单纯的调用这个方法,并不会启动新线程,我们可以通过JDK自带的调试工具jconsole查看我们程序中正在运行的线程 :

Java多线程初阶(一)(图片+源码+超详细)_第3张图片

2.调用start方法的本质是调用Thread类中的start0方法,这个start0方法在底层会执行一系列操作来启动我们创建的线程并且让这个新线程调用我们覆写的run方法。 

Java多线程初阶(一)(图片+源码+超详细)_第4张图片

所以,在我们启动新线程时一定是调用Thread类的start方法去真正启动一个新线程然后让这个新线程调用我们的run方法;而不是简单的调用我们的run方法❗❗❗

4. 继承Thread类启动新线程的逻辑

我们编写的自定义类继承Thrad类并重写其中的run方法后,在去调用Thread类的start方法时,其中的start方法调用start0方法,而start0方法启动的新线程调用Thrad类中的run方法时,会动态绑定到我们自定义类中覆写的run方法,也就执行到了我们自己书写的run方法逻辑。

Java多线程初阶(一)(图片+源码+超详细)_第5张图片

5. 实现Runnable接口启动新线程的逻辑 

Runnable接口中只有一个run方法,那么它是怎么实现我们线程的创建和启动的呢?再来回顾实现Runnable接口创建和启动线程的逻辑过程:

Java多线程初阶(一)(图片+源码+超详细)_第6张图片

1. 我们在实现Runnable接口后是将它的实例当作构造参数传入了Thread对象的构造中,这个Runnable接口实例对象会被赋值给Thread对象中的成员变量身上。

public
class Thread implements Runnable {
    ...
   /* What will be run. */
    private Runnable target;    //Runnable接口的实例对象传进来后被赋值给这个成员属性
    ...
}

2. 当我们调用Thread对象的start方法时,start方法调用start0方法,底层start0执行创建出新线程对象时调用我们的run方法,而Thread类中的run方法回去调用target成员属性的run方法,我们的实例对象又实现了Runnable接口,此时发生动态绑定就调用到了我们覆写的run方法。其最终效果和直接执行Thread类创建和启动新线程的效果还是一样的。有的绕的话我们画张图来描述下这个过程:

Java多线程初阶(一)(图片+源码+超详细)_第7张图片

6. 线程相关API

6.1 中断一个线程(包含注意事项)

当一个线程进入到工作状态后,按理来说应当是执行完自己的逻辑后才停止运行。但是这种一成不变的执行方法在实际开发中可能并不适用。因此我们在想有没有一种方法能够让其接受我们的指控,在某些特定的时间终止线程的运行呢?

我们可以通过volatile修饰的自定义标志位的方法,让线程在某些时刻停止运行。同时在Thread内部已经为我们提供了一个boolean类型的变量作为线程是否被中断的标志,我们可以直接借用这个标志位变量控制我们线程的的中断。下面是相关操作的API:

方法 说明
public void interrupt() 中断对象关联的线程,如果线程处于阻塞状态,则会抛出阻断异常,否则设置该线程的阻断标志位为false
public static boolean Interrupted() 判断当前线程的中断标志位是否被设置,且在调用后清除标志位
public boolean isInterrupted 判断对象关联线程的标志位是否设置,调用后不清楚标志位

既然只是标志位,那么我们应当能够想到单纯的调用一个线程的interrupt方法只是改变了这个标志位的值,并不能真正阻断这个线程的执行。

  • 因此interrupt方法通常与实例方法isInterrupted搭配使用,当线程的标志位状态被实例的interrupt方法设置后,这个线程在获取自己标志位状态不满足条件后就终止运行。这里应当注意的是线程的中断标志位默认是false。以下是程序实例:
public class InterruptTestDrive {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //在循环的条件判断中新增了获取当前线程中断标志位的状态,但是不要修改标志位的值
                //如果当前线程的标志位的值为false,即没有被设置,则继续执行。否则因不满足循环条件退出循环
                System.out.println(Thread.currentThread().isInterrupted());
                for (int i = 0; i < 1_0000_0000 && !Thread.currentThread().isInterrupted(); i++) {
                    System.out.print(i + " ");
                }
            }
        });
        thread.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        thread.interrupt();
        System.out.println(thread.isInterrupted());
    }
}
  • 同时当通过interrupt改变线程中断标志位的值时,如果当前被改变线程由于wait/join/sleep方法而被阻塞挂起,则会抛出InterruptedException异常并清除标志位(将标志位恢复到默认状态)。
public class InterruptTestDrive {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().isInterrupted());
                for (int i = 0; i < 1_0000_0000; i++) {
                    System.out.print(i + " ");
                    try {
                        //当前这个线程绝大部分时间处于阻塞状态,当我们调用当前线程对象的interrupt方法时会抛出InterruptedException异常,
                        // 我们捕获并处理这个异常来决定这个线程的执行状态,同时这时线程对象调用的interrupted方法会恢复中断标志位的默认值false
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        //在这里处理中断异常
                        break;
                    }
                }
            }
        });
        thread.start();
        Thread.sleep(2000);
        thread.interrupt();
        System.out.println(thread.isInterrupted());     //thread的中断标志位并没有被设置为true,仍然保持着默认值
    }
}

那么,实例的isInterrupted方法和静态的interrupted方法有什么区别呢?

  • 实例的isInterrupted会判断与Thread对象关联的线程的中断标志位是否被设置,并不会清除中断标志位的值。
  • 静态的interrupted方法,是判断当前线程的中断标志位是否被设置,是先获取再清除当前线程的中断标志位。即当前线程的中断标志位会先进行保存,然后恢复中断标志位的默认值false,返回保存的标志位的值

6.2 等待一个线程 - join

有时,我们需要在当前线程中等待其他线程执行结束后,才能进行下一步工作,这是我们就可以使用等待线程对象的join方法。Thread类中有三种重载重载的join方法,如下所示:

方法 说明
public void join() 等待线程结束,可以认为是死等(不见不散)
public void join(long millis) 等待线程结束,最多等millis毫秒。等不到就继续当前线程的执行
public void join(long millils,int nanos) 同一个参数的join方法,只不过精度更高了

例如,我们需要在main线程中等待一个自定义线程类执行再接着执行main线程中接下来的逻辑。以下是程序实例代码:

public class JoinThreadTestDrive {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            //lambda表达式。直接书写run方法的逻辑
            for (int i = 0; i < 5; i++) {
                System.out.println(i + 1 + "thread-0");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "thread-0");
        thread.start();
        System.out.println(1 + "-main");
        System.out.println(2 + "-main");
        System.out.println("main线程开始等待thread-0线程的执行");
        
        
        //main线程死等thread-0线程,直到thread-0线程执行结束后才接着往下执行
        //thread.join();      
        
        
        //main线程等待thread-0线程1666ms,在0-1666ms之间thread-0线程执行结束了,main线程就直接往下执行
        //否则,main线程最多等待thread线程1666ms,如果1666ms之后thread-0线程仍未执行结束,则直接往下执行,不再等待thread-0线程
        //main:设还没点小脾气呢!
        thread.join(1666);
        //在当前main线程中等待thread-0线程执行结束后再执行接下来的逻辑
        System.out.println("main线程等待结束");
        System.out.println(3 + "-main");
    }
}

6.3 获取当前线程的引用

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

6.3 休眠当前线程

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

你可能感兴趣的:(JavaEE,java,jvm,开发语言)