前言
前篇文章,我们已经学习过了,进程与线程的相关内容。那么在Java中如何实现多线程编程。原来对于线程的操作,是使用操作系统提供的 API,由于 java 是个跨平台的语言,很多操作系统提供的功能,都被 JVM 封装好了,咱们这里不需要学习操作系统原生态的 API(基于C语言实现的),只需要学习 java 提供的 API 即可。
提示:以下是本篇文章正文内容,下面案例可供参考
一、Thread 类
Java 中操作多线程,最核心的类就是 Thread。
1.1 创建 Thread
在 Java 中创建线程的写法有很多种,下面是一些创建线程常用的写法!!!
1.1.1 继承 Thread 并重写 run 方法
// 继承 Thread, 并重写 run 方法
class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello,thread");
}
}
public class Thread1{
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
注意事项:
1.start() 方法的内部并没有调用 run() 方法,start 的工作是创建了一个新的线程,然后由这个创建好的线程去执行run方法的任务。
2.系统调度线程,是 抢占性执行,具体哪个线程先上,哪个线程后上,不确定,取决于操作系统调度器具体实现策略,虽然多线程间具有优先级,但在应用层面无法修改。
1.1.2 实现 Runnable 接口
这种实现方式较第一种相比,达到了解耦合:目的是为了让 线程 和 线程要执行的任务 之间分开,对于未来的代码改动更加方便。
// 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("hello thread");
}
}
public class Thread2 {
public static void main(String[] args) {
// 描述了线程要执行的任务
Runnable runnable = new MyRunnable();
// 创建线程
Thread t = new Thread(runnable);
// 启动线程
t.start();
1.1.3 匿名内部类继承Thread
public class Thread3 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
System.out.println("hello,thread");
}
};
t.start();
}
}
1.1.4 匿名内部类,实现 Runnable 接口
public class Thread4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello thread");
}
});
t.start();
}
}
1.1.5 Lambda 表达式
使用 lambda 表达式最简单,更推荐使用。
public class Thread5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello lambda thread");
});
t.start();
}
}
1.2 Thread 的常见属性
Thread 常见的属性基本包括有:ID、名称、状态、优先级、是否为后台线程、是否存活、是否被中断。
1.2.1 ID
使用 getID 来获取线程标识符。
public class Thread5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello lambda thread");
});
t.start();
// 获取 t 线程的标识符ID
System.out.println(t.getId());
}
}
1.2.2 名称
使用 getName 来获取线程名称。
public class Thread5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello lambda thread");
},"Thread5");
t.start();
// 获取 t 线程的名称
System.out.println(t.getName());
}
}
也可以通过 jdk 中的 jconsole 查看线程信息,如下图:
1.2.3 状态
使用 getState 获取线程状态。
public class Thread5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello lambda thread");
},"Thread5");
t.start();
// 获取 t 线程的状态
System.out.println(t.getState());
}
}
1.2.4 优先级
使用 getPriority 设置线程优先级,虽然是可以设置优先级,但是设置了用处也不大,这里就不作演示。
1.2.5 是否为后台线程
使用 isDaemon 查看线程是否为后台线程 (守护线程)。
前台线程:前台线程会阻止进程结束,前台线程没完成,整个进程没法结束。
后台线程:后台线程不会组织进程结束。
public class Thread5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello lambda thread");
},"Thread5");
t.start();
// 查看 t 是否为后台线程
System.out.println(t.isDaemon());
}
}
另外,可以把线程设置为 后台线程,此时进程的结束,就跟这个线程没有关系了。
public class Thread5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello lambda thread");
},"Thread5");
// 查看 t 是否为后台线程
System.out.println(t.isDaemon()); // false
t.setDaemon(true);
System.out.println(t.isDaemon()); // true
t.start();
}
}
1.2.6 是否存活
使用 isAlive 查看线程是否存活。
public class Thread5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello lambda thread");
},"Thread5");
t.start();
// 查看 t 是否存活
System.out.println(t.isAlive());
}
}
1.2.7 是否被中断
使用 isInterrupted 查看线程时候被中断。
public class Thread5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello lambda thread");
},"Thread5");
t.start();
// 查看 t 是否被中断
System.out.println(t.isInterrupted());
}
}
二、中断一个线程
要理解,这里的 中断 的意思不是让线程立即停止,而是给线程发了一个通知,告诉线程应该要结束了,但是否真的结束,取决于线程这块的代码具体怎么编写的。
2.1 自定义标志位
可以使用一个标志位来控制是否要中断线程。
public class Thread6 {
public static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (flag) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread");
});
t.start();
Thread.sleep(300);
// 在主线程这里,就能通过 flag 标志位来操作 t 线程是否要结束
flag = true;
}
}
在这个程序中,之所以修改 flag,能结束线程,完全取决于 t 线程内部的代码,代码中通过 flag 控制循环。因此,这里只是告诉这个线程要结束,线程是否真的结束,要看线程内部执行的代码。
2.2 自带标志位
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread");
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}
上述代码中:
Thread.currentThread() 是在 t.run() 中调用的,此处获取的线程就是 t 线程。
currentThread 是 Thread 类的静态方法,通过这个方法,可以截取到当前线程,哪个线程调用这个方法,就得到哪个线程的引用。
t.interrupt 就可以终止线程了
调用 t.interrupt() 中断线程,此时 interrupt 要做两件事情:
1.把线程内部的标志位(boolean)设置成 true。
2.如果线程在 sleep,会触发异常,把 sleep 唤醒。
注意!!! 但是在唤醒的时候,还会再做一件事,把刚刚设置的标志位,再设置回 flase 。(清空了标志位) 这就导致,当异常被 catch 完了之后,循环继续往下执行。
所以,这里可以设置三种情况:
2.2.1 忽略中断请求
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread");
}
});
t.start();
Thread.sleep(1000);
t.interrupt();
2.2.2 立即响应中断请求
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
System.out.println("thread");
}
});
t.start();
Thread.sleep(1000);
t.interrupt();
当 catch 捕捉完异常之后,使用 break 立即退出循环,那么 t 线程就不会继续往下执行。
2.2.3 稍后中断
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
// 可以在此处处理一些工作
try {
Thread.sleep(3000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
break;
}
System.out.println("thread");
}
});
t.start();
Thread.sleep(1000);
t.interrupt();
可以在 beak 跳出循环之前处理一些工作,之后再中断线程。
三、等待一个线程
使用 jon。
线程是随机调度执行的,所谓等待一个线程,其实是处理两个线程之间的执行结束顺序。
下面代码,本来执行完 start 之后,t 线程和 main 线程就并发执行了。当主线程碰到了 join,main 线程发生了阻塞,一直阻塞到 t 线程结束,main 线程才继续往下执行。
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("你好,thread");
}
});
t.start();
// 此处的 join 就是让主线程 main 等待 t 线程执行完毕,再继续执行 main 线程。
t.join();
System.out.println("main 主线程");
}
另外,注意事项:
1.当主线程碰到 join 的时候,如果 t 线程已经执行完了,那么 main 线程就不会发生阻塞。
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread");
}
});
t.start();
Thread.sleep(5000);
System.out.println("join 之前");
t.join();
System.out.println("join之后");
}
- join 还提供了带参数的版本。可以在参数部分指定一个超时时间。(最长等待时间)
Thread t = new Thread(() -> {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("你好,thread");
}
});
t.start();
// 此处的 join 就是让主线程 main 等待 t 线程执行完毕,再继续执行 main 线程。
// 无参数,main 线程相当于 死等。
// 有参数,1000 - 指定了 main 线程的最大等待时间
t.join(1000);
System.out.println("main 主线程");
四、获取线程引用
通过 Thread.currentThread 来获取线程的引用,哪个线程调用,获取的就是哪个线程对应的引用。
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("thread");
System.out.println(Thread.currentThread());
});
t.start();
t.join();
System.out.println(Thread.currentThread());
}
五、休眠一个线程
使用 sleep 来休眠一个线程。
本质上就是让这个线程不去 CPU 上执行,不去参与 CPU 调度。
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
// 让 t 线程休眠 1000 ms
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread");
});
t.start();
}
前面我们讲到 线程的就绪状态 和 阻塞状态。
在操作系统中,就绪状态是 PCB 在链表中都是 随叫随到 的。
当 线程A 调用了 sleep, A 就会进入休眠状态,把 A 从就绪链表中拿出来,放到另一个链表中,另一个链表中的 PCB 都是阻塞状态,暂时不参与 CPU 的调度和执行。
而操作系统每次需要调度,就直接去 就绪链表中 拿就好了。
注意
:
1.前面所说的,PCB 是使用链表来组织的,这里并不准确,而是使用一系列简单的链表,以这一系列链表为核心的数据结构。
2.一但线程进入了阻塞状态,就是对应的 PCB 进入阻塞队列中,此时无法参与调度。
这里有个问题:加入一个线程 sleep(1000),对应的 PCB 就要在阻塞队列中待 1000ms 这么久,当这个 PCB 回到了就绪队列,会被立即调度吗?
答案肯定不是的,虽然这里是 sleep(1000),但是实际上考虑是调度的开销,对应的线程是无法被唤醒之后就立即执行的,实际上的时间大海率是 大于1000ms 的。
六、线程的状态
Java 对线程的状态进行了细化。
6.1 NEW
new 代表的是,创建了 thread 类,还没有调用 start 方法。(内核里还没创建线程)
6.2 TIMED_WAITING
timed_waiting 表示的是,线程执行完毕了,Thread对象还在。
6.3 RUNNABLE
runnable 表示的是,可运行的,这里有两种含义;
1.线程正在 CPU 上参与调度执行。
2.线程对应 PCB 在就就绪队列中,随时都可以去 CPU 上执行。
6.4 WAIT、TIMED_WAITING、BLOCKED
wait,timed_waiting,blocked 这个三个表示的都是,线程处于阻塞阻塞状态,只是阻塞的原因不同。
6.5 线程状态转换
比如以下程序:
七、总结
以上就是今天要讲的内容,下一篇会继续续分享我对于多线程的其他认识,如果小伙伴们有喜欢作者写的内容或者对你有些帮助,希望能点个关注,支持一下小编,我也会继续努力,大家一起进步!!!