一、Thread 的常⻅构造⽅法
最后一个线程组的概念是java的概念,和系统中的线程组不一样,不是同一个东西。
第一个和第二个构造方法在初阶一有介绍,
第三是创建一个线程,并且可以给这个线程命名,并不是它默认的Thread-0 / 1 / 2.....,第四个也是可以给线程对象命名,不过是使用接口的方法。
继承Thread代码:
class MyThread extends Thread { @Override public void run() { System.out.println("hello thread1"); } String name; public MyThread(String name) { super(name); this.name = name; } } public class ThreadDemo1 { public static void main(String[] args) { Thread t1 = new MyThread("这是我的线程"); t1.start(); Thread t2 = new Thread("这是我的线程"){ @Override public void run() { System.out.println("hello thread2"); } }; t2.start(); } }
执行效果:
两个线程:
上面两种的两种创建线程方式都可以。
实现Runnable接口代码:
class MyThread2 extends Thread { String name; public MyThread2(String name) { super(name); this.name = name; } @Override public void run() { System.out.println("hello thread1"); } } public class TestDemo2 { public static void main(String[] args) { Thread t1 = new MyThread2("这是我的线程1"); t1.start(); Thread t2 = new Thread("这是我的进程2") { @Override public void run() { System.out.println("hello thread2"); } }; t2.start(); } }
执行效果:
两个线程:
注意:我们创建的线程如果不起名字,默认是Thread-0 1 2 3....,给不同线程起不同名字,对于线程的执行,没啥影响,主要是为了方便调试;线程之间的名字是可以重复的,在同一个工作中,需要多个线程完成,都可以起一样的名字;但是名字也不要乱起,最后要有一定的描述性。
二、Thread 的⼏个常⻅属性
1、id:
jvm自动分配的身份标识,会保证唯一性。
2、获取线程的名字。
3、进程的状态:
就绪状态、阻塞状态;线程也有状态,Java中对线程的状态进行了进一步的区分(比系统原生的状态更丰富一些)。
4、在java中设置的优先级,
效果不是很明显,因为系统是随机调度的(对内核的调度器调度过程会产生一些影响)。
5、是否后台线程,
也称为是否守护线程(比较抽象),所以记住和理解是否后台线程会轻松一些,与此相反,也有前台线程(和Android上的前台app,后台app完全不同)。
后台线程和前台线程的区别:前台线程的运行,会阻止进程结束
后台线程的运行,不会阻止进程结束
我们创建的线程,默认是前台进程。
如下代码:
public class TestDemo3 { public static void main(String[] args) { Thread t = new Thread() { @Override public void run() { while (true) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }; t.start(); } }
执行效果:会不停的输出打印hello thread
那我们试着把这个线程设置为后台线程试试(默认为前台),代码:
public class TestDemo3 { public static void main(String[] args) { Thread t = new Thread() { @Override public void run() { while (true) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }; //在start之前,设置线程为后台线程(不能在start之后设置) t.setDaemon(true); t.start(); } }
执行效果:
根本就没有打印hello thread,进程就结束了,因为是后台进程,不会阻止进程的结束。
设为true是后台,后台,可以理解为背后,不出面的人,你感知不到;后台不会阻止进程结束。
不设为true是前台,前台,可以理解为明面上的人,你能感知到;前台会阻止进程结束。
6、是否存活,
isAlive( )表示内核中的线程(PCB)是否还存在。java代码中定义的线程对象(Thread)实现,虽然是表示一个线程,但这个对象本身的生命周期,和内核中的PCB生命周期 ,是不完全一样的。
isAlive的测试,代码:
public class TestDemo4 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } }); System.out.println("start之前:" + t.isAlive()); t.start(); System.out.println("start之后" + t.isAlive()); Thread.sleep(2000); System.out.println("t结束之后" + t.isAlive()); } }
执行效果:
解释:启动线程之前,也就是start之前,t这个实例线程是还没开始的,所以isAlive返回的是false,start后就查看这个线程存不存在,因为线程也刚启动,所以isAlive返回的是true,休眠两秒之后,t线程已经跑完了,所以isAlive返回的也是false。
7、是否中断,
也是终止的意思。
下面写一个代码,用boolean类型的变量来控制while循环,也起到终止线程的作用。
public class ThreadDemo1 { private static boolean isQuit = false; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (!isQuit) { System.out.println("我是一个线程,正在工作"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("线程工作完毕"); }); t.start(); Thread.sleep(3000); System.out.println("让线程结束"); isQuit = true; } }
注意:这里的isQuit变量放在类中,不放在方法里作为局部变量的原因是涉及到了变量捕获,如果放在main方法里,是局部变量,就必须要加 final 修饰将其变成常量,为什么呢?这时因为main线程和我们创建的线程的栈帧生命周期不同,如果main线程先结束了,我们创建的线程要获取这个变量,但是我们main线程的栈帧生命周期已经结束了,我们拿不到这个变量,java这里的做法就比较暴力,直接把这个变量变成常量,要么这个变量就是全局变量,以至于不会发生上面的情况。
执行效果:
这时,我们也可以使用jdk里自带的方法:isInterrupted( )
代码如下:
public class ThreadDemo2 { 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(); } } System.out.println("线程执行完毕"); }); t.start(); Thread.sleep(3000); System.out.println("让线程结束"); t.interrupt(); } }
执行效果:
这个线程并不会结束,会继续执行。
解释:可以看到这里抛出了个异常,而这个异常是InterruptedException 异常
如果没有sleep,interrupt是可以让线程顺利结束的,但是有了sleep就会引起变数。
这说明,sleep这出现问题了,为什么呢?原因是这个线程还在执行,main线程休眠3秒后,终止了这个线程,但sleep还没休眠够1秒,这里就会提前唤醒。
这里,提前唤醒,会做两件事:
1、抛出 InterruptedException (紧接着就会被catch获取到)。
2、清除 Thread 对象的 isInterrupt 标志位。(sleep清空标志位,是为了给程序猿更多的“可操作性空间”)。
所以,interrupt已经把标志位设置为true了,sleep被提前唤醒,清除了isInterrupt标志位后,就又把标志位设回了false,这个while循环还是能进去的,所以线程还在继续执行。
此时,程序猿就可以在catch语句中加入一些代码,来做一些处理:
1、让线程立即结束。(加上break)
2、让线程不结束,继续执行。(不加break)
3、让线程执行一些逻辑后,再结束。(写一些其他代码,再加break)
对于一个服务器程序来说,稳定性很重要,这些所谓的“问题”,在java中就会以异常的形式体现出来,可以通过catch语句,对这些异常进行处理。主要的几种处理方式:
1、尝试自动恢复
2、记录日志
3、发出警报
8、等待线程(结束),
join( )。我们知道,多线程的执行顺序是不同的(随机调度,抢占式执行),有不可预期性,虽然线程的调度是无序的,但是我们可以调用一些api,来影响带线程的执行顺序,join就是一种方式,影响 线程结束 的先后顺序。比如:t2 线程等待 t1,这时,一定是 t1先结束,t2 后结束。这里的join就会使 t2 线程阻塞。
代码:
public class ThreadDemo3 { public static void main(String[] args) { Thread t = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("我是一个线程,正在工作"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("线程结束"); }); t.start(); //t.join(); System.out.println("这时一个主线程,在t线程结束后再执行"); } }
我们想让t线程执行完后再执行主线程,如果不写join方法。因为多线程的调度是随机性的,执行效果如下:
执行效果和我们预期的不一样,加上join方法后,
代码:
public class ThreadDemo3 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("我是一个线程,正在工作"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("线程结束"); }); t.start(); t.join(); System.out.println("这时一个主线程,在t线程结束后再执行"); } }
执行效果:
结果和我们想要预期效果相同。这里的join的意思是:让 t 线程执行完,再执行本线程。
这里是让main线程主动放弃去调度器调度,t 线程执行完后,main线程才执行,这也代表哪个线程调用join,哪个线程就是阻塞等待。这里就有了先后顺序。
注意:这里的先后顺序和优先级不同,优先级是系统调度器,在内核中完成的工作,即使优先级有差异,但是每个线程的执行顺序还是随机的(控制不了)。
等待线程结束还有两个重载方法,里面放的是要等待的时间,如图:
如果join不加参数,就是死等,第一个加参数的就是:等待多少毫秒,第二个加参数:数值精确到纳秒,不过很少用,因为计算机精确不到这么小。加参数是带有时间的等。
注意,有时候不想等,我们也可以不等,例如加 interrupt,可以终止这个等待,我们也可以在idea看到join的实现,是throws了 InterruptedException的。
9、获取线程的引用,currentThread()
我们对于一个类继承Thread是可以通过this拿到线程的实例的,
代码如下:
class MyThread extends Thread { @Override public void run() { System.out.println("id:"+ this.getId() + " name:" + this.getName()); } } public class ThreadDemo1 { public static void main(String[] args) { Thread t = new MyThread(); t.start(); } }
效果如下:
但是,如果是接口或者我们使用匿名内部类 / lambda表达式,就不能获取到Thread的引用了,这时,Thread已经给我们提供了方法获取引用了:Thread.currentThread()。如下展示:
代码:
public class ThreadDemo2 { public static void main(String[] args) { Thread t1 = new Thread(() -> { Thread t = Thread.currentThread(); System.out.println(t.getId()); }); Thread t2 = new Thread(() -> { Thread t = Thread.currentThread(); System.out.println(t.getName()); }); t1.start(); t2.start(); } }
效果如下: