一、从最简单的例子说起
再开始讲解java线程的api我们还需要先对线程有一个回顾和了解。对此,给出一个最基本的线程案例。
public class MyThread extends Thread { private String name; public MyThread(String name) { super(); this.name = name; } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(name + ":正在执行!" + i); } } }
我们在这里定义了一个线程,然后在run方法中循环输出i。然后我们就可以像使用其他类一样去使用了。
public class Test { public static void main(String[] args) { MyThread myThread = new MyThread("线程一"); myThread.start(); } }
这就是一个最简单的例子,我们可能会想,线程好像也没啥特殊的,new出来一个对象然后执行就结束了。那他的生命周期会不会也是和普通类一样呢?再继续往下看:
二、线程的生命周期
1、思考一个问题
在上面我们new出来一个线程实例,然后调用start方法之后,这个线程就开始执行了嘛?
在上一篇文章曾经说过,这和CPU的时间片轮询有关,CPU需要在多条线程之间切换,轮到他他才会执行。因此呢,当调用start方法之后,线程只能说有可能在执行。
2、线程的生命周期
对于线程来说,它的一生要经过五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(terminated)。
在网上看过N多个版本的线程生命周期状态图,自己也使用画图工具花了一个。
这张图基本上涵盖了线程的整个生命周期。上面的123456序号表示实现状态之间转换的方法,比如说调用了线程的某些API方法,或者是外部环境的一些改变,都能够使得线程从一个状态转变到另外一个状态,下面我们对这五个状态分别来解释一下;
(1)new状态
也就是说,在这里我们只是实例化了线程,还没有start。此时的Thread和普通的类完全没有区别。因为在这里new出来一个对象却没有使用,什么也干不了。start方法被调用之后就进入了框框里面的两个状态。
(2)runnable状态
这个状态表示没有获取CPU执行权,也就是CPU时间片还没有轮询到这个线程,此时的线程表示有了能够执行得能力,但是还没有轮到他执行而已。
(3)running状态
这个状态表示获得了CPU的执行权,然后线程里面的run方法就开始执行了。
(4)blocked状态
这个状态表示的是阻塞状态,为什么会出现这个状态呢?有常见的3个原因:
原因一:从running状态转变而来,因为调用了线程调用了sleep或者是wait方法。
原因二:突然执行了IO操作,比如文件保存或者是读取了网络文件等等。
原因三:想要获取某一个锁,但是当前获取锁的线程太多,于是去排队了。
在这个blocked状态中,可以直接到terminated状态或者是回到running状态。
(5)terminated状态
这个状态表示线程的生命周期就要结束了。
在这里上面的123456里面的内容,在我们讲解线程api的时候再进行理解,因为在这里直接列出来是什么原因造成的状态之间切换的话,到了下面就忘了。
注意:这张图时刻牢记着
三、线程常见的API
1、休眠sleep
这种方式会使得线程从running状态转变到runnable状态或者是blocked状态。
用法很简单,直接让线程去调用执行就OK了,会抛出一个InterruptedException异常:
public class Test { public static void main(String[] args) { MyThread myThread = new MyThread("线程一"); myThread.start(); try { //表示会休眠1秒钟 myThread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
不过目前的这种做法正在被TimeUnit所取代,比如我们想要休眠的话,我们可以直接这样:
public class Test { public static void main(String[] args) { MyThread myThread = new MyThread("线程一"); myThread.start(); try { //单位是秒,直接写1即可 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } }
当然还有分钟、小时、天等单位。
2、中断interrupt
这种方式会线程blocked状态转变到其他状态,比如说runnable、terminated和running状态。
意思是打断当前线程的阻塞状态,就好比你正在睡觉,突然有人把你吵醒了。代码演示一下:
public class Test { public static void main(String[] args) { MyThread myThread = new MyThread("线程一"); //myThread企图休眠一分钟 myThread.start(); try { //单位是秒,直接写1即可 TimeUnit.SECONDS.sleep(2); //休眠两秒就被中断了 myThread.interrupt(); } catch (InterruptedException e) { e.printStackTrace(); } } }
在myThread中企图休眠一分钟,但是在主线程main中,休眠两秒就被打断了,此时就会报错:
java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) at com.fdd.chapter2.MyThread.run(MyThread.java:17)
上面异常的意思就是睡眠被打断了。这个方法为什么能够中断线程呢?我们来深入的理解一下:
(1)中断线程相关的方法
在线程内部有一个中断flag,如果我们执行了interrupt方法,那么这个标签将会被置为true。表示此线程被打上了中断标记,与线程中断的api还有两个:
其中,interrupt 方法是唯一能将中断状态设置为 true 的方法。静态方法 interrupted 会将当前线程的中断状态清除,但这个方法的命名极不直观,很容易造成误解,需要特别注意。
(2)如何中断一个线程
在这里你可能有一个疑问,上面不是已经说调用中断方法就会中断一个线程?你先来看看下面中断失败的例子:
public class Test { public static void main(String[] args) { Thread thread = new Thread(() -> { while (true) { } }); thread.start(); //我们尝试中断这个线程 thread.interrupt(); } }
我们尝试中断这个线程,会发现不起任何作用,这是因为你只是去中断线程,但是线程里面没有对其进行回应,就好比说别人在吃饭,你说我要把你的饭端走,别人懒得理你,继续吃一样。
所以要想中断一个线程,不仅仅要给线程一个中断信号,线程也要能够积极地响应这个中断信号。
public class Test { public static void main(String[] args) { Thread thread = new Thread(() -> { while (true) { // 响应中断信号 if (Thread.currentThread().isInterrupted()) { //判断当前线程是否被中断 return; } } }); thread.start(); thread.interrupt(); } }
上面的这个例子中,线程内部积极的相应了外部的中断信号,就能够中断线程了。现在相信你稍微有点豁然开朗了
3、yied方法
这种方式会使得线程从running状态转变到runnable状态。
意思是线程告诉调度器,愿意放弃当前的CPU资源,你可以去执行其他的线程了。当然调度器比较忙的时候,可以不用搭理这个线程的请求。
public class Test { public static void main(String[] args) { MyThread myThread = new MyThread("线程一"); myThread.start(); try { myThread.yield(); } catch (InterruptedException e) { e.printStackTrace(); } } }
4、线程优先级
(1)设置线程优先级
优先级高的话表示线程优先调用,但不是绝对的被优先调用。就好比是线程告诉调度器,想要更高的权限去获取CPU资源,但是调度器同样可以忽略他的请求。
优先级有1到10。数字越大优先级越大。
public class Test { public static void main(String[] args) { Thread thread1 = new Thread(() -> { System.out.println("优先级4"); }); Thread thread2 = new Thread(() -> { System.out.println("优先级8"); }); thread1.setPriority(4); thread2.setPriority(8); thread1.start(); thread2.start(); } }
在这里线程2就可能比线程1先执行。
(2)获取优先级
public class Test { public static void main(String[] args) { Thread thread1 = new Thread(() -> {}); Thread thread2 = new Thread(() -> {}); System.out.println(thread1.getPriority()); System.out.println(thread2.getPriority()); thread1.start(); thread2.start(); } }
这两个线程的优先级如果不提前设置都是5.
5、join方法
此方法可以使其他线程从running状态转换到blocked状态。
join线程A之后,其他的线程必须要等待线程A执行结束才可以执行。此时其他的线程一直处于bloecked。我们可以看一下例子:
新建一个线程:
public class MyThread extends Thread { private String name; public MyThread(String name) { super(); this.name = name; } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(name+i); } } }
然后我们使用一下join方法:
public class Test { public static void main(String[] args) { MyThread thread1 = new MyThread("线程A"); MyThread thread2 = new MyThread("线程B"); thread1.start(); try { thread2.start(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
在这里就是让线程B先执行完再执行线程A,但这里并不是绝对的,因为在start线程B之前,可能CPU刚好执行了线程A,于是输出了一个线程A,然后轮到线程B的时候,就会一下子全部输出。
6、getId获得线程ID
线程的ID在整个jvm中都是唯一的。
public class Test { public static void main(String[] args) { Thread thread = new Thread(()->{}) ; System.out.println(thread.getId()); } }
要注意,在这里输出的线程ID不是从0开始的,因为再执行一个程序的时候,jvm会默认开辟很多个 线程。
7、获取当前线程
public class Test { public static void main(String[] args) { Thread thread = new Thread(()->{}) ; System.out.println(Thread.currentThread()); } } //输出: Thread[main,5,main]
以上就是常用的一些api。还有一个stop关闭线程的方法,但是一般不会用到。
到现在为止我们已经分析了其生命周期和常用的API,但是还有一点就是我们好像还没说过线程的源码,下一节就对线程的源码进行一个分析,最主要的就是其构造函数的使用,也揭晓一下为什么调用start方法就启动了一个线程。