一、创建线程
Java中创建线程的写法有很多种!!!这里介绍其中5种。
方法1:继承Thread类,重写run
创建一个类,让这个类继承自Thread父类,再重写我们的run方法就可以了。
使用Thread类,不需要import别的包,因为它是再Java.lang下面的。
java
//写一个类,继承自标准库的Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello word!");
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
//创建线程,是希望线程成为一个独立的执行流(执行一段代码)
//创建线程是相当于雇了个人帮我们干活
Thread t = new MyThread(); //这里就不用new标准库的thread的了,而是刚才创建的子类
t.start(); //线程中的特殊方法,启动一个线程
}
}
注意:
start() 是创建了一个新的线程,由新的线程来执行t.run()方法。
这个新的线程就是调用操作系统的API
通过操作系统内核创建新线程的PCB,并且把要执行的指令交给这个PCB,当PCB被调度到了CPU上执行的时候,也就执行到了线程run方法的代码了。
如果只是在main方法中输出"hello world",你的Java进程主要就是有一个线程(调用main方法的线程),主线程通过t.start(),主线程调用stat(),创建出一个新的线程,新的线程调用t.run(),如果我们run()方法执行完毕,这个线程自然销毁了。
方法2:实现Runnable接口
java
//Runnable 作用是描述一个"要执行的任务",run 方法就是任务的执行细节。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("hello thread");
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
//这只是描述了个任务
Runnable runnable = new MyRunnable();
//把任务交给线程来执行
Thread t = new Thread(runnable);
t.start();
}
}
解耦合。目的就是为了让 线程和任务(线程要干的活)之间分离开。
未来如果要改代码不用多线程,使用多进程、线程池或协程......此时代码改动比较小。
方法3:使用匿名内部类,继承Thread
java
//使用匿名内部类来创建线程
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
super.run();
System.out.println("hello");
}
};
t.start();
}
}
其中new Thread()
:
①创建了一个Thread子类,(子类没有名字)所以才叫做"匿名";
②创建了子类的实例,并且让 t 引用指向该实例。
方法4:使用匿名内部类,实现Runable
java
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
t.start();
}
}
这个写法和方法2本质相同,只不过把实现Runable任务交给匿名内部类的语法。
此处是创建了一个类,实现Runable,同时创建了类的实例,并且传给Thread的构造方法。
方法5:使用Lambda表达式
此方法是最简单,最推荐的方法。
java
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("helloDemo5");
});
t.start();
}
}
把任务用lambda表达式来描述,直接把lambda传给Thread构造方法。
lambda就是个匿名函数(没有名字的函数),用一次就没有了。
() - > {}
二、Thread类及常见方法
Thread类是JVM用来管理线程的一个类,换句话说,每个线程都有一个Thread对象与之相关联 。
可以说,每个执行流,都需要有一个对象来描述,类似下图,而Thread类的对象,就是用来描述一个线程执行流的,JVM会将这些Thread对象组织起来,用于线程调度,线程管理。
1. Thread的常见构造方法
其中:
最后两个Thread(String name)
和 Thread(Runnable target, String name)
多了个name参数,这个参数是为了方便调试而给线程起了个名字。
线程的默认的名字为 thread-0,1,2之类的。
构造方法的格式如下:
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
例如:
java
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("hello");
}
}
},"mythread");
t.start();
}
}
2. Thread的几个常见属性
-
ID是线程的唯一标识,不同线程不会重复;
-
名称是各调试工具用到的;
-
getStat:构造方法里起的名字;
-
getStat:线程状态,Java中线程的状态要比操作系统原生的状态更丰富一些;
-
getPrior:线程的优先级可以获取,也可以设置,但是设置了不管用,因为优先级是很多要素影响得到的;
-
isDaem:是否守护线程,是否"后台线程"
前台线程会阻止进程结束,前台线程的工作没做完,进程是完不了的;
后台线程,不会阻止线程结束,后台线程没做完,进程是可以结束的。
代码里手动创建的线程,默认都是前台线程,包括main,默认也是前台;
其他JVM自带的线程都是后台线程。
也可以手动使用setDaemon设置成后台线程,是后台线程,就是守护线程。
把t设置成 守护/后台线程,此时进程的结束与否就和t无关了。
-
isAlive()是否存活:判断当前系统中的这个线程是不是有了。
在用户态(应用程序)中创建了一个线程
Thread t = new Thread();
程序中通过
t.start();
来调用,在真正调用start之前,调用
t.isAlive()
,此时是false,此时内核态(操作系统内核)里没有这个线程;而调用start后,就会让内核创建一个PCB,此时这个PCB才表示一个真正的线程,此时isAlive是 true。
也可以简单的理解为start/run方法是否允许结束了。
另外,如果内核里线程把run执行完了,此时线程销毁,PCB随之释放。但是Thread t 这个对象还不一定被释放,此时isAlive也是false。
【总结】
如果 t.run还没执行,isAlive 为 false
如果 t,run 正在执行,isAlive 为 true
如果 t.run执行结束,isAlive 为 false
通过上述我们可以了解,Thread t这个对象比内核里的PCB存在的周期要久。
3. 启动一个线程------start()
之前我们已经看到了如何通过复写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
- 复写run方法是提供给线程要做的事情的指令清单;
- 线程对象可以认为是把李四、王五等新线程叫过来了;
- 而调用start()方法,就是相当于喊一声"行动起来!",线程才真正独立去执行了。
总之,run方法是描述了我们要做啥任务,而stat才是真正开始任务。
调用start方法,才真正在操作系统的底层创建出一个线程!
4. 终止一个线程
终止线程的意思是,不是让线程立即就停止,而是通知线程,你应该要停止了,但是是否真的停止,取决于线程这里的具体写法。终止线程有以下两种方式:
- 使用标志位来控制线程是否要停止;
java
public class ThreadDemo8 {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException{
Thread t = new Thread(() -> {
while (flag) { //当flag为true的时候
System.out.println("hello!!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000); //当执行到第三秒的时候,把flag变为false,结束循环
flag = false;
}
}
这个代码之所以能够起到作用,修改flag,t线程就结束,完全取决于t线程内部的代码,代码里通过flag控制循环。
因此,这里只是告诉让这个线程结束,这个线程是否要结束,什么时候结束,都是线程内部自己代码来决定的。因为是while(flag)所以3秒钟后结束了,要是while(true),怎么改都不会结束循环。
此方法的缺点:自定义变量这种方式,不能及时相应,尤其在sleep休眠的时间比较久的时候。
- 使用Thread自带的标志位( interrupt() 方法),来进行判定是否要停止。
这里调用interrupt,只是通知终止,不是线程一定要乖乖终止!!
【格式】
while (!Thread.currentThread().isInterrupted()) {}
其中:
① while判定,判断条件是true,则执行方法体内的循环,为false,则循环结束;
② Thread.currentThread()
:这是Thread类的静态方法,通过这个方法可以获取到当前线程。哪个线程调用这个方法,就是得到哪个线程的对象引用(类似于this);
③ isInterrupted()
为true表示被终止,为false表示未被终止,继续执行;
④ 前面有个 ! 逻辑取反符,所以当isInterrupted()
为true时,!isInterrupted()
就为false,反之,则为true。
interrupt 会做两件事:
① 把线程内部的标志位(boolean)给设置成 true;
② 如果线程在进行sleep,就会触发异常,把sleep给唤醒。
但是sleep在唤醒的时候,还会做一件事,就是把刚在设置的这个标志位,再设置回false。(清空了标志位),这就导致,当sleep的异常被catch完了之后,循环继续执行!
如下述例子:
【例1】:线程t忽视了终止请求
java
public class ThreadDemo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt(); //3秒后,把线程内部的标志位(boolean)给设置成 true
}
}
此时循环会一直执行下去!这里就是sleep清空的例子。
【例2】:线程t立即响应了终止请求(加了break)
java
public class ThreadDemo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt(); //3秒后,把线程内部的标志位(boolean)给设置成 true
}
}
【例3】:稍后进行终止
执行完等了3秒钟,代码执行完毕。
java
public class ThreadDemo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//稍后再终止!
try {
Thread.sleep(3000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
break;
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}
}
【总结】
大前提:调用interrupt,只是告诉线程你该终止了,但是它是不是真的终止,这是它自己的事情。
注意其中sleep有个清楚标志位的情况,唤醒之后,线程终不终止,立即还是稍后终止,就把选择全交给程序员自己了。
5. 等待一个线程
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作,这是我们需要一个方法明确等待线程的结束
可理解为等待一个线程结束!
线程是一个随机调度的过程,等待线程做的事,就是再控制两个线程结束的顺序。
【例1】
java
public class ThreadDemo10 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("hello!!!");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
System.out.println("join 之前");
//此处的join就是让当前的main线程来等待t线程执行结束(等待t的run执行完)
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("join之后");
}
}
执行过程:
t 调用了start()后,t 线程和 main主线程就并发执行,分头行动;
主线程这里打印了"join之前",同时t线程打印了hello thread;
我们的t线程在执行过程中,主线程并没有打印join之后,而是在join这里等待了一会(发生阻塞block),等待3s之后,t线程执行完了,我们的主线程才会执行后面的 "join之后";
主线程,等待t线程彻底执行完毕之后,才继续往下执行了。
通过输出,我们也能看出 t 线程肯定比 main 线程先结束。
【例2】
java
public class ThreadDemo10 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("hello!!!");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("join 之前");
//此处的join就是让当前的main线程来等待t线程执行结束(等待t的run执行完)
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("join之后");
}
}
在join前加了一个等待5s钟,此时在执行join的时候,t 已经结束了,所以join不会堵塞,就会立即返回。
public void join()
:无参版本,一直等;public void join(long millis)
:指定一个超时时间(最大等待时间),这种是最常见的,一直等很容易又问题。
【总结】
线程没结束,就等待,线程结束了,就立即返回;
总之可以保证这两个线程的返回顺序。
6. 获取当前线程引用
public static Thread currentThread();
返回当前线程对象的引用。
调用这个方法,不需要实例,直接通过类名来调用。
java
Thread t = new Thread();
Thread.currentThread();
在哪个线程中调用,就能获取到哪个线程的实例。
7. 休眠当前线程
是我们比较熟悉的一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
public static void sleep(long millis) throws InterruptedException
休眠当前线程 millis毫秒
public static void sleep(long millis, int nanos) throws InterruptedException
可以更高精度的休眠
java
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}