第1关:线程的状态与调度
任务描述
本关任务:学习本关知识完成选择题。
相关知识
为了完成本关你需要掌握:
1.线程的状态与调度;
2.线程执行的优先级。
线程的状态与调度
如果看懂下图,你对线程的了解就会更上一层楼。
- 当我们使用
new
关键字新建一个线程,这个时候线程就进入了新建状态(New
),也就是图中未启动状态; - 调用
start
方法启动线程,这个时候就进入了可运行状态,也就是就绪状态(Runnable
); - 就绪状态获取了CPU资源,开始执行
run
方法,就进入了运行状态(Running
); - 阻塞状态(
Blocked
):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种;
- 等待阻塞:运行的线程执行
wait()
方法,JVM会把该线程放入等待池中。(wait
会释放持有的锁); - 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中;
- 其他阻塞:运行的线程执行
sleep()
或join()
方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()
状态超时、join()
等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep
是不会释放持有的锁);
- 死亡状态(
Dead
):线程执行完了或者因异常退出了run()
方法,该线程结束生命周期。
线程优先级
我们知道,多个线程的执行是随机的,在一个时段执行哪个线程是看哪一个线程在此时拥有CPU
的执行权限。
在Java
中线程有优先级,优先级高的线程会获得较多的运行机会。
Java
线程的优先级用整数表示,取值范围是1~10
,Thread
类有以下三个静态常量:
staticintMAX_PRIORITY线程可以具有的最高优先级,取值为10。staticintMIN_PRIORITY线程可以具有的最低优先级,取值为1。staticintNORM_PRIORITY分配给线程的默认优先级,取值为5。
如果要设置和获取线程的优先级,可以使用Thread
类的setPriority()
和getPriority()
方法。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY
。
线程的优先级有继承关系,比如A
线程中创建了B
线程,那么B
将和A
具有相同的优先级。
JVM提供了10
个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread
类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
关于线程调度与优先级你还需要了解:
线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。 线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。 线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。 线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。 线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
编程要求
略 ####测试说明 略
题
- 1、有三种原因可以导致线程不能运行,它们是(
ABC
)
A、等待
B、阻塞
C、休眠
D、挂起及由于I/O操作而阻塞 - 2、Java语言中提供了一个(
D
)线程,自动回收动态分配的内存。
A、异步
B、消费者
C、守护
D、垃圾收集 - 3、当(
A
)方法终止时,能使线程进入死亡状态
A、run
B、setPriority
C、yield
D、sleep - 4、用(
B
)方法可以改变线程的优先级。
A、run
B、setPriority
C、yield
D、sleep - 5、线程通过(
D
)方法可以休眠一段时间,然后恢复运行
A、run
B、setPriority
C、yield
D、sleep - 6、下列关于线程的说法正确的是(
ABD
)
A、如果线程死亡,它便不能运行
B、在Java中,高优先级的可运行线程会抢占低优先级线程
C、线程可以用yield方法使低优先级的线程运行
D、一个线程可以调用yield方法使其他线程有机会运行
第2关:常用函数(一)
任务描述
本关任务:获取子线程执行的结果并输出。
相关知识2
本关你需要掌握sleep
与join
函数的用法。
sleep()函数
sleep(long millis)
: 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。
使用方式很简单在线程的内部使用Thread.sleep(millis)
即可。
sleep()
使当前线程进入停滞状态(阻塞当前线程),让出CPU的使用,目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
在sleep()
休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
join()函数
join
函数的定义是指:等待线程终止。
我们在运行线程的时候可能会遇到,在主线程中运行子线程,主线程需要获取子线程最终执行结果的情况。
但是有很多时候子线程进行了很多耗时的操作,主线程往往先于子线程结束,这个时候主线程就获取不到子线程的最终执行结果了。
使用join
函数,可以解决这一问题。
我们通过两个示例来理解:
不使用join
函数的版本:
classMyThreadextendsThread { privateStringname; publicMyThread(Stringname) { this.name=name; } publicvoidrun() { System.out.println("子线程开始运行"); for (inti=0; i<5; i++) { System.out.println("子线程"+name+"运行"+i); try { Thread.sleep(30); } catch (InterruptedExceptione) { e.printStackT\frace(); } } System.out.println("子线程结束"); } } publicclassTest { publicstaticvoidmain(String[] args) { Threadt=newMyThread("子线程A"); t.start(); for (inti=0; i<5; i++) { System.out.println("主线程执行"+i); } System.out.println("主线程结束"); } }
输出结果(每次都不一样):
主线程执行0
主线程执行1
主线程执行2
主线程执行3
主线程执行4
主线程结束
子线程开始运行
子线程子线程A运行0
子线程子线程A运行1
子线程子线程A运行2
子线程子线程A运行3
子线程子线程A运行4
子线程结束
可以发现每次运行,主线程都是先于子线程结束的。
使用join
函数:
packagestep1; classMyThreadextendsThread { privateStringname; publicMyThread(Stringname) { this.name=name; } publicvoidrun() { System.out.println("子线程开始运行"); for (inti=0; i<5; i++) { System.out.println("子线程"+name+"运行"+i); try { Thread.sleep(30); } catch (InterruptedExceptione) { e.printStackT\frace(); } } System.out.println("子线程结束"); } } publicclassTest { publicstaticvoidmain(String[] args) { Threadt=newMyThread("子线程A"); t.start(); for (inti=0; i<5; i++) { System.out.println("主线程执行"+i); } try { t.join(); } catch (Exceptione) { e.printStackT\frace(); } System.out.println("主线程结束"); } }
输出结果:
主线程执行1
主线程执行2
主线程执行3
子线程开始运行
主线程执行4
子线程子线程A运行0
子线程子线程A运行1
子线程子线程A运行2
子线程子线程A运行3
子线程子线程A运行4
子线程结束
主线程结束
可以发现无论运行多少次,主线程都会等待子线程结束之后在结束。
编程要求
请仔细阅读右侧代码,根据方法内的提示,在Begin - End
区域内进行代码补充,具体任务如下:
- 创建自定义线程,实现求第
num
项斐波那契数列的值num
从0
开始,并且在main
函数中获取子线程最终计算的结果。
测试说明
补充完代码后,点击测评,平台会对你编写的代码进行测试,当你的结果与预期输出一致时,即为通过。
输入:5
输出:子线程计算结果为:5
输入:8
输出:子线程计算结果为:21
输入:10
输出:子线程计算结果为:55
开始你的任务吧,祝你成功!
代码示例
packagestep2; importjava.util.Scanner; publicclassTask { publicstaticvoidmain(String[] args) { Scannersc=newScanner(System.in); intnum=sc.nextInt(); // 请在此添加实现代码/********** Begin **********/MyThreadmt=newMyThread(num); mt.start(); try { mt.join(); } catch (InterruptedExceptione) { // TODO 自动生成的 catch 块e.printStackTrace(); } /********** End **********/ } } //请在此添加实现代码/********** Begin **********/classMyThreadextendsThread { intnum; publicMyThread(intnum) { this.num=num; } publicvoidrun() { // TODO 自动生成的方法存根System.out.println("子线程计算结果为:"+num(num)); } publicintnum(intn) { if (n<3) { return1; } elsereturnnum(n-1) +num(n-2); } } /********** End **********/
第3关:常用函数(二)
任务描述
本关任务:使用三个线程交替打印5
次EDU
。
相关知识
为了完成本关任务你需要掌握:
1.yield
函数的用法;
2.wait
函数的用法;
3.其他常用函数的使用。
yield 函数
yield
函数可以理解为“让步”,它的作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()
应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的
目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()
达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
示例:
publicclassTask { publicstaticvoidmain(String[] args) { System.out.println(Thread.currentThread().getName()+"主线程运行开始!"); MyThreadmTh1=newMyThread("A"); MyThreadmTh2=newMyThread("B"); mTh1.start(); mTh2.start(); System.out.println(Thread.currentThread().getName()+"主线程运行结束!"); } } classMyThreadextendsThread { privateStringname; publicMyThread(Stringname) { this.name=name; } publicvoidrun() { for (inti=0; i<50; i++) { System.out.println(name+"线程"+"执行"+i); if(i==10){ this.yield(); } } } }
运行结果有两种情况:
第一种情况:A
(线程)当执行到10
时会让掉CPU
时间,这时B
(线程)抢到CPU时间并执行。
第二种情况:B
(线程)当执行到10
时会让掉CPU
时间,这时A
(线程)抢到CPU
时间并执行。
我们还可以考虑在其中加入setPriority
函数改变线程优先级从而改变线程的执行顺序。
wait 函数
要弄明白wait
函数我们首先需要了解线程锁的概念。 线程锁:其实就像我们日常生活中的锁,如果一个房子上了锁,别人就进不去,在Java中也类似,如果一段代码取得了锁,那么其他地方就不能运行这段代码,只能等待锁的释放。
这里我们会涉及到两个函数,1.wait()
,2.notify()
。这两个函数都是Object
类自带的函数。
在下面的例子中我们会使用synchronized(Obj)
来实现线程同步,同步的概念后面会讲到,这里不理解没关系,不影响对于wait
函数的理解。
从功能上来说:
wait()
就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()
唤醒该线程,才能继续获取对象锁,并继续执行;- 相应的
notify()
就是对对象锁的唤醒操作。
但有一点需要注意的是notify()
调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}
语句块执行结束,自动释放锁后,JVM会在wait()
对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。
Thread.sleep()
与Object.wait()
二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()
在释放CPU同时,释放了对象锁的控制。
纸上得来终觉浅,欲知此事须躬行,我们还是通过实例来学习这些函数的用法。
问题:建立两个线程,A线程打印5
次A
,B线程打印5
次B
,要求线程同时运行,交替打印5
次AB
。
这个问题用Object
的wait()
,notify()
就可以很方便的解决。代码如下:
publicclassMyThreadimplementsRunnable { privateStringname; privateObjectprev; privateObjectself; privateMyThread(Stringname, Objectprev, Objectself) { this.name=name; this.prev=prev; this.self=self; } publicvoidrun() { intcount=5; while (count>0) { synchronized (prev) { synchronized (self) { System.out.print(name); count--; self.notify(); } try { prev.wait(); } catch (InterruptedExceptione) { e.printStackTrace(); } } } System.exit(0);//退出jvm } publicstaticvoidmain(String[] args) throwsException { Objecta=newObject(); Objectb=newObject(); MyThreadta=newMyThread("A", b, a); MyThreadtb=newMyThread("B", a, b); newThread(ta).start(); Thread.sleep(100); //确保按顺序A、B执行newThread(tb).start(); Thread.sleep(100); } }
运行程序,结果为:
ABABABABAB
线程常用函数总结
下表列出的是线程类常用的函数:
函数 | 描述 |
public void start() | 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
public void run() | 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
public final void setName(String name) | 改变线程名称,使之与参数 name 相同。 |
public final void setPriority(int priority) | 更改线程的优先级。 |
public final void setDaemon(boolean on) | 将该线程标记为守护线程或用户线程。 |
public final void join(long millisec) | 等待该线程终止的时间最长为 millis 毫秒 |
public void interrupt() | 中断线程。 |
public final boolean isAlive() | 测试线程是否处于活动状态。 |
public static void yield() | 暂停当前正在执行的线程对象,并执行其他线程。 |
public static void sleep(long millisec) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
public static boolean holdsLock(Object x) | 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
public static Thread currentThread() | 返回对当前正在执行的线程对象的引用。 |
public static void dumpStack() | 将当前线程的堆栈跟踪打印至标准错误流。 |
编程要求
请仔细阅读右侧代码,根据方法内的提示,在Begin - End
区域内进行代码补充,具体任务如下:
- 建立三个线程,
A
线程打印5
次E
,B线程打印5
次D
,C
线程打印5
次U
,要求线程同时运行,交替打印5
次EDU
。
测试说明
补充完代码后,点击测评,平台会对你编写的代码进行测试,当你的结果与预期输出一致时,即为通过。
开始你的任务吧,祝你成功!
代码示例
packagestep3; publicclassMyThreadimplementsRunnable { // 请在此添加实现代码/********** Begin **********/privateStringname; privateObjectprev; privateObjectself; privateMyThread(Stringname, Objectprev, Objectself) { this.name=name; this.prev=prev; this.self=self; } publicvoidrun() { intcount=5; while (count>0) { synchronized (prev) { synchronized (self) { System.out.print(name); count--; self.notify(); } try { prev.wait(); } catch (InterruptedExceptione) { e.printStackTrace(); } } } System.exit(0);// 退出jvm } publicstaticvoidmain(String[] args) throwsException { Objecta=newObject(); Objectb=newObject(); Objectc=newObject(); MyThreadta=newMyThread("E", c, b); MyThreadtb=newMyThread("D", b, a); MyThreadtc=newMyThread("U", a, c); newThread(ta).start(); Thread.sleep(100); // 确保按顺序A、B执行newThread(tb).start(); Thread.sleep(100); newThread(tc).start(); } /********** End **********/}
输出:
EDUEDUEDUEDUEDU