上一篇文章我们讲了Java关于线程的基本知识和如何创建一个简单的线程。Java使用Thread类来创建线程。多线程编程对于以前没有相关经验的人来说,最难理解的地方在于操作系统调度的执行过程。对于这样的代码:
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("thread");
});
thread.start();
System.out.println("main");
}
我们知道,java在启动的时候,已经自动创建了主线程,主线程调用main方法,main方法中又创建了一个线程(命名为”thread线程“),这个线程的任务是打印”thread“,同时,main方法中在创建一个新线程后的任务是打印”main“。当创建了thread线程并且start后,主线程和thread线程就是广泛上定义的并发(并发+并行)的过程,这个时候先打印”main“还是先打印”thread“是不确定的,是随机的,要看操作系统的随机调度。 同样着,谁先结束也是不确定的,也可能是主线程先结束,也可能是thread线程先结束。所以,为了让主线程等待thread线程结束,需要使用join方法。
创建线程是看start的顺序,但是具体的线程对应的任务什么时候执行,要看系统调度器。
Thread常见构造方法
比如使用lambda表达式创建一个线程并且起名字。线程在操作系统内部是没有名字的,只有一个身份标志。但是在java中为了更加明白线程是谁,在jvm中给对应的Thread对象加了一个和内核中线程一一对应关系的名字,就像姓名和身份证号之间的关系。这个名字对于程序的执行没有关系。如果不手动创建,也会有默认的名字。
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("thread");
}, "thread线程");
}
Thread常见的几个属性
ID表示线程的身份。ID有好几个,这里的方法获取到的是线程在JVM中的身份标识。线程的标识有好几个,在内核的PCB上有标识,在用户态线程库中也有标识(操作系统系统的线程库)。
getName()方法是获取到在Thread构造方法中传入的名字。
getState()方法是一个比较关键的属性。在PCB里面有几个状态,在这里得到的状态是JVM设定的状态,比操作系统内置的状态更加丰富。
isDaemon()方法,daemon称为”守护线程“,也可以称为后台线程。比如手机上的app,打开app,这个app就来到前台,当我们切换应用的时候,这个app就到后台了。线程也分成前台线程和后台线程,也可以设置线程的前后台。
一个线程创建出现默认是前台线程,前台线程会阻止进程的退出,进程会保证所有的前台线程都执行结束,才会退出。对于后台线程,不会阻止进程的结束,进程退出的时候,不管线程是否执行结束。main线程是一个前台线程。
使用setDaemon(布尔值)方法设置一个线程为后台,true表示后台线程。
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "thread线程");
thread.start();
System.out.println(thread.getId());
System.out.println(thread.getName());
System.out.println(thread.getState());
System.out.println(thread.getPriority());
System.out.println(thread.isDaemon());
System.out.println(thread.isAlive());
}
上述操作都是获取到的一瞬间的状态,不是持续的状态。
start方法
再次强调,创建Thread实例并没有真正在操作系统内核中创建出线程(通过run方法,或Runnable,lambda中的内容安排任务),只有调用start,才是真正的在系统中创建出线程,才是真正的开始执行任务。
中断线程
想让线程结束,只要让线程的入口方法执行结束,线程就随之结束了。线程的入口方法,对于主线程来说,main方法就是它的入口方法;对于其他线程来说,run()方法中要执行的任务或者Runnable,lambda中的内容就是它的入口方法。
中断线程,就是让线程尽快的把入口方法执行结束。有2种方法。
(1)直接手动创建标志位来区分线程是否要结束
//boolean表示线程标志位
private static boolean isQuit = false;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (!isQuit) {
System.out.println("线程运行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("新线程执行结束");
});
thread.start();
try {
Thread.sleep(5000);
System.out.println("控制新线程退出");
isQuit = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
能控制线程结束,主要是这个线程有一个循环,这个循环执行结束,就算结束了这个线程。这样的情况是非常常见的,很多时候创建线程都hi让线程完成一些比较复杂的任务,往往都有一些循环。如果线程本身执行很快,一下子就结束了,也就没有提前控制的必要了。
Thread内置了标志位,不需要手动创建标志位。
(2)Thread内置的标志位
public static void main(String[] args) {
Thread thread = new Thread(() -> {
//静态方法(获取到当前线程的实例)
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程运行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
try {
Thread.sleep(5000);
System.out.println("控制新线程退出!");
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
其中,Thread.currentThread()方法是一个静态的方法,通过这个方法获取当前线程的实例(Thread对象),这个方法总会有一个线程会调用它。哪一个线程调用这个方法,就返回哪一个线程的Thread对象。
isInterrupted()方法是一个判定内置的标志位,默认是false,true表示线程要中断。现在运行,查看结果。
调用interrupt,产生异常,但是线程还是在继续运行。
interrupt方法的行为主要有两种:
(1)如果thread线程没有处于阻塞状态,这个时候interrupt就会修改内置的标志位,将isInterrupted的状态改为true;
(2)如果thread线程正在处于阻塞状态,这个时候interrupt就让线程内部能产生阻塞的方法,比如让sleep方法抛出InterruptedE xception异常。
阻塞的意思就是程序停下来,不走了。更形象的例子是让你下载视频的时候,进度条在99%之前都是顺利的,但是到了99%进度条就停了下来,不走了。99%之前就是未阻塞状态,99%的时候就是阻塞状态,过了好长的时间,进度条才继续从99%开始走。
能产生阻塞的方法比如sleep方法(实际中能产生阻塞的方法很多,这里只是用sleep方法模拟一下)抛出的异常正好被新创建的线程给捕获,然后执行了thread线程中的printStackTrace()方法。
下面创建的线程大部分时间是在sleep阻塞状态,但是始终反复在阻塞状态和正常运行状态之间切换。
这样捕获后,我们就可以自己控制线程的退出的行为了,可以立马退出(break),也可以等一会退出(收尾操作后break),也可以不退出。主线程发出“退出”的信息后,新线程自己决定如何处理退出的行为。
需要注意的是,这里的InterruptedException不是让interrupt方法抛出异常,而是interrupt方法让线程中的sleep方法抛出异常。sleep方法抛出的异常被捕获,打印了信息。第二个sleep方法对应的是下面的捕获异常。
还可以使用Thread.interrupted()方法来创建标志位。这个方法的标志位会自动清除,控制中断,标志位会先设为true,读取的时候就会读取到true,但是读取结束后这个标志位就会自动恢复成false,这个一般很少用。而代码中的方法状态不会恢复。
有的同学可能奇怪,明明interrupt方法是在创建的线程后面,为啥还能被创建的线程捕获异常呢,实际上,前面我们说过,main线程调用main方法,main方法中创建新的线程(命名为thread线程)后,main线程和thread方法就是并发执行的,它们各自的任务什么时候被调度是不确定的,总有那么一个时间main线程中的interrupt方法被thread线程捕获,这个时候thread线程是什么状态, interrupt方法就执行什么样的操作,多线程的调度是随机的,这种随机调度是抢占式执行的。
join方法
线程之间的执行顺序是完全随机的,要看系统的调度。写代码的时候,是比较讨厌这种随机性的,更需要手段能让顺序确定下来。join方法就是一种确定线程执行顺序的手段。不能确定两个线程开始的执行顺序,但是可以通过join来控制两个线程的结束顺序。
注意一下代码,当我们不注释join方法和注释join方法后运行结果有什么不同。
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
// try {
// thread.join();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
for (int i = 0; i < 5; i++) {
System.out.println("main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
加了join方法后的程序会先让thread线程执行完了再执行main,而没有加join方法的则是并发执行。我们是无法干预调度器的行为,但是可以让main线程主动阻塞进行等待,等待thread执行结束了,main解除阻塞,才能继续往下执行。有多个join方法的时候,方法的先后顺序是没有关系的。
join方法自身也会阻塞,Java线程但凡是阻塞的方法都可能抛出这个异常。
现在的情况是我创建了两个线程,分别命名为thread1,thread2。想要thread1先执行结束后,再执行thread2,最后执行main线程的后序任务。这样,虽然调度器仍然是随机的,但是在形式上就是顺序执行的代码。当我们的代码需要先执行一组线程后,在执行另一组的线程,就可以这样写了。
public class Main {
static Thread thread1 = null;
static Thread thread2 = null;
public static void main(String[] args) {
thread1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("thread1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2 = new Thread(() -> {
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 3; i++) {
System.out.println("thread2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
);
thread2.start();
try {
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 3; i++) {
System.out.println("main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
join有两种行为:
(1)如果被等待的线程还没有执行结束,那么直接阻塞等待;
(2)如果被等待的线程已经执行结束了,就直接返回。
join方法不带参数的时候,就是死等,一直等待下去。方法带参数的时候,如果超过了等待的最大时间,就开始执行下一步的代码。一般写带参数的join方法。