✨个人主页:bit me
✨当前专栏:Java EE初阶
✨每日一语:海压竹枝低复举,风吹山角晦还明。
多线程是非常复杂的,以至于程序猿为了规避多线程代码而发明了很多其他的方法,来实现并发编程。
最复杂的地方实际上在于操作系统的调度执行
例如:主线程运行创建新线程,主线程中要完成 abcdef 个任务,而新线程要完成 123456 个任务
情况一:(时间线从上到下,按照时间先后顺序执行)
情况二:(时间线从上到下,按照时间先后顺序执行)
情况三:(时间线从上到下,按照时间先后顺序执行)
还有许许多多的情况,远远不止上面三种,多线程最复杂的地方就在于操作系统 的 "随机" 调度,也可以理解为线程的创建是有先后顺序的,但是执行线程先后顺序是随机的!!!具体这个线程里的任务啥时候执行要看调度器!!!
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 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();
}
}
}
}
例如上述代码中我们把线程命名为 " 我的线程 " 之后,我们可以根据上篇博客查看进程信息,在这个地方省略步骤。
如上我们就是进程命名成功了。
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
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());
}
}
创建 Thread 实例,并没有真的在操作系统内核里创建出线程!调用 start 才是真正在系统里创建出新的线程,才真正开始执行任务!调用 start 方法, 才真的在操作系统的底层创建出一个线程。
创建 Thread 实例 --> 安排任务,各就各位。任务内容的体现在于 Thread 的 run 或者 Runnable 或者 lambda 。
线程的执行结束:只要让线程的入口方法(run,Runnable,lambda)执行完了,线程就随之结束了。(主线程的入口方法,就可以视为是 main 方法)
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 中有内置的标志位,不需要咱们手动创建
在循环条件中改一下即可
!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.啥也不做
线程没有阻塞:通过标志位直接停止
线程之间的执行顺序是完全随机的,看系统的调度!我们不能确定两个线程的开始执行顺序,但是可以控制两个线程结束的顺序!
join 的顺序谁先调用谁后调用是无所谓的
t1.join();
t2.join();
如上先调用 t1.join 后调用 t2.join ,此时 t1 t2 开始运行了,main 先阻塞在 t1 这里
- 如果是 t1 先结束,t2 后结束。当 t1 结束的时候,main这里的 t1.join 就执行完毕了,继续执行 t2.join,就阻塞了,然后 t2 结束的时候,main 的 t2.join 也返回(结束阻塞),main 继续执行,完成计时操作。
- 如果是 t2 先结束,t1 后结束。当 t2 结束的时候,由于 t1 没结束,main 仍然在 t1.join 这里阻塞,当 t1 结束之后,t1.join 解除阻塞,main 继续执行到 t2.join,由于 t2 已经结束了,t2.join 就不再阻塞了,直接返回,main继续执行
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");
}
}
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 会让服务器卡死导致无法继续工作
为了对线程进行操作(线程等待,线程中断,获取各种线程的属性),就需要获取到线程的引用
在操作系统内核中有就绪队列(这里的PCB随时可以去CPU上执行),还有阻塞队列(这里的PCB暂时不参与调度,不去CPU上执行)。当某个代码中的线程调用 sleep 这个线程就从就绪队列跑到阻塞队列中,暂时不参与调度,等待 sleep 时间执行结束,然后就会调回就绪队列中(不是立马上 CPU 执行,还得看调度器的情况)。
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 中)
主干道是 NEW => RUNNABLE => TERMINATED
在 RUNNABLE 会根据特定的代码进入支线任务,这些支线任务都是 "阻塞状态",这三种阻塞状态,进入的方式不一样,同时阻塞的时间也不同,被唤醒的方式也不同。