创建线程的几种方法
1.继承Thread类,重写run方法。
//创建一个线程 并发编程 (默认)并发 = 并行 + 并发
class MyThread extends Thread {
@Override
public void run() { //方法重写 线程的入口方法
while(true) {
System.out.println("========");
try {
//使用静态方法sleep 受查异常 需要抛异常 抛异常有两种 throws 和 try..catch
//由于这里是重写方法 重写的方法方法名等都要相同 被重写的方法没有throws 故不能用throws 只能用 try..catch
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo1 {
//main 也是一个线程 由JVM自动创建
public static void main1(String[] args) {
Thread thread = new MyThread();
//start 和 run 都是 Thread 的成员
//run 只是描述了线程的入口 线程要做什么
//start 是调用了系统API,在系统中创建线程,让线程再调用 run
thread.start();
//thread.run(); //这样调用会先执行调用run方法的线程,此线程执行完后,再执行其他线程
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new MyThread();
thread.start();
while(true) {
System.out.println("********");
Thread.sleep(1000);
}
}
}
2. 实现Runnable接口,重写run方法。
class MtThread implements Runnable { //实现Runnable接口
@Override
public void run() {
while (true) {
System.out.println("&&&&&&&&&&&&&&");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo02 {
public static void main(String[] args) throws InterruptedException {
//使用 Runnable 的写法 和 直接继承 Thread 之间的区别主要就是【解耦合】
Runnable runnable = new MtThread();
Thread thread = new Thread(runnable);
thread.start();
while (true) {
System.out.println("=========");
thread.sleep(1000);
}
}
}
使用实现 Runnable 的写法 和 直接继承 Thread 之间的区别?
主要区别就是解耦合,解耦合,即就是 实现Runnable 会把任务存在runnable的run方法中,如果要执行此任务,再创建线程进行调用,这样,任务就不会单独属于某个进程,而是创建的进程都可以去调用。实现了任务与线程的分离,即实现Runnable的写法会解耦合。
3.Thread+匿名内部类,重写run方法。
//利用匿名内部类
public class Demo03 {
public static void main(String[] args) {
//匿名内部类
Thread thread = new Thread() {
@Override
public void run() {
while (true) {
System.out.println("=======");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//启动线程
thread.start();
while (true) {
System.out.println("++++++++++++");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.Runnable+匿名内部类,重写run方法。
//利用匿名内部类
public class Demo04 {
public static void main1(String[] args) {
//第一种
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("======");
}
}
};
Thread thread = new Thread();
thread.start();
while (true) {
System.out.println("*********");
}
}
public static void main2(String[] args) {
//第二种
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("???????");
}
}
});
thread.start();
}
}
5.Thread+lambda表达式,不用重写。
public static void main(String[] args) {
Thread thread = new Thread( () -> {
while (true) {
System.out.println("=========");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"这是新线程"); //给线程起名字
//设置 thread 为后台进程 改为后台进程后 主线程飞快的执行完了,thread 还没来得及执行整个
进程就完了
//前台进程 会影响进程结束 而后台进程则不会即当前台进程若是早于后台进程结束,无论后台结
束与否,整个进程都得结束
//thread.setDaemon(true); //设置 thread 为后台进程
thread.start(); //启动线程
System.out.println(thread.isAlive()); //线程是否存活
}
前台进程 后台进程概念
前台进程 会影响进程结束 而后台进程则不会即当前台进程若是早于后台进程结束,无论后台结
束与否,整个进程都得结束。
在java中可通过调用方法 setDaemon(true) 来将线程设置为后台进程。
关于后台进程的要点:JVM会在一个进程的所有非后台进程都执行结束完后,才会结束运行。即后台进程完不完并不关心。只关心所有非后台进程完没完。
Thread的几个常见属性
属性 | 获取方法 | 说明 |
ID | getId() |
线程的唯一标识 是Java分配给线程的,不是系统 API 提供的。 |
名称 |
getName() | 线程名 |
状态 | getState() | |
优先级 | getPriority() | |
是否后台线程 | isDaemon() | |
是否存活 | isAlive() | 简单理解为run方法是否运行结束 |
是否被中断 | isInterrupted() | 标志位 |
面试题:在JAVA中创建线程都有哪些方式?
lamda表达式不用重写的原因及什么是变量捕获?
lambda本质上是一个匿名函数,并且使用lambda表达式的前提是接口为函数式接口,何为函数式接口,即接口中只包含唯一一个抽象方法。
而Runnable接口就是函数式接口,只包含一个抽象方法run(),圆括号对应run()方法的参数列表,为空则就为空。箭头后面的一对花括号对应run()方法的方法体。
这就是与匿名内部类的区别:匿名内部类可用于接口中有多个抽象方法的情况。
面试题:start方法和run方法的区别?
- start方法是启动线程的,在方法内部,会调用系统API,并在系统内核中创建出线程。
- run方法是线程的入口方法,即告诉线程你要干啥,要执行啥内容。
- 本质区别是是否在系统内部创建出新的线程。
标志位
private static boolean isQuit = false; //标志位
public static void main1(String[] args) throws InterruptedException {
Thread thread = new Thread( () -> { //lambda 表达式是不需要写 run 方法的 , 自身本就是 run
while(!isQuit) {
System.out.println("线程正在工作");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程运行结束");
});
thread.start();
Thread.sleep(5000);
isQuit = true;
}
若将成员变量设置为局部变量时的问题
其实将红色方框的代码去掉就好了,但是我们设置标志位的目的就是让 isQuit 在线程执行一段时间后从 false 改为 true 。从而使线程结束,但如果去掉变量就没用了,达不到目的了。返回到问题,即为什么用局部变量就不行呢?原因是与lambda表达式的变量捕获有关。
所谓变量捕获,匿名内部类有,lambda也有,即就是访问所在方法或代码块中的局部变量,这个过程被称为"变量捕获"。并且这个局部变量得是有效的最终变量。有效的最终变量不一定是非得用 final 修饰,而是指一个在生命周期中没有被修改过的值。而在上面代码中局部变量 isQuit 在红框框中被修改的话就不符合有效的最终变量。
匿名内部类的变量捕获与lambda表达式的变量捕获的区别?
lambda表达式可以捕获外面的this,而匿名内部类无法直接捕获外面的this。
上述标志位不够好。
因为需要手动创建变量。
并且当线程内部在sleep的时候,主线程修改变量,新线程内部不能及时响应。
下面用另一种方法:Thread类内部,有一个现成的标志位,可以用来判定当前的循环是否要结束。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread( () -> {
//Thread类内部,有一个现成的标志位,可以用来判定当前的循环是否要结束
//isInterrupted() 判断标志位
while (!Thread.currentThread().isInterrupted()) {
System.out.println("==========");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
// 1. 若不加break 程序抛出异常后 线程仍然继续正常执行
// 2. 加上break ,程序抛出异常后线程立即结束
// 3. 做一些其他工作收尾工作 完成之后再break
// 其他工作的代码放到这里
break;
}
}
});
//启动线程
thread.start();
//5s后线程往下执行
Thread.sleep(5000);
//设置标志位 即让线程中断停止
thread.interrupt();
}
代码中thread.interrupt();这个操作,就是把Thread对象内部的标志位设置为true了。即使线程内部还在sleep,也是可以通过这个方法立即唤醒的。这就是使用这个方法比额外使用一个成员变量充当标志位的优点,及时。并且会抛出InterruptedException异常。
有个注意点是; 一定要在 try... catch 的 catch 里加 break;
如果不加break,程序抛出异常后线程仍然继续正常执行。原因是interrupt()唤醒线程之后,sleep()方法抛出异常,同时会自动清除刚才设置的标志位。while() 循环又再次可以执行。
加了break之后,程序抛出异常后线程立即结束。
线程等待 join
无参join() 谁调用,等待谁,直至结束为止
带参join(1000) 单位是毫秒,等待一秒,但这里会有一个调度开销,等待完成后只是立即进入就绪状态,并不一定立即就执行。
进程,线程的状态
对进程来说,最核心的状态就是就绪状态和阻塞状态,对线程也是。
而在Java中,还赋予了线程一些其他的状态。
状态 | 说明 |
NEW | Thread对象已经创建但start方法还没调用时 |
TERMINATED | Thread对象还在,内核中的线程不存在了 |
RUNNABLE | 就绪状态(线程已经上CPU执行 / 线程正等待上CPU) |
TIMED_WAITING | 阻塞:由于sleep这种固定时间的方式产生的阻塞 |
WAITING | 阻塞:由于wait这种不固定时间的方式产生的阻塞 |
BLOCKED | 阻塞:由于锁竞争导致的阻塞 |
通过这些方法,若在编程中遇到线程卡死等问题时就可以通过这些状态来确定原因。