了解了线程的基础使用后,我们需要学习一下线程的生命周期,首先看如下视图
线程的基本状态
线程有5种基本状态:
- 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
- 就绪状态(Runnable):当调用线程对象的
start()
方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了start()方法此线程立即就会执行; - 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
- 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 等待阻塞:运行状态中的线程执行
wait()
方法,使本线程进入到等待阻塞状态; - 同步阻塞 – 线程在获取
synchronized同步锁
失败(因为锁被其它线程所占用),它会进入同步阻塞状态; - 其他阻塞 – 通过调用线程的
sleep()
或join()
或发出了I/O
请求时,线程会进入到阻塞状态。当sleep()
状态超时、join()
等待线程终止或者超时、或者I/O
处理完毕时,线程重新转入就绪状态。
- 等待阻塞:运行状态中的线程执行
- 死亡状态(Dead):线程执行完了或者因异常退出了
run()
方法,该线程结束生命周期。
线程状态转换
- 就绪状态转换为运行状态:当此线程得到处理器资源;
- 运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。
- 运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。
- 注意:
- 执行线程的
yield()
方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()
方法后,接下来CPU仍然调度了A线程的情况。
- 执行线程的
常用的线程函数
- join():指等待t线程终止
join()
是Thread
类的一个方法,启动线程后直接调用,即join()
的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()
方法后面的代码,只有等到子线程结束了才能执行。在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()
方法了。- 实例:
- 不加join()
class Thread1 extends Thread{
private String name;
public Thread1(String name) {
super(name);
this.name=name;
}
public void run() {
System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
for (int i = 0; i < 5; i++) {
System.out.println("子线程"+name + "运行 : " + i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
}
}
public class Main {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
}
}
运行结果:
main主线程运行开始!
main主线程运行结束!
B 线程运行开始!
子线程B运行 : 0
A 线程运行开始!
子线程A运行 : 0
子线程B运行 : 1
子线程A运行 : 1
子线程A运行 : 2
子线程A运行 : 3
子线程A运行 : 4
A 线程运行结束!
子线程B运行 : 2
子线程B运行 : 3
子线程B运行 : 4
B 线程运行结束!
发现主线程比子线程早结束
* 加jioin()
public class Main {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
try {
mTh1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
mTh2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
}
}
运行结果:
main主线程运行开始!
A 线程运行开始!
子线程A运行 : 0
B 线程运行开始!
子线程B运行 : 0
子线程A运行 : 1
子线程B运行 : 1
子线程A运行 : 2
子线程B运行 : 2
子线程A运行 : 3
子线程B运行 : 3
子线程A运行 : 4
子线程B运行 : 4
A 线程运行结束!
B 线程运行结束!
main主线程运行结束!
主线程一定会等子线程都结束了才结束
* **yield()**:暂停当前正在执行的线程对象,并执行其他线程。
* `yield()`应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用`yield()`的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证`yield()`达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
* `yield()`从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,`yield()`将导致线程从运行状态转到可运行状态,但有可能没有效果。可看上面的图。
* 实例:
class ThreadYield extends Thread{
public ThreadYield(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 50; i++) {
System.out.println("" + this.getName() + "-----" + i);
// 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
if (i ==30) {
this.yield();
}
}
}
}
public class Main {
public static void main(String[] args) {
ThreadYield yt1 = new ThreadYield("张三");
ThreadYield yt2 = new ThreadYield("李四");
yt1.start();
yt2.start();
}
}
运行结果:
第一种情况:李四(线程)当执行到30时会CPU时间让掉,这时张三(线程)抢到CPU时间并执行。
第二种情况:李四(线程)当执行到30时会CPU时间让掉,这时李四(线程)抢到CPU时间并执行。
* **sleep()和yield()的区别**
* `sleep()`使当前线程进入停滞状态,所以执行`sleep()`的线程在指定的时间内肯定不会被执行
* `yield()`只是使当前线程重新回到可执行状态,所以执行`yield()`的线程有可能在进入到可执行状态后马上又被执行
* `sleep()`方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的
* `yield()`方法使当前线程让出CPU占有权,但让出的时间是不可设定的
* 实际上,`yield()`方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把CPU的占有权交给此线程,否则,继续运行原来的线程。所以`yield()`方法称为“退让”,它把运行机会让给了同等优先级的其他线程
* 另外,`sleep()`方法允许较低优先级的线程获得运行机会,但`yield()`方法执行时,当前线程仍处在可运行状态,所以,不可能让较低优先级的线程获得CPU占有权。在一个运行系统中,如果较高优先级的线程没有调用sleep方法,又没有受到I\\O阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
* **setPriority(): 设定线程的优先级**
* MIN\_PRIORITY = 1
* NORM\_PRIORITY = 5
* MAX\_PRIORITY = 10
* 用法:
MyThread t1 = new MyThread("t1");
MyThread t2 = new MyThread("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
* **interrupt():向线程发送中断信号**
* 它不是直接中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出,从而结束线程
* **sleep()**:
* `sleep()`是`Thread`类的方法,使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用,目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会
* `sleep()`是`Thread`类的`static`方法;因此他不能改变对象的对象锁,所以当在一个`synchronized`块中调用`sleep()`方法时,线程虽然休眠了,但是对象的机锁并没有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
* 在`sleep()`休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
* **wait()**
* `wait()`方法是`Object`类里的方法;当一个线程执行到`wait()`方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁),使其他线程可以获取这个对象的锁并访问这个对象
* `wait()`使用`notify()`或者`notifyAlll()`或者指定睡眠时间来唤醒当前等待池中的线程。
* `wait()`必须放在`synchronized语句块`中,否则会在程序运行时抛出`java.lang.IllegalMonitorStateException`异常。
* **sleep()和wait()方法的区别**
* 同步:`sleep()`不需要在同步方法或同步块中调用,`wain()`只能在同步上下文中调用,否则抛出`IllegalMonitorStateException`异常
* 作用对象:`sleep()`方法定义在`java.lang.Thread`中,作用于当前线程,`wait()`方法定义在`Object`类中,作用于对象本身
* 释放锁资源:`sleep()`睡眠时,保持对象锁,仍然占有该锁,而`wait()`睡眠时,释放对象锁
* 唤醒条件:
* sleep():超时或者调用`interrupt()`方法体
* wait():其他线程调用对象的`notify()`或者`notifyAll()`方法或者调用`interrupt()`方法抛出异常
* 方法属性:`sleep()`是`Thread类`的静态方法,`wait()`是`Object类`的实例方法
* **为什么wait()方法调用要在synchronized方法/语句块内**
* 调用·wait()·就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁。
* `notify()`,`notifyAll()`是将锁交给含有`wait()`方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢(本质是让处于入口队列的线程竞争锁)
* 详解:每个对象都又一个独占锁,在同步中,多线程中线程相互竞争的都是这个独占锁,只有获取独占锁才可以执行同步方法,如果这个独占锁被其他线程占用,那么另外一个调用该同步方法的线程就会处于阻塞状态,此线程进入入口队列。若一个拥有该独占锁的线程调用该对象同步方法的wait()方法,则该线程会释放独占锁,并加入对象的等待队列。某个线程调用notify(),notifyAll()方法是将等待队列的线程转移到入口队列,然后让他们竞争锁,所以这个调用线程本身必须拥有锁。
* **synchronized应用举例:生产者消费者模型**
* 消费者线程需要等待直到生产者线程完成一次写入操作。
* 生产者线程需要等待消费者线程完成一次读取操作。
* 假设没有应用Synchronized关键字,当消费者线程执行wait操作的同时,生产线线程执行notify,生产者线程可能在等待队列中找不到消费者线程。导致消费者线程一直处于阻塞状态。那么这个模型就要失败了。所以必须要加Synchronized关键字。