学习Thread类,这一篇就够了!

 


目录

1.如何利用Java实现多线程编程?

2.创建线程的五种方法(面试题) 

 3.start方法 和 run方法

二者区别:

4. jconsole工具

 5.Thread类及常见方法

5.1常见的构造方法

5.2常见的属性

5.3守护线程与非守护线程

6.线程中断 

方法①:使用自定义变量来作为标志位

方法②:使用Thread.currentThread().isInterrupted()来代替自定义标志位


1.如何利用Java实现多线程编程?

首先,“线程”是操作系统抽象出来的概念,可以被操作系统独立地调度和执行,这使得多线程程序能够并发执行,从而提高程序的执行效率。

其次,为了支持多线程编程,操作系统内核提供了线程管理机制,并为用户层提供了API,如Linux中的pthread库。这些API允许程序员在程序中创建、同步和管理线程。当我们编写多线程程序时,实际上是在使用操作系统提供的线程API。

最后,Java虚拟机屏蔽了底层操作系统的差异,使得Java程序可以在不同的操作系统上运行。JVM已经为Java程序员封装了很多底层细节,使Java程序员无需直接使用操作系统提供的原生API,而可以使用Java标准库Thread类中的线程API。

在Java标准库中,Thread类可以视为是对操作系统所提供的API的进一步抽象及封装!

2.创建线程的五种方法(面试题) 

方法一:继承Thread类

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
    }
}

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

 方法二:实现Runnable接口

class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}

方法三:匿名内部类创建Thread类对象

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("这里是线程运行的代码");
            }
        };
        t.start();
    }
}

方法四:匿名内部类创建Runnable子类对象

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("这里是线程运行的代码");
            }
        });
        t.start();
    }
}

方法五:lambda表达式创建Runnable子类对象

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            System.out.println("这里是线程运行的代码"); 
        });
        t.start();
    }
}

 3.start方法 和 run方法

上述创建线程的方法都免不了调用start方法,t.start(); 这个操作其实就是创建一个新线程,这个线程执行t对象中的run方法

start 方法创建一个新的线程,本质上就是调用操作系统的API,通过操作系统内核创建新线程的 PCB(进程控制块,一种数据结构,具体请看上篇博客),然后把要执行的指令交给这个 PCB,当 PCB 调度到 CPU 上执行的时候,也就执行到了线程中的 run 方法了!

注意:start 方法里是没有调用 run 方法的,start 只是创建了一个线程,由新创建的线程去调用 run 方法!

上述代码的执行流程:主线程(main线程)执行 t.start(); 创建一个新线程,新线程调用 run方法,当 run 方法执行完毕,这个新的线程也会随之销毁。

二者区别:

start方法 向操作系统申请系统资源创建线程,是真正的创建了一个线程。

run方法 则是描述该线程需要干什么活、要完成什么任务。

我们可以在主线程中直接调用 run方法 ,此时就不会创建一个新线程。

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.run();
    }
}

上述代码就不会创建新的线程,只是通过主线程执行t对象中的run方法

4. jconsole工具

jconsole是jdk自带的工具,用来查看当前进程中的所有线程(bin目录下) 

 

一个进程里面有多个线程,我们需要连接进入进程才可以看到其中的线程 

如上图所示,我们可以看到一个是 main 线程,也就是主线程,还有一个是我们创建的线程,由于没有手动命名,系统默认起了个名字 Thread-0,除了这两个线程外,其他的线程都是 JVM 自带的,后续还会谈到

 5.Thread类及常见方法

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

5.1常见的构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用Runnable对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名

若直接 Thread t = new Thread(); t.start(); 这样相当于执行了一个空的 run 方法。

 target 是一个 Runnable 类型的,要想创建的线程能正常的执行 run方法,我们要么就继承 Thread 类重写 run,要么就实现 Runnable 接口重写 run。

上述介绍的可以给线程命名的构造方法,是为了方便调试,线程默认的名字叫做 Thread-0,Thread-1....

5.2常见的属性

属性对应获取方法
IDgetId()
名称getName()

状态

getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  •  ID     是线程的唯一标识,不同的线程 ID 都不同
  • 名称   是线程的名字,创建线程对象通过构造方法指定的名称,如果没指定就是默认的名字
  • 状态   是线程所处的状态,有很多种,具体我们后续讲解
  • 优先级  理论上优先级越高的线程越容易被调度到
  • 是否是后台线程,需记住JVM会在一个进程的所有非后台线程结束后,才会结束运行
  • 是否存活,简单理解为 run方法 是否运行结束了
  • 是否被中断,可以通过一些手段中断线程,后面会讲解

5.3守护线程与非守护线程

守护线程,是在程序运行时在后台提供服务的线程,我们更习惯称为后台线程

非守护线程,指在程序运行过程中不会随着主线程的结束而自动结束的线程,也称为前台线程

后台线程(守护线程),不会阻止进程的结束,即使后台线程的工作没有做完,进程也可以结束!
前台线程(非守护线程),会阻止进程的结束,如果前台线程的工作没有做完,进程不能结束!

我们前面所创建的一系列线程,包括main主线程在内,其实都是前台线程!像前面介绍jconsole工具时候显示出来的一系列系统自动生成的线程,那些线程就是后台线程!

在我们的日常开发中,可以使用 setDaemon() 方法来将线程设置为后台线程

public static void main(String[] args) {
    Thread t = new Thread(()->{
        System.out.println("这里是线程运行的代码");
    });
    t.setDaemon(true);//将创建的线程对象所对应的线程设置为后台线程
    t.start();
}

线程t被设置成后台线程后,进程的结束就与线程t无关了

public static void main(String[] args) {
    Thread t = new Thread(()->{
        while(true){
            System.out.println("这里是线程运行的代码");
        }
    });
    t.setDaemon(true);
    t.start();
    System.out.println("进程结束");
}

 现在已经将线程设置为后台线程了,虽然无限死循环打印,但是我们可以看到下面的结果,代码并没有无限打印下去

 

我们在将线程设置为后台线程后,后台线程无论任务是否完成,都不影响进程的结束,所以后台线程无法无限死循环打印,随着进程的结束就结束了

感兴趣的小伙伴可以尝试前台线程,这种情况的话,就会无限死循环打印下去,因为进程的结束受前台线程影响,前台线程还没有完成任务,进程就结束不了 

6.线程中断 

 目前常见的有以下两种方式:

  • 通过共享的标记来进行沟通
  • 调用 interrupt()方法 来通知

方法①:使用自定义变量来作为标志位

public class Main {
    private static boolean isQuit = false; //标志位
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(!isQuit) {
                System.out.println("线程运行中");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(5000);
        isQuit = true;
    }
}

上述代码在start后,系统休眠5秒,然后把标志位改为true,这个时候循环条件不成立,就可以告诉线程停止运行,从而达到线程中断的效果,但这里也存在内存可见性问题,这是一个缺点!可以通过引用volatile来解决这个问题。

 

方法②:使用Thread.currentThread().isInterrupted()来代替自定义标志位

 Thread.currentThread().isInterrupted() 是Thread类自带的标志位,自定义标志位的同时,也可使用当前线程自带的标志位。

 Thread.currentThread() 这部分是Thread类的静态方法,用来获取当前线程对象的引用,谁调用,就获得谁的引用

isInterrupted() 则是上文5.2提到过的线程常见属性中,是否被中断的获取方法,返回的是一个布尔值,true表示线程终止,false则表示线程继续执行

public class Main {
    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);
        t.interrupt(); //告知线程要中断了!!
    }
}

 当我们执行上述代码,会发现触发了异常,而且线程没有中止,这到底怎么回事?

 

其实这都是interrupt在捣蛋!快跟我一起喊捣蛋鬼别捣蛋,搞错了,再来!

 interrupt在背后干了两件事情:

  • 把线程内部的标志位设置成 true,告诉线程该终止了

  • 如果线程在 sleep,则会触发 sleep 的异常,把 sleep 提前唤醒!但是 sleep 在被唤醒的同时,还会把标志位设置成 false!

另外,像wait、join等类似造成线程堵塞的方法,都有类似的标志位清除机制

如此一来,interrupt就白忙活了,刚把标志位设置成true,结果线程在sleep,把线程唤醒了,标志位又给改回false了

正所谓 “存在即合理”,这样的设定其实是有好处的!相信大家都有刚开游戏老妈就喊你去买油盐酱醋茶的经历,这时候你就有三个选择了:

  1. 放下游戏,立刻马上去买;
  2. 不能坑队友,打完这把再去;
  3. 无视老妈,假装听不见(小心吃藤条炒猪肉);

版本一 :立刻马上去买!

Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        System.out.println("游戏开局");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            break; //立刻马上去买!
        }
    }
});

版本二:打完这把再去!

Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        System.out.println("游戏开局");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("等我打完这把!");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
        }
    }
});

 版本三:无视老妈(小心挨打)

Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        System.out.println("游戏开局");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("假装听不见,继续玩!");
        }
    }
});

 为什么 interrupt 不设定成立刻终止线程,而是让线程自己做选择呢?

线程是抢占式执行,CPU随机调度, interrupt方法执行时,线程不知道执行到哪里了,如果线程的活还没完成就给打断了,这是非常危险的!就比如微信打钱操作,我发了个红包给了老爸,结果线程执行一半中止了,我钱包里的钱还没了,老爸还没收到我的红包,以为我吞了这笔钱,于是就请我吃了一顿藤条炒猪肉······所以这里把是否中止线程的选择权交给程序员,根据不同场景进行调配,这才是一个好的选择!

Thread 类中还有一个 Thread.interrupted() 方法,手动清除标志位,了解即可!

 

 


感谢观看,希望对您有所帮助! 

下期预告:线程状态

  • 34
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 40
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 40
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

A小码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值