介绍
位于java.lang包下的Thread类是非常重要的线程类,它实现了Runnable接口,学习Thread类包括这些相关知识:线程的几种状态、上下文切换,Thread类中的方法的具体使用。
线程的状态
线程从创建到最终的消亡,要经历若干个状态,一般来说包括以下几个状态:
- 创建(new)
- 就绪(runnable)
- 运行(running)
- 阻塞(blocked)、主动睡眠(time waiting)、等待唤醒(waiting)
- 消亡(dead)
当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,譬如程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。
当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。
线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。
当由于突然中断或者子任务执行完毕,线程就会被消亡。
下面这副图描述了线程从创建到消亡之间的状态:
上下文切换
对于单核CPU来说(对于多核CPU,此处就理解为一个核),CUP在某一个时刻只能运行一个线程,当在运行一个线程的过程中去运行另外一个线程,这个就叫做线程上下文切换。
由于可能当前的线程并没有执行完,所以在切换时需要保存线程的运行状态,以便下次线程切换回来的时候能够以上次状态去继续运行,举个简单的列子,比如一个线程A正在读取某个文件的内容,读取到一半的时候,此时CPU需要切换线程去执行线程B,当再次切换回来执行A的时候,我们不希望线程A从头开始读取,因此需要记录线程A的运行状态,下次线程回复的时候,我们需要知道线程执行到第几条指令了,搜易需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
对于线程的上下文切换其实就是存储和回复CPU状态的过程,他使得线程能从断点处恢复执行。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。
Thread类中的方法
Thread类实现了Runnable接口,在Thread类中,有一些比较关键的属性,比如name是表示Thread的名字,可以通过Thread类的构造器中的参数来指定线程名字,priority表示线程的优先级(最大值为10,最小值为1,默认值为5),daemon表示线程是否是守护线程,target表示要执行的任务。
下面是Thread类中常用的方法:
1.start()
start()用来启动一个线程,当调用start()方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
2.run()
run()方法是不需要用户来调用的,当通过start()方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run()方法,在run()方法中定义具体要执行的任务。
3.sleep()
sleep方法有两个重载版本:
sleep(long millis) //参数为毫秒 |
sleep(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒 |
sleep方法相当于让当前线程睡眠,交出CPU,让CPU去执行其他的任务
当前线程调用sleep()方法进入阻塞状态后,在其睡眠期间,该线程不会获得执行机会,即是系统中没有其他可执行线程,因此sleep方法常用来暂停程序执行。
但是有一点需要注意,sleep()方法不会释放锁,也就是说如果当前线程持有某个对象的锁,调用sleep()方法,其他线程就无法访问这个对象。
代码示例:
public class ThreadDemo {
private int i = 0 ;
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo() ;
MyThread thread1 = threadDemo.new MyThread() ;
MyThread thread2 = threadDemo.new MyThread() ;
thread1.start();
thread2.start();
}
class MyThread extends Thread{
@Override
public void run() {
synchronized (ThreadDemo.class){
i ++ ;
System.out.println("线程:" + Thread.currentThread().getName() + " i = " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + "--睡醒 ");
}
}
}
}
打印结果
线程:Thread-0 i = 1
线程:Thread-0--睡醒
线程:Thread-1 i = 2
线程:Thread-1--睡醒
在这里可以看出当线程1调用sleep()方法睡眠时,线程2没有执行被synchronized 锁起来的代码,当线程1睡醒之后,执行完同步代码块里面的代码,线程2才执行,所以从这里可以看到sleep()方法持有当前同步代码块的锁。
4.interrupt()
interrupt()方法解释为中断线程,实际是为了对线程做一个中断标记,但是线程还是可能还是会执行,不立即,不强制,默认不终止。
interrput()方法是替换stop()方法,stop()方法已经弃用,为什么弃用呢?
是这样,线程是一点一点执行,任何时间都有可能发生线程切换,任何时间都可以调用stop()方法,这个线程就会立即停止,可以产生非常随机的中间状态,比如在某个时间切到别的线程再也切不回来了,比如正在改某一个对象时线程停止了,会造成不可预估的影响。
所以我们要使用interrupt()方法,让程序去判断在什么时候中断当前线程,这样就能保证代码的健壮性和程序的可控性。
既然interrupt()不能立即停止线程,那么怎么才能让线程按照我们的要求停止呢?
这里我要介绍俩个方法:
isInterrupted() |
Thread.interrupted() |
用法:
//用于判断当前线程是否为中断状态,不会重置状态
if(isInterrupted()){
//做一些收尾工作
return ;
}
//用于判断当前线程是否为中断状态,先调用isInterrupted(boolean ClearInterrupted)方法,然后重置状态,true变为false,false还是false
if(Thread.interrupted()){
//做一些收尾工作
return ;
}
而且interrupt()可以打断睡眠状态,立即抛出异常。
//判断是否中断线程
if(Thread.interrupted()){ //检查当前的线程,
//收尾工作
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
//收尾工作
}
5.yield()
yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入到就绪状态。即让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,当某个线程调用了yield()方法之后,只有优先级与当前线程相同或者比当前线程更高的处于就绪状态的线程才会获得执行机会。
注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
6.join()
join方法有三个重载版本:
join() |
join(long millis) //参数为毫秒 |
join(long millis,int nanoseconds) //第一参数为毫秒,第二参数为纳秒 |
假如在main线程中,调用thread.join()方法,则main()方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join()方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的事件。
代码示例:
public class ThreadDemo {
private int i = 0 ;
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo() ;
System.out.println("进入线程"+Thread.currentThread().getName());
MyThread thread1 = threadDemo.new MyThread() ;
thread1.start();
System.out.println("线程等待"+Thread.currentThread().getName());
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程继续执行"+Thread.currentThread().getName());
}
class MyThread extends Thread{
@Override
public void run() {
synchronized (ThreadDemo.class){
i ++ ;
System.out.println("线程:" + Thread.currentThread().getName() + " i = " + i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + "--睡醒 ");
}
}
}
}
打印结果:
进入线程main
线程:Thread-0 i = 1
线程等待main
线程:Thread-0--睡醒
线程继续执行main
根绝打印结果可以很清晰的看到,当子线程调用jion()方法之后,主线程main就等待子线程执行完,才继续执行。
7.获取线程属性的几个方法
- getId() 得到线程的ID
- getName和setName 用来得到或者设置线程名称。
- getPriority和setPriority 用来获取和设置线程优先级。
- setDaemon和isDaemon 用来设置线程是否成为守护线程和判断线程是否是守护线程。
守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程不依赖,举个简单的例子,如果在main()线程中创建一个守护线程,当main()方法执行结束之后,守护线程也会随之消亡。而用户线程不会,用户线程会一直运行直到运行完毕,在JVM中,像垃圾收集器线程就是守护线程。
5.currentThread() 用来获取当前的线程
8.wait()、notify()、notifyAll()
wait()、notify()、notifyAll()这三个方法不是Thread类中的方法,是Object本地的final方法,但是多线程中也是不可或缺的。
wait()、notify()、notifyAll()和synchronized是配合使用的。
wait()在synchronized中在对应monitor维护等待队列,会把当前的锁让开,其他线程也可以访问同一个synchronized里面的代码。
notify()会唤醒同一个monitor的wait(),让monitor去唤醒,notify()唤醒wait()不确定是哪一个,所以一般不适用notify()这个方法。
notifyAll() 是唤醒同一个moitor所有的wait(),被唤醒后,需要到monitor的执行队列中等待,等待拿锁,拿锁后从wait()位置继续执行。
代码示例:
public class WaitDemo {
private String sharedString ;
private synchronized void initString(){
System.out.println(Thread.currentThread().getName() + "--- 赋值,并且唤醒等待线程");
sharedString = "Rengwuxing";
notifyAll(); //唤醒等待线程
}
private synchronized void printString(){
//if(sharedString == null){
//循环判断,当前线程被唤醒之后,在判断一次 是否为空,
// 因为不确定被谁唤醒,有可能是其他的notifyAll 唤醒,保证了代码的健壮性
while(sharedString == null){
try {
System.out.println(Thread.currentThread().getName() + "--- 等待");
wait(); // 等待
System.out.println(Thread.currentThread().getName() + "--- 被唤醒,继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("String: " + sharedString);
}
public void runTest(){
Thread thread1 = new Thread(){
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "--- 睡二秒");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
initString();
}
} ;
thread1.start();
Thread thread2 = new Thread(){
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "--- 睡一秒");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
printString();
}
} ;
thread2.start();
}
public static void main(String[] args) {
WaitDemo waitDemo = new WaitDemo() ;
waitDemo.runTest();
}
}
运行结果:
Thread-0--- 睡二秒
Thread-1--- 睡一秒
Thread-1--- 等待
Thread-0--- 赋值,并且唤醒等待线程
Thread-1--- 被唤醒,继续执行
String: Rengwuxing
从运行结果可以看出,线程1 睡一秒后执行,然后发现sharedString==null,所以就等待,调用wait(),线程0睡两秒执行,给sharedString赋值之后,告诉所有等待的线程我给他赋值了,不用等了,调用notifyAll(),线程1接到通知,但是不相信,自己又去判断了一次,发现真的赋值了,才执行下面的代码。
这个就是使用while判断和if判断的区别,如果用if判断,被唤醒之后就不会去检查是否为空,用while判断如果为空,我还继续等着,直到不为空为止。
在上面已经说到了Thread类中的大部分方法,那么Thread类中的方法调用到底会引起线程状态发生怎样的变化呢?下面一幅图就是在上面的图上进行改进而来的: