多线程的Thread 类及方法

Posted bit me

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程的Thread 类及方法相关的知识,希望对你有一定的参考价值。

✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇
✨每日一语:海压竹枝低复举,风吹山角晦还明。

目 录

🌲一. 线程的复杂性

多线程是非常复杂的,以至于程序猿为了规避多线程代码而发明了很多其他的方法,来实现并发编程。

最复杂的地方实际上在于操作系统的调度执行

例如:主线程运行创建新线程,主线程中要完成 abcdef 个任务,而新线程要完成 123456 个任务

情况一:(时间线从上到下,按照时间先后顺序执行)

情况二:(时间线从上到下,按照时间先后顺序执行)

情况三:(时间线从上到下,按照时间先后顺序执行)


还有许许多多的情况,远远不止上面三种多线程最复杂的地方就在于操作系统 的 "随机" 调度,也可以理解为线程的创建是有先后顺序的,但是执行线程先后顺序是随机的!!!具体这个线程里的任务啥时候执行要看调度器!!!

 

🌴二. Thread 类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。

每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

📕2.1 Thread 的常见构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
【了解】Thread(ThreadGroup group,Runnable target)线程可以被用来分组管理,分好的组即为线程组

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread(“这是我的名字”);
Thread t4 = new Thread(new MyRunnable(), “这是我的名字”);

Thread(String name) --> 给线程起名字!线程在操作系统内核里,是没有名字的,只有一个身份标识,但是在 Java 中,为了能让程序猿调试的时候方便理解这个线程是谁,就在 JVM 中对应的 Thread 对象加了个名字(多个线程名字可以相同,不取名字也会有默认的名字)

public class Demo7 
    public static void main(String[] args) 
        Thread t = new Thread(new Runnable() 
            @Override
            public void run() 
                while (true) 
                    System.out.println("hello Thread");
                    try 
                        Thread.sleep(1000);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                
            
        ," 我的线程 ");
        t.start();

        while (true)
            System.out.println("hello main");
            try 
                Thread.sleep(1000);
             catch (InterruptedException e) 
                e.printStackTrace();
            
        
    

例如上述代码中我们把线程命名为 " 我的线程 " 之后,我们可以根据上篇博客查看进程信息,在这个地方省略步骤。

如上我们就是进程命名成功了。

📗2.2 Thread 的几个常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • 线程的中断问题后续详解

getId() --> 线程在 JVM 中的身份标识
 
线程的身份标识有好几个

  • 内核的 PCB 上有标识
  • 到了用户态线程库里也有标识。(pthread,操作系统提供的线程库)
  • 到了 JVM 中又有一个标识(JVM Thread 类底层也是调用操作系统的 pthread 库)
     
    标识各不相同,但是目的都是作为身份的区别

getName() --> 在 Thread 构造方法里传入的名字

getState() --> PCB 里有个状态,此处得到的状态是 JVM 里面设立的状态体系,比操作系统里的状态体系要丰富一些

getPriority() --> 获取到优先级

isDaemon() --> 守护线程(后台线程)。类似于手机 APP 前后台,线程分为前台线程和后台线程。一个线程创建出来默认是前台线程,前台线程会阻止进程结束,进程会保证所有的前台线程都执行完了才会退出;后台线程不会阻止进程结束,进程退出的时候不管后台线程是否执行完。 main线程就是一个前台进程,通过 t.setDaemon(true); 把线程设置为后台线程。

public class Demo8 
    public static void main(String[] args) 
        Thread t = new Thread(() ->
           while(true) 
               try 
                   Thread.sleep(1000);
                catch (InterruptedException e) 
                   e.printStackTrace();
               
           
        ,"我的线程");

        t.start();
        System.out.println(t.getId());
        System.out.println(t.getName());
        System.out.println(t.getState());
        System.out.println(t.getPriority());
        System.out.println(t.isDaemon());
        System.out.println(t.isAlive());

    


上述操作都是获取到一瞬间的状态,不是持续状态

📘2.3 启动一个线程-start()

创建 Thread 实例,并没有真的在操作系统内核里创建出线程!调用 start 才是真正在系统里创建出新的线程,才真正开始执行任务!调用 start 方法, 才真的在操作系统的底层创建出一个线程。

创建 Thread 实例 --> 安排任务,各就各位。任务内容的体现在于 Thread 的 run 或者 Runnable 或者 lambda 。

线程的执行结束:只要让线程的入口方法(run,Runnable,lambda)执行完了,线程就随之结束了。(主线程的入口方法,就可以视为是 main 方法)

📙2.4 中断一个线程

  1. 手动创建标志位,来区分线程是否要结束

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.

public class Demo9 
    //用一个布尔变量表示线程是否要结束
    //这个变量是一个成员变量,而不是局部变量
    private static boolean isQuit = false;

    public static void main(String[] args) 
        Thread t = new Thread(()->
            while(!isQuit)
                System.out.println("线程运行中...");
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
            System.out.println("新线程执行结束!");
        );
        t.start();

        try 
            Thread.sleep(5000);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println("控制新线程退出!");
        isQuit = true;
    

如上代码中控制线程结束,主要是这个线程有个循环,这个循环执行完毕就结束了。很多时候创建线程都是让线程完成一些比较复杂的任务,往往都是有一些循环(正是有这些循环,执行的时间才可能比较长一点),如果线程本身执行的很快,刷一下就完了,也就没有必要提前控制他结束的必要了。

Thread 中有内置的标志位,不需要咱们手动创建

  1. 使用 Thread 自带的标志位

在循环条件中改一下即可

Thread.currentThread().isInterrupted()

currentThread() --> 静态方法,获取到当前线程的实例(Thread 对象),这个方法总是会有一个线程调用他。线程 1 调用这个方法,就能返回线程 1 的 Thread 对象;线程 2 调用这个方法,就能返回线程 2 的 Thread 对象。
 
isInterrupted() --> 判定内置的标志位,为 true 表示线程要被中断,要结束。

public class Demo10 
    public static void main(String[] args) throws InterruptedException 
        Thread t = new Thread(()->
            while (!Thread.currentThread().isInterrupted())
                System.out.println("线程运行中...");
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );
        t.start();

        Thread.sleep(5000);
        System.out.println("控制线程退出!");
        t.interrupt();
    

调用 interrupt,产生了一个异常!异常出现了,线程还在运行!

注意:interrupt 方法的行为

  • 如果 t 线程没有处在阻塞状态,此时 interrupt 就会修改内置的标志位
  • 如果 t 线程正在处于阻塞状态,此时 interrupt 就让线程内部产生阻塞的方法,例如 sleep 抛出异常,interruptedException。

此处异常被 catch 捕获了,捕获之后,什么都没做,就只是打印个调用栈就完了,因此把异常的打印忽略即可。

正是因为这样的操作,程序猿就可以自行控制线程的退出行为了

  • 可以立即退出
break;
  • 可以等一会儿退出
try 
    Thread.sleep(1000);
 catch (InterruptedException ex) 
    ex.printStackTrace();

break;
  • 可以不退出
啥也不做,相当于忽略了异常

主线程发出 “退出” 命令时,新线程自己来决定如何处理这个退出行为

综上:Java 中终止新线程
线程阻塞:1. 立即停止 2. 待会停止 3.啥也不做
线程没有阻塞:通过标志位直接停止

📓2.5 等待一个线程-join()

线程之间的执行顺序是完全随机的,看系统的调度!我们不能确定两个线程的开始执行顺序,但是可以控制两个线程结束的顺序!

join 的顺序谁先调用谁后调用是无所谓的

t1.join();
t2.join();

如上先调用 t1.join 后调用 t2.join ,此时 t1 t2 开始运行了,main 先阻塞在 t1 这里

  1. 如果是 t1 先结束,t2 后结束。当 t1 结束的时候,main这里的 t1.join 就执行完毕了,继续执行 t2.join,就阻塞了,然后 t2 结束的时候,main 的 t2.join 也返回(结束阻塞),main 继续执行,完成计时操作。
  2. 如果是 t2 先结束,t1 后结束。当 t2 结束的时候,由于 t1 没结束,main 仍然在 t1.join 这里阻塞,当 t1 结束之后,t1.join 解除阻塞,main 继续执行到 t2.join,由于 t2 已经结束了,t2.join 就不再阻塞了,直接返回,main继续执行
  • 实现让 t2 等待 t1执行完,main 等待 t2 执行完
public class Demo11 
    private static Thread t1 = null;
    private static Thread t2 = null;

    public static void main(String[] args) 
        t1 = new Thread(()->
            System.out.println("t1 begin");
            try 
                Thread.sleep(1000);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            System.out.println("t1 end");
        );
        t1.start();

        t2 = new Thread(()->
            System.out.println("t2 begin");
            try 
                Thread.sleep(1000);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            System.out.println("t2 end");
        );
        t2.start();

        try 
            t2.join();
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println("main end");
    


  • 主线程先执行 t1 然后 join 等待 t1 执行完毕,t1 执行完之后,主线程再启动 t2 等待 t2 执行完毕
public class Demo12 
    public static void main(String[] args) throws InterruptedException 
        System.out.println("main begin");
        Thread t1 = new Thread(()->
            System.out.println("t1 begin");
            try 
                Thread.sleep(1000);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            System.out.println("t1 end");
        );
        t1.start();
        t1.join();

        Thread t2 = new Thread(()->
            System.out.println("t2 begin");
            try 
                Thread.sleep(1000);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            System.out.println("t2 end");
        );
        t2.start();

        System.out.println("main end");
    

join 的行为:

  • 如果被等待的线程还没执行完,就阻塞等待
  • 如果被等待的线程已经执行完了,直接就返回
方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos)同理,但可以更高精度
  • 第一个是死等
  • 第二个和第三个设定了最大等待时间
  • 第三个的俩参数第一个是毫秒,第二个是纳秒,然后就是总的时间

未来实际开发程序的时候一般不会使用死等,会因为小的代码 bug 会让服务器卡死导致无法继续工作

📔2.6 获取当前线程引用

为了对线程进行操作(线程等待,线程中断,获取各种线程的属性),就需要获取到线程的引用

  • 如果是继承 Thread,然后重写 run 方法,可以直接在 run 方法中使用 this 即可获取到线程的实例,但是如果是 Runnable 或者 lambda,this 就不行了(this 就不是指向 Thread 实例)
  • 更通用的办法,Thread.currentThread()。哪个线程来调用这个方法,得到的结果就是哪个线程的实例。

📒2.7 休眠当前线程

  • 使用 sleep

在操作系统内核中有就绪队列(这里的PCB随时可以去CPU上执行),还有阻塞队列(这里的PCB暂时不参与调度,不去CPU上执行)。当某个代码中的线程调用 sleep 这个线程就从就绪队列跑到阻塞队列中,暂时不参与调度,等待 sleep 时间执行结束,然后就会调回就绪队列中(不是立马上 CPU 执行,还得看调度器的情况)。

 

🌳三. 线程的状态

🌻3.1 线程的所有状态

  • NEW: 安排了工作, 还未开始行动(创建了 Thread 对象,但是还没有调用 start 方法,系统内核里还没有线程)
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作(就绪状态:1. 正在 CPU 上运行;2. 还没在 CPU 上运行,但是一切准备就绪)
  • BLOCKED: 这几个都表示排队等着其他事情(等待锁)
  • WAITING: 这几个都表示排队等着其他事情(线程中调用了 wait)
  • TIMED_WAITING: 这几个都表示排队等着其他事情(线程中通过 sleep 进入的阻塞)
  • TERMINATED: 工作完成了(系统里面的线程已经执行完毕,销毁了,相当于线程的 run 执行完了,但是 Thread 对象还在)
public class Demo13 
    public static void main(String[] args) throws InterruptedException 
        Thread t = new Thread(()->

                System.out.println("hello thread");
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                

        );
        //start 之前获取,获取到的是线程还未创建的状态
        System.out.println(t.getState());

        t.start();
        Thread.sleep(500);//加上之后就是由 RUNNABLE 改成了 TIMED_WAITING
        System.out.println(t.getState());
        t.join();

        //join 之后获取,线程结束之后的状态
        System.out.println(t.getState());
    

Thread.sleep(500);之前:(正在工作中)

Thread.sleep(500);之后:(正在 sleep 中)

🌹3.2 线程状态和状态转换


看起来复杂,简化起来就很简单:

主干道是 NEW => RUNNABLE => TERMINATED

