目录
方法②:使用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常见的属性
属性 | 对应获取方法 |
---|---|
ID | getId() |
名称 | 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了
正所谓 “存在即合理”,这样的设定其实是有好处的!相信大家都有刚开游戏老妈就喊你去买油盐酱醋茶的经历,这时候你就有三个选择了:
- 放下游戏,立刻马上去买;
- 不能坑队友,打完这把再去;
- 无视老妈,假装听不见(小心吃藤条炒猪肉);
版本一 :立刻马上去买!
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() 方法,手动清除标志位,了解即可!
感谢观看,希望对您有所帮助!
下期预告:线程状态