在上一篇文章中我们介绍了线程创建和线程状态的相关知识 :线程的基本状态,如何创建线程,以及线程的生命周期。 我们已经知道,进程和线程都是一个时间段的描述,是CPU工作时间段的描述。进程是程序的执行过程,线程是计算机系统的最小执行单元,是进程中的一个个小任务。如下图,形象生动地表述了进程和线程的关系。
线程的创建有两种方式,继承Thread类或者实现Runnable接口。当我们创建好一个线程之后,线程处于新建状态。在线程的整个生命周期中,包括新建,就绪,运行,阻塞和死亡等。利用已有知识我们已经可以写出如何利用多线程处理大量任务这样简单的程序。但是当应用场景复杂时,我们还需要从管理控制入手,更好的操纵多线程,包括:对线程的等待、守护线程、线程的睡眠、线程的突然停止、线程的让步、线程的优先级等。
当我们开始应用线程来解决工作中的相关问题时,
(1)如何让一个线程按照我们的设想来操作复杂情形,如线程的等待、守护线程、线程的睡眠等。
(2)多个线程被cpu调用时,如何线程不同线程的调用顺序呢?
(2)线程的特点在于资源共享,当多个线程操作同一数据会使得数据混乱,那么,应该如何处理这种情况呢?
(一)线程的常用方法体介绍
-
线程的等待和唤醒
在Object.java中,定义了wait(), notify()和notifyAll()等方法,用来实现线程的等待和唤醒。
wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。
notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;
notify()是唤醒单个线程—————— 唤醒在此对象监视器上等待的单个线程。
notifyAll()是唤醒所有的线程————-- 唤醒在此对象监视器上等待的所有线程。
wait() -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)。
wait(long timeout) -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
wait(long timeout, int nanos) -- 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量”,当前线程被唤醒(进入“就绪状态”)。
package myTest;
public class TestDemo {
//定义三个线程t1,t2,t3,同时还有一个主线程mian();应该注意的是
//当启用start()方法时,程序执行ThreadA中的run方法,最重要的
//是wait()方法的作用。
private static Object obj = new Object();
public static void main(String[] args) {
ThreadA t1 = new ThreadA("t1");
ThreadA t2 = new ThreadA("t2");
ThreadA t3 = new ThreadA("t3");
t1.start();
t2.start();
t3.start();
try {
System.out.println(Thread.currentThread().getName()+" sleep(3000)");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(obj) {
// 主线程等待唤醒。
System.out.println(Thread.currentThread().getName()+" notifyAll()");
obj.notifyAll();
}
}
static class ThreadA extends Thread{
public ThreadA(String name){
super(name);
}
public void run() {
synchronized (obj) {
try {
// 打印输出结果
System.out.println(Thread.currentThread().getName() + " wait");
// 唤醒当前的wait线程
obj.wait();
// 打印输出结果
System.out.println(Thread.currentThread().getName() + " continue");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果是:
代码分析:
参考下面的流程图。
(01) 主线程中新建并且启动了3个线程"t1", "t2"和"t3"。
(02) 主线程通过sleep(3000)休眠3秒。在主线程休眠3秒的过程中,我们假设"t1", "t2"和"t3"这3个线程都运行了。以"t1"为例,当它运行的时候,它会执行obj.wait()等待其它线程通过notify()或额nofityAll()来唤醒它;相同的道理,"t2"和"t3"也会等待其它线程通过nofity()或nofityAll()来唤醒它们。
(03) 主线程休眠3秒之后,接着运行。执行 obj.notifyAll() 唤醒obj上的等待线程,即唤醒"t1", "t2"和"t3"这3个线程。 紧接着,主线程的synchronized(obj)运行完毕之后,主线程释放“obj锁”。这样,"t1", "t2"和"t3"就可以获取“obj锁”而继续运行了!
wait()的作用是让“当前线程”等待,而“当前线程”是指正在cpu上运行的线程!
这也意味着,虽然t1.wait()是通过“线程t1”调用的wait()方法,但是调用t1.wait()的地方是在“主线程main”中。而主线程必须是“当前线程”,也就是运行状态,才可以执行t1.wait()。所以,此时的“当前线程”是“主线程main”!因此,t1.wait()是让“主线程”等待,而不是“线程t1”!
注:
在上面的分析中,我们将wait()和notify()在Object对象中,而不是Thread中,这是为什么呢?
Object中的wait(), notify()等函数,和synchronized一样,会对“对象的同步锁”进行操作。
wait()会使“当前线程”等待,因为线程进入等待状态,线程应该释放它锁持有的“同步锁”,否则其它线程获取不到该“同步锁”而无法运行!OK,线程调用wait()之后,会释放它锁持有的“同步锁”;而且,根据前面的介绍,我们知道:等待线程可以被notify()或notifyAll()唤醒。
现在,请思考一个问题:notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据“对象的同步锁”。负责唤醒等待线程的那个线程(我们称为“唤醒线程”),它只有在获取“该对象的同步锁”(这里的同步锁必须和等待线程的同步锁是同一个),并且调用notify()或notifyAll()方法之后,才能唤醒等待线程。虽然,等待线程被唤醒;但是,它不能立刻执行,因为唤醒线程还持有“该对象的同步锁”。必须等到唤醒线程释放了“对象的同步锁”之后,等待线程才能获取到“对象的同步锁”进而继续运行。
总之,notify(), wait()依赖于“同步锁”,而“同步锁”是对象锁持有,并且每个对象有且仅有一个!
2. 线程的让步和休眠
在Object.java中,定义了 sleep()和yield()方法来实现线程的休眠和让步。
- sleep() 定义在Thread.java中。
sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。也就是说,让当前线 程暂停一下,当前线程随之进入阻塞状态,当睡眠时间结束后,当前线程重新进入就绪状态,开始新一轮的抢占计 划。
常用的两种方法:
sleep(long millis)
sleep(long millis,int nanos)
代码示例:
sleep()方法和wait()的比较
/*
* wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁
*sleep()的作用是也是让当前线程由“运行状态”进入到“休眠(阻塞)状态”。
* wait()会释放对象的同步锁,而sleep()则不会释放锁。
*/
public class TestDemo{
private static Object obj = new Object();
public static void main(String[] args){
ThreadA t1 = new ThreadA("t1");
ThreadA t2 = new ThreadA("t2");
t1.start();
t2.start();
}
static class ThreadA extends Thread{
public ThreadA(String name){
super(name);
}
public void run(){
// 获取obj对象的同步锁
synchronized (obj) {
try {
for(int i=0; i <10; i++){
System.out.printf("%s: %d\n", this.getName(), i);
// i能被4整除时,休眠100毫秒
if (i%4 == 0)
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果如下:
结果说明:
主线程main中启动了两个线程t1和t2。t1和t2在run()会引用同一个对象的同步锁,即synchronized(obj)。在t1运行过程中,虽然它会调用Thread.sleep(100);但是,t2是不会获取cpu执行权的。因为,t1并没有释放“obj所持有的同步锁”!如果说我们注释掉同步锁synchronized(obj) ,则运行结果是
可以看得出线程1和线程2在争夺时间片,因而运行结果不是先t1后t2.
-
yield()介绍
yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行! yield()方法不会释放锁。
所有的这一切都是由于事情的紧急性当前正在处理的线程被搁置起来,我们(cpu)处理当前的紧急事务。在软件开发中,也有类似 的场景,比如一条线程处理的任务过大,其他线程始终无法抢占到资源,这时候我们就要主动的进行让步,给其他线程一个公平 抢占的机会。
class ThreadA extends Thread{
public ThreadA(String name){
super(name);
}
public synchronized void run(){
for(int i=0; i <10; i++){
System.out.printf("%s [%d]:%d\n", this.getName(), this.getPriority(), i);
// i整除4时,调用yield
if (i%4 == 0)
Thread.yield(); //调用yield方法
}
}
}
public class TestDemo{
public static void main(String[] args){
ThreadA t1 = new ThreadA("t1");
ThreadA t2 = new ThreadA("t2");
t1.start();
t2.start();
}
}
运行过结果如下:
结果说明:
以上是某一次的运行状态,让步方法什维利让优先级的更高的线程得到处理。“线程t1”在能被4整数的时候,并没有切换到“线程t2”。这表明,yield()虽然可以让线程由“运行状态”进入到“就绪状态”;但是,它不一定会让其它线程获取CPU执行权(即,其它线程进入到“运行状态”),即使这个“其它线程”与当前调用yield()的线程具有相同的优先级。
注::
wait(),sleep()和yield()方法的比较:
wait() ,yield()方法的比较:
wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。而yield()的作用是让步,它也会让当前线程离开“运行状态”。它们的区别是:
(01) wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,而不yield()是让线程由“运行状态”进入到“就绪状态”。
(02) wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁。
sleep方法和yield()方法。
(1)sleep方法 暂停线程后,线程会进入阻塞状态(即使是一瞬间),那么在这一刻cpu只会选择已经做好就绪状态的线程,故不会选择当前正在睡眠的线程。(即使没有其他可用线程)。而yield()方法会使当前线程即刻起进入就绪状态,cpu选择的可选线程范围中,包含当前执行yield()方法的线程。如若没有其他线程的优先级高于(或者等于) yield()的线程,则cpu仍会选择原有yield()的线程重新启动。
(2)sleep方法会抛出 InterruptedException 异常,所以调用sleep方法需要声明或捕捉该异常(比C#处理异常而言是够麻烦的),而yield没有声明抛出异常。
(3)sleep方法的移植性较好,可以对应很多平台的底层方法,所以用sleep()的地方要多余yield()的地方;
(4)sleep 暂停线程后,线程会睡眠 一定时间,然后才会变为就绪状态,倘若定义为sleep(0)后,则阻塞状态的时间为0,即刻进入就绪状态,这种用法与yield()的用法基本上是相同的:即都是让cpu进行一次新的选择,避免由于当前线程过度的霸占cpu,造成程序假死。
3.线程的终止
在Java多线程体系中,interrupt()的作用是中断本线程;
interrupt()方法终止线程有两种类型,终止处于阻塞状态得线程或者终止处于运行状态的线程,
interrupt():不要以为它是中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!
Java多线程系列--“基础篇”09之 interrupt()和线程终止方式
package VideoTest;
/*
* 程序的目的是利用线程逐次输出ABC三个字符,用到了线程常用方法中的wait()h和notify()方法。
*/
public class TestDemo implements Runnable {
private String name;
private Object prev;
private Object self;
private TestDemo(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
while (count > 0) {
synchronized (prev) {
synchronized (self) {
System.out.println(name);
count--;
self.notify();
}
try {
prev.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws Exception {
Object a = new Object();
Object b = new Object();
Object c = new Object();
TestDemo pa = new TestDemo("A", c, a);
TestDemo pb = new TestDemo("B", a, b);
TestDemo pc = new TestDemo("C", b, c);
new Thread(pa).start();
Thread.sleep(100); //确保按顺序A、B、C执行
new Thread(pb).start();
Thread.sleep(100);
new Thread(pc).start();
Thread.sleep(100);
}
}
运行结果如下:
(二)线程优先级和守护线程
-
线程优先级
当我们定义了多个线程的时候,有的线程要优先于其他线程,故而需要对线程设计线程优先级,优先级高的线程要比优先级低的线程先执行。在Java中,我们通常用setPriority()来定义一个线程的优先级,其范围值为1~10;其中,MIN_PTIORITY的优先级为1 MAX_PRIORITY的优先级为10;NORM_PRIORITY的优先级为5.
2.守护线程
通过t.setDaemon(true);将线程转换为守护线程。
java 中有两种线程:用户线程和守护线程。可以通过isDaemon()方法来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。
用户线程一般用户执行用户级任务,而守护线程也就是“后台线程”,一般用来执行后台任务。需要注意的是:Java虚拟机在“用户线程”都结束后会后退出。
借鉴::