在 RUNNABLE 会根据特定的代码进入支线任务,这些支线任务都是 "阻塞状态",这三种阻塞状态,进入的方式不一样,同时阻塞的时间也不同,被唤醒的方式也不同。

Java多线程——Thread 类及常见方法和线程的基本操作


接上一篇 线程的概念和创建
介绍了什么是线程,线程和进程的关系以及线程的创建。再来了解一下Java多线程类 Thread 的常见构造方法和属性

一、Thread 的常见构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable对象创建线程
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group,Runnable target)线程可以被用来分组管理,分好的组即为线程组

这些构造方法都比较简单,在上一篇博客中也介绍了线程的几种创建方法

public static void main(String[] args) {
        Thread t1 = new Thread("线程1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {

            }
        },"线程2");
        
    }

二、Thread常见属性

属性获取的方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

来看一段代码:

当线程没有启动时

public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("线程运行中");
                }
            }
        },"线程");
        System.out.println("id:"+t.getId());
        System.out.println("name:"+t.getName());
        System.out.println("state:"+t.getState());
        System.out.println("priority:"+t.getPriority());
        System.out.println("isDaemon:"+t.isDaemon());
        System.out.println("isAlive:"+t.isAlive());
        System.out.println("isInterrupted:"+t.isInterrupted());

    }

运行结果

当执行 start() 方法启动线程后

public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程运行中");
                }
            }
        },"线程");

        t.start();
        System.out.println("id:"+t.getId());
        System.out.println("name:"+t.getName());
        System.out.println("state:"+t.getState());
        System.out.println("priority:"+t.getPriority());
        System.out.println("isDaemon:"+t.isDaemon());
        System.out.println("isAlive:"+t.isAlive());
        System.out.println("isInterrupted:"+t.isInterrupted());
    }

1.线程名字

Thread 的 name ,存在的意义就是为了方便调试

当我们把线程跑起来的时候,打开jconsole软件就可以看到我们为线程起的名字

2.线程的状态

此处说的线程的状态和这篇文章里 进程 ,的进程的状态是类似的效果。存在的意义都是辅助进行线程调度

3.优先级

优先级,也是和"进程的优先级”是类似的效果,此处的状态和优先级,和内核PCB中的状态优先级并不完全一致。

4.后台线程

关于后台线程,需要记住:JVM会在一个进程的所有非后台线程结束后,才会结束运行,也就是说是否是后台线程,影响了JVM进程是否能够退出。

创建一个新线程,默认不是后台线程。

如果不是后台线程
此时,如果main方法结束了,线程还没有结束,JVM进程不会结束

如果当前线程是后台线程
此时,如果main方法结束,线程还没有结束,JVM进程就会直接结束,同时也就把这个后台线程也一起带走了

5.线程是否存活

判断一个线程是否存活,最简单直观的方法就是 run 方法是否结束了

而用户代码中的 t 这个对象,要靠GC来销毁
Thread t 的生命周期和内核的PCB不一样,它会比 PCB更长

那么就可以通过 isAlive() 方法来判断当前内核中的PCB是否存在

三、线程的基本操作

1.start(启动线程)

前面的博客已经讲过线程的创建和启动了,这里就不细说了。
通过 start()方法来启动一个线程,注意start()和 run()的区别,
调用 start 方法, 才真的在操作系统的底层创建出一个线程。

2.Sleep(休眠线程)

也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的
通过 sleep() 方法来休眠一个线程,sleep() 是一个类方法

public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //休眠1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

Sleep 这个方法,本质上就是把线程PCB给从就绪队列,移动到了阻塞队列,只有当 Sleep时间到了或者抛出异常了才会回到就绪队列中

3.currentThread(获取当前线程引用)

currentThread 能够获取到当前线程对应的 Thread 实例的引用,相当于 this关键字

但是需要注意的是,如果是使用 Runnable 或者 lambda 的方式来创建的线程,就无法使用 this 了。
this指向的是 Runnable 实例,而不是Thread 实例了,此时也就没有 getId 方法了。

4.interrupt(中断线程)

让线程结束的关键,就是让线程对应的入口方法,执行完毕
常见的创建线程的方式有
继承 Thread 重写 run
实现 Runnable 重写 run
lambda

像这种情况,线程一下就执行完了


