引言:
本章博客涉及进程线程内容,如果不了解的可以看:什么是进程线程-优快云博客
线程是操作系统的概念,操作系统提供的API供程序员使用操作。但是不同的操作系统(Winodws、Linux、Unix……差别很大),但是做为JAVA程序员就不需要担心了,JVM把这些封装好了。我们不需要关注系统原生API,只需要学习java提供的这一套API就可以了。
一、创建线程的几种方法
1.继承Thread类
//1.自己创建一个类,去继承 Thread类 并重写run方法
class MyThread extends Thread{
//2.线程会自动调用run方法中的内容,类似主线程中的main方法
@Override
public void run() {
while (true) {
System.out.println("hello thread");
}
}
}
public class demo1 {
public static void main(String[] args) {
//3.实例化继承了Thread的类
MyThread myThread = new MyThread();
//4.创建线程
myThread.start();
while (true) {
System.out.println("hello main");
}
}
}
结果就会不停打印:hello main 和 hello thread
但是不方便观察而且,我电脑的风扇已经转起来了,因为相当于同时执行两个程序,并且死循环那么它的执行就非常的快。我们可以加上sleep(休眠),第一让线程休息指定时间,第二方便我们观察。
1.1 Thread.sleep()
传入的单位是毫秒(ms),sleep会让出CPU资源,不再争抢CPU资源。
而且sleep还是静态的方法,我们直接使用类名调用。
sleep抛出异常问题:
如果是run方法中使用,不能使用throws向上抛,因为Thread类中没有处理这个异常,我们只能使用try catch处理掉这个异常:
在主函数中的sleep:
在主函数中的sleep使用throws 和 try catch都是可以的,如果使用throwsJVM会处理这个异常,主函数中可以用
throws
处理Thread.sleep()
方法抛出的InterruptedException
异常,JVM 会处理这个被抛出的异常,具体表现为打印异常堆栈信息和终止程序执行。
//1.自己创建一个类,去继承 Thread类 并重写run方法 class MyThread extends Thread{ //2.线程会自动调用run方法中的内容,类似主线程中的main方法 @Override public void run() { while (true) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class demo1 { public static void main(String[] args) throws InterruptedException { //3.实例化继承了Thread的类 MyThread myThread1 = new MyThread(); //4.创建线程 myThread1.start(); while (true) { System.out.println("hello main"); Thread.sleep(1000); } } }
结果:
1.2 start()
start方法是创建线程的方法,底层会调用对应操作系统的API来创建线程。
并且一个Thraed对象只能调用一次start:
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { //执行3次大概3秒 for (int i = 0; i < 3; i++) { System.out.println("Hello Thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); //线程大概率结束了 Thread.sleep(5000); t1.start(); }
1.3 run()
run()会被创建的线程自动调用,理解为另一个执行线程任务的入口,就像是JVM调用main方法一样。
2.实现Runnable接口
Thrad实现了Runnable接口:
Runnable接口是函数式接口,意味着我们还可以通过Lambda表达式来写。我们先用普通的方式写:
//1.定义一个类实现Runnable接口 class MyRunnable implements Runnable { //2.实现run方法 @Override public void run() { while (true) { System.out.println("hello MyRunnable"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class demo2 { public static void main(String[] args) throws InterruptedException{ //3.实例化实现Runnable的接口 MyRunnable runnable = new MyRunnable(); //4.把Runnable的接口给Thread Thread t = new Thread(runnable); //5.创建线程 t.start(); while (true) { System.out.println("hello main"); Thread.sleep(1000); } } }
记得是先实例化实现Runnable的类,在实例化Thread类的时候,把实现了Runnable接口的类传入构造方法中。
结果:
为什么在Thread对象的时候可以传入Thread,肯定是有这样的一个构造方法,我们现在来了解一下Thread的构造方法。
2.1 Thread的构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable子类对象创建线程对象 |
Thread(String name) | 创建线程对象,并给线程取名字 |
Thread(Runnable target, String name) | 使用Runnable子类对象创建线程对象,并给线程命名 |
【了解】Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即为线这个目前我们了解即可 |
后面我会教两种方式观察线程,到时候就可以观察到,给线程的命名。
后面的实现多线程的本质都是上面两种方法。
3.使用匿名内部类来继承Thread类
本质就是第一种方法
public class demo3 { public static void main(String[] args) throws InterruptedException { //1.使用匿名内部类来继承Thread方法 Thread t = new Thread() { //2.重写run方法 @Override public void run() { while (true) { System.out.println("hello run"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; //3.创建线程 t.start(); while (true) { System.out.println("hello main"); Thread.sleep(1000); } } }
结果:
4.在创建Thread对象的时候传入,实现Runnable的接口
本质是第二种方法
public class demo4 { public static void main(String[] args) throws InterruptedException { //1.在创建Thread对象的时候,传入实现Runnable的匿名内部类 Thread t = new Thread(new Runnable() { //2.重写run方法 @Override public void run() { while (true) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }); //3.掉用线程 t.start(); while (true) { System.out.println("hello main"); Thread.sleep(1000); } } }
5.Lambda表达式实现Runnable
不了解Lambda的可以看我这篇文章
本质也是第二种方式
public class demo8 { public static void main(String[] args) throws InterruptedException { //1.实例化Thread对象时,传入实现了Runnable的匿名内部类 Thread thread = new Thread(() -> { while (true) { System.out.println("hello Thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); while (true) { System.out.println("hello main"); Thread.sleep(1000); } } }
结果:
更具第一种和第二种方式,其实还可以创建出好多种,这里就不一一举例了,每一种方式都很重要,都需要理解。
二、观察线程
1.方式一
双击打开jconsole文件 ,程序必须是在运行的情况下
点击连接
选择不安全的连接
选择线程
用这个观察非常有用,以后程序多了就需要要用到这样的工具
2.方式二
打断点开始调试
这里的线程默认命名也是Thread-?
如果我自己定义一个名字:
三、Thread常用方法
1.getId()
获取Thread对象的身份标识
public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println("hello Thread"); }); // 获取JVM分配的Id System.out.println(thread.getId()); thread.start(); // 获取JVM分配的Id System.out.println(thread.getId()); }
结果:
2.getName()
获取线程的名字,这个名字可以自己设置,如果不设置那他就会是: Thread-? 这样的形式,当线程多了后需要一个合理的线程名,方便调试
public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println("hello Thread"); }); thread.start(); //获取线程的名字 System.out.println(thread.getName()); }
结果:
如果我们自己设置线程名字:
public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println("hello Thread"); },"我定义的名字"); thread.start(); //获取线程的名字 System.out.println(thread.getName()); }
3. getState()
获取线程状态
3.1 NEW
当前Thread对象虽然有了,但是内核的线程还没有(还没调用start)
public static void main(String[] args) { Thread t = new Thread(() -> { for (int i = 0; i < 3; i++) { System.out.println("Hello Thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); //获取线程状态 System.out.println(t.getState()); t.start(); }
结果:
3.2 TERMINATED
当前内存虽然还在但是线程已经销毁(线程已经结束了)
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { System.out.println("Hello Thread"); }); t.start(); Thread.sleep(500); System.out.println(t.getState()); }
3.3 RUNNABLE
就绪状态。正在cpu上运行或者随时可以去cpu上运行
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (true) { } }); t.start(); Thread.sleep(500); System.out.println(t.getState()); }
3.4 BLOCKED
锁竞争造成的等待
public static void main(String[] args) throws InterruptedException { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) { System.out.println("t1 对locker1 加锁完成"); synchronized (locker2) { System.out.println("t1 对locker2 加锁完成"); } } }); Thread t2 = new Thread(() -> { synchronized (locker2) { System.out.println("t2 对locker2 加锁完成"); synchronized (locker1) { System.out.println("t1 对locker1 加锁完成"); } } }); t1.start(); t2.start(); t1.join(); t2.join(); }
结果:
程序一直没有结束,进入了阻塞:
我们可以通过jconsole观察:
3.5 TIMED_WAITING
有超时时间的等待,例如:sleep、带参数版本的join
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (true) { } }); t.start(); Thread.sleep(500); System.out.println(t.getState()); }
3.6 WAITING
没有超时时间的阻塞等待
public static void main(String[] args) throws InterruptedException { //获取主线程的线程对象 Thread mainThread = Thread.currentThread(); Thread t = new Thread(() -> { while (true) { System.out.println("主线程的状态: " + mainThread.getState()); } }); t.start(); //主线程等待t线程结束 t.join(); }
这里面有两个方法后面会将不懂可以看完后面再上来看
结果:
我们还可以通过jconsolo观察
4.getPriority()
获取线程优先级
优先级范围为 1(最低)~10(最高) ,默认值为5
public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println("hello Thread"); }); thread.start(); //获取线程的优先级 System.out.println(thread.getPriority()); }
6.isDaemon()
1.什么是前台线程
如果某个线程在执行过程中,能够阻止进程结束,此时这个线程就是“前台线程”,只要当进程中的所有前台线程都结束那么这个进程才可以结束。
public static void main(String[] args) { Thread t1 = new Thread(() -> { while (true) { System.out.println("hello Thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); // 是否为后台线程 System.out.println(t1.isDaemon()); }
2.什么是后台线程
如果这个线程执行过程中,不能阻止进程结束(虽然线程在执行着,但是进程要结束了,此时这个线程也会随之被带走),这样的线程就是“后台线程”。
public static void main(String[] args) { Thread t1 = new Thread(() -> { while (true) { System.out.println("hello Thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); //设置线程为后台线程,需要在start之前设置 t1.setDaemon(true); System.out.println(t1.isDaemon()); t1.start(); }
7.isAlive()
线程是否存活,为true表示内核线程存在,为false表示内核线程无了
代码中,创建Thread对象的生命周期,和内核中实际存在的线程是不一样的。因为Thread只是用来描述线程,真正创建线程的操作是调用start方法,start方法再根据你的操作系统类型,调用相应的API。
例如:
- 创建好了Thread对象,但是还没有调用start()方法
- 线程的run执行完了,内核线程就没有了,但是Thread对象仍然还在
我们用一段代码来感受一下:
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { //执行3次大概3秒 for (int i = 0; i < 3; i++) { System.out.println("Hello Thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); //还未创建线程 System.out.println(t1.isAlive()); t1.start(); //线程大概率是在运行的时候 System.out.println(t1.isAlive()); //大概5秒 Thread.sleep(5000); //到这里t1线程大概率是结束了 System.out.println(t1.isAlive()); }
8.isInterrupted() 和 interrupt() 和 currentThread()
isInterrupted()线程是否被中断,但是我认为不应该这么说,应该是线程是否被终止。因为中断有一种还可以启动的意思。
功能分析:(建议结合下面的代码学习)
- Thread.currentThread():这个方法是一个静态方法,可以直接通过类名调用。用于获取当前线程的对象,在哪个线程调用,就是获取的哪个线程的对象。
- 对象.isInterrupted():判断线程是否终止。Thread类里面有一个boolean类型的属性,表示是否被终止,默认值是false。
- 对象.interrupt():设置标志位,如果线程正在阻塞:并中断线程阻塞,例如:sleep、wait……,但是需要注意处理和被中断阻塞方法抛出的异常。将上面的这个属性置为true。
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { //获取当前线程 Thread currentThread = Thread.currentThread(); //判断是否中断了线程,默认为false while (!currentThread.isInterrupted()) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); //主线程希望5秒后t线程终止 Thread.sleep(5000); t.interrupt(); }
运行结果:
因为:
那很奇怪呀,我在main方法中想要停止线程,将标志位为改为true。为什么sleep还要清空标志位?
java为了防止“中间结果”,执行到一般停止了的情况,给程序员更多选择:
- 如果t线程想无视main,就直接在catch中什么都不做,程序会继续运行。(所有如果sleep不清空标志位,t线程就一定会结束)
- 如果t线程想要立即结束,就在catch中写return还在break
- 如果t线程想待会再结束,就可以在catch中写上一些收尾工作(比如释放资源,清理一些数据,提交结果……),执行完后再继续break或者return
注意如果sleep被Interrupt唤醒:
- 抛出异常
- 清空标志位(给程序员更多选择)