更多情况下,线程不一定这么快就能执行完 run 方法 ,如果 run 方法里面带的是一个死循环,此时这个线程就会一直持续运行,直到整个进程结束


实际开发中,肯定是不希望线程的 run 就是一个 死循环,更希望控制这个线程,按照咱们的需要随时结束
为了实现这个效果,就有一些常见的方法

简单粗暴的办法,直接定义一个 flag 变量,这种方式并不怎么好。

还有一种办法就是使用标准库里的内置的标记

获取线程内置的标记位:线程的 isInterrupted() 判断当前线程是不是应该结束循环,且默认返回是 true

修改线程内置的标记位:Thread.interrupted() 来修改这个标记位

来看一段代码:


4秒之后, interrupt 方法好像并没有修改这个标记位,循环看起来还是在继续执行,同时这里有个异常。


这里的 interrupt 方法有两种行为

1.如果当前线程正在运行中,此时就会修改 Thread.islnterruppted() 标记位为 true

2.如果当前线程 正在 sleep、wait、等待锁,此时就会触发 InterruptedException

只要在 catch 里面加一个 break 就好了


isInterrupted() 这个是 Thread 的实例方法,
还有和这个方法类似的:
interrupted() 这个是Thread的类方法(static)

那么这两者有什么区别呢?
列如:调用 interrupt() 方法,把标记位设为 true,就应该结束循环

当调用 静态的 interrupted 来判定标记位的时候,就会返回 true,同时就会把标记位再改回 false,下次再调用interrupted() 就返回 false

如果是调用非静态的 isInterrupted() 来判断标记位,也会返回 true,但不会对标记位进行修改,后面再调用isInterrupted() 的时候仍然返回 true

5.join(线程等待)

线程和线程之间,调度顺序是完全不确定的(取决于操作系统调度器自身的实现)
但是有的时候希望这里的顺序是可控的
此时线程等待,就是一种办法,这里的线程等待,主要就是控制线程结束的先后顺序

通过 t.join 来阻塞线程,此时线程处于阻塞等待,代码就不继续往下走了,具体来说就是,操作系统短时间内不会把这个线程调度到 CPU上了。

执行到 start 方法的时候,就会立刻创建出一个新的线程来
同时 main 这个线程也立刻往下执行,就执行到了 t,join
执行到 t.join 的时候就会发现,当前 t 线程还是再运行中
只要 t 在运行中,join 方法就会一直阻塞等待,一直等到 t 线程执行结束(run执行完),才会执行后面的代码

直接使用 join()就是相当于死等,在开发中死等是比较危险的。
join 还有个有参数版本,给了个参数就不是死等了

四、线程的状态

线程的状态,用于辅助系统对于线程进行调度这样的属性

线程的状态是一个枚举类型 Thread.State


运行结果

NEW:Thread 对象创建出来了,但是内核的PCB还没创建出来

RUNNABLE:当前的PCB已经创建出来了,同时这个PCB随时待命(就绪状态),这个线程可能是正在CPU上运行,也可能是在就绪队列中排队

TIMED_WAITING:表示当前的PCB在阻塞队列中等待,这样的等待是一个带有结束时间的等待,sleep就会触发这个状态

WAITING:线程中如果调用了 wait 方法,也会阻塞等待。此时处于 WAITING状态(死等),除非是其他线程唤醒了该线程

BLOCKED:线程中尝试进行加锁,结果发现锁已经被其它线程占用了。此时线程也会阻塞等待。这个等待就会在其它线程释放锁之后,被唤醒

TERMINATED:表示当前PCB已经结束了,Thread 对象还在,此时调用获取状态,得到的就是这个状态

yield()
yield()方法的效果是让线程主动让出CPU,但是不改变线程的状态。
不过这个方法一般不会使用

不用 yield 方法的时候,t1和t2线程打印的次数基本是五五开,
如果使用 yield 方法,t2打印明细比 t1 多。


下一篇线程的安全,马上更新!

以上是关于多线程的Thread 类及方法的主要内容,如果未能解决你的问题,请参考以下文章

Java中Thread类及常见方法

Java多线程——Thread 类及常见方法和线程的基本操作

多线程(二):Thread 类及常见方法

多线程的Thread 类及方法

写出java多线程程序设计中常用类及方法名,并分别说明它们的作用。

20165333第八周学习总结