线程同步
同步和互斥
线程的同步分为互斥和同步
- 互斥:同时运行的几个线程需要共享一些数据,而这些共享的数据在一个时刻只允许一个线程对其进行操作,否则就可能出错。如对某个数据的读和写操作,如果不严格控制,可能发生读写错误。
- 同步:有时线程之间需要相互配合,一个线程的运行以另一个线程的结果为前提。如生产者——消费者问题。
生产——售票问题
这里结合上一篇文章的售票过程,模拟类似生产者——消费者问题的过程。
【前提】假定两个线程模拟存票、售票的过程;开始售票时售票处没有票,一个线程往里面存票,另一个线程售出票;显然存票的数量不能超过存票的最大值,有存票时才能售票;本例设置一个票类对象,让存票和售票线程都去访问它,两个线程同时共享对同一份数据的操作。
我们假定每日生产的票的数量有限,且将售票进程先于存票进程启动,代码如下
class Tickets{
int number = 0; // 票的编号
int size; // 总票数
boolean available = false; // 是否可售
public Tickets(int size) {this.size = size;}
}
class Producer extends Thread{
Tickets t = null;
public Producer(Tickets t) {this.t = t;}
@Override
public void run() {
super.run();
while(t.number<t.size){
System.out.println("Producer puts ticket "+(++t.number));
t.available = true;
}
}
}
class Consumer extends Thread{
Tickets t = null;
int i=0; // 当前要售出的编号
public Consumer(Tickets t){this.t = t;}
@Override
public void run() {
super.run();
while(i<t.size){
if(t.available && i<t.number)
System.out.println("Consumer buys ticket "+(++i));
if(i==t.number) // 票卖完了
t.available = false;
}
}
}
class Test{
public static void testSellTickets(){
Tickets t = new Tickets(10); // 最大存票数为10
new Consumer(t).start();
new Producer(t).start();
}
}
public class Exp1 {
public static void main(String[] args) {
Test.testSellTickets();
}
}
结果如下:
Producer puts ticket 1
Producer puts ticket 2
Producer puts ticket 3
Producer puts ticket 4
Producer puts ticket 5
Producer puts ticket 6
Consumer buys ticket 1
Consumer buys ticket 2
Consumer buys ticket 3
Consumer buys ticket 4
Consumer buys ticket 5
Consumer buys ticket 6
Consumer buys ticket 7
Producer puts ticket 7
Consumer buys ticket 8
Producer puts ticket 8
Producer puts ticket 9
Consumer buys ticket 9
Consumer buys ticket 10
Producer puts ticket 10
可以看到生产和售出的顺序有混乱,而且每次运行的结果会不同。
而如果我们调换生产和售票进程的启动顺序,如下:
public static void testSellTicketsChangeOrder(){
Tickets t = new Tickets(10); // 最大存票数为10
new Producer(t).start();
new Consumer(t).start();
}
则几乎每次的运行结果都如下所示:
Producer puts ticket 1
Producer puts ticket 2
Producer puts ticket 3
Producer puts ticket 4
Producer puts ticket 5
Producer puts ticket 6
Producer puts ticket 7
Producer puts ticket 8
Producer puts ticket 9
Producer puts ticket 10
Consumer buys ticket 1
Consumer buys ticket 2
Consumer buys ticket 3
Consumer buys ticket 4
Consumer buys ticket 5
Consumer buys ticket 6
Consumer buys ticket 7
Consumer buys ticket 8
Consumer buys ticket 9
Consumer buys ticket 10
这是由 JVM 进程调度机制决定的,当两个进程的优先级相同的时候,进程调度器往往会先调度先启动的进程,所以 Producer 进程先执行,当 Producer 进程执行完了之后 Consumer 进程再执行,直到结束。
生产——售票问题(续)
如果我们在 Consumer 线程将 available 设置为 false 之前让程序休眠 1ms, 会发生什么呢?
Consumer 的代码变为如下所示:
class Consumer extends Thread{
Tickets t = null;
int i=0; // 当前要售出的编号
public Consumer(Tickets t){this.t = t;}
@Override
public void run() {
super.run();
while(i<t.size){
if(t.available && i<t.number)
System.out.println("Consumer buys ticket "+(++i));
if(i==t.number){ // 票卖完了
try{
sleep(1);
}catch (InterruptedException e){}
t.available = false;
}
}
}
}
如果我们按照先启动 Consumer 后启动 Producer 的顺序运行,则程序就会出错。因为先启动 Consumer,JVM 一般就会先调用 Consumer,此时 i
和 t.number
相同,都是 0,所以 Consumer 线程会休眠 1ms,JVM就会转去调用 Producer 进程。1ms 的时间内 Producer 可以执行完毕,然后返回 Consumer 继续执行,t.available
被设置为 false。而以后的循环中由于 t.available
被设为 false,第一个 if 不会执行,此时 Procuder 已经结束,t.available
再也不会被设置为 true,所以 i 永远得不到增长,所以循环就变成了死循环。
使用 synchronized 关键字实现线程加锁
使用 synchronized 关键字可以方便地实现锁机制,通过一定的包装,配合 wait() 和 yield() 函数又可以实现信号量机制和 P, V 操作。锁机制常常用于线程的互斥,而信号量机制可以用于线程互斥和同步。
synchronized 关键字
- 用于代码块或者方法中。被其修饰的代码块就相当于监视区(临界区)。
- 可以实现与另一个所的交互。synchornized 修饰代码块的格式如下
synchronized (obj) { code... }
- synchronized 的功能是:首先判断 obj 的锁是否存在,如果在就获得锁,然后可以执行 code,注意这里 synchronized 关键字包裹的代码段已经成为了原子操作,在代码块执行完之前不会释放锁;如果对象的锁不在,即已经被其它线程拿走,就进入等待状态,指导获得锁
- synchronized 代码块执行完就释放锁
synchronized 代码段
Java 中每个对象只有一个“锁”,利用“锁”机制可以实现线程间的互斥。
改进的代码如下:
class Tickets{
int number = 0; // 票的编号
int size; // 总票数
boolean available = false; // 是否可售
public Tickets(int size) {this.size = size;}
}
class Producer extends Thread{
Tickets t = null;
public Producer(Tickets t) {this.t = t;}
@Override
public void run() {
super.run();
while(t.number<t.size){
synchronized (t){
System.out.println("Producer puts ticket "+(++t.number));
t.available = true;
}
}
}
}
class Consumer extends Thread{
Tickets t = null;
int i=0;
public Consumer(Tickets t) {this.t = t;}
@Override
public void run() {
super.run();
while(i<t.size){
synchronized (t){
if(t.available && i<t.number)
System.out.println("Consumer buys ticket "+(++i));
if(i==t.number){
try {
sleep(1);
}catch (InterruptedException e) {}
t.available = false;
}
}
}
}
}
class Test{
public static void testSynchronized(){
Tickets t = new Tickets(10); // 最大存票数为10
new Consumer(t).start();
new Producer(t).start();
}
}
public class Exp2 {
public static void main(String[] args) {
Test.testSynchronized();
}
}
这样即使先启动 Consumer 线程,由于 sleep() 不会释放锁,所以不存在 Consumer sleep 的时候 Producer 能够运行 synchronized 代码块中的内容。所有程序不会出错。当 JVM 发现 Consumer 运行了很长一段时间了(由于 sleep ),就会转去调度 Producer 运行,所以最后会输出正确结果。
Producer puts ticket 1
Producer puts ticket 2
Producer puts ticket 3
Producer puts ticket 4
Producer puts ticket 5
Producer puts ticket 6
Producer puts ticket 7
Producer puts ticket 8
Producer puts ticket 9
Producer puts ticket 10
Consumer buys ticket 1
Consumer buys ticket 2
Consumer buys ticket 3
Consumer buys ticket 4
Consumer buys ticket 5
Consumer buys ticket 6
Consumer buys ticket 7
Consumer buys ticket 8
Consumer buys ticket 9
Consumer buys ticket 10
synchronized 关键字修饰方法
除了对指定的代码段进行同步控制之外,还可以定义整个方法在同步控制之下执行,只要在方法定义前面加上 synchronized 关键字即可。
下面采用 synchronized 修饰的方法修改上面的程序,代码如下:
class Tickets{
int number = 0; // 票的编号
int size; // 总票数
int i; // 售票序号
boolean available = false; // 是否可售
public Tickets(int size) {this.size = size;}
public synchronized void put(){ // 实现存票功能
System.out.println("Producer puts tickets "+(++number));
available = true;
}
public synchronized void sell(){ // 实现取票的功能
if(available && i<number)
System.out.println("Consumer buys ticket "+(++i));
if(i==number) available = false;
}
}
class Producer extends Thread{
Tickets t = null;
public Producer(Tickets t) {this.t=t;}
@Override
public void run() {
super.run();
while(t.number<t.size) t.put();
}
}
class Consumer extends Thread{
Tickets t = null;
public Consumer(Tickets t) {this.t = t;}
@Override
public void run() {
super.run();
while(t.i < t.size)
t.sell();
}
}
class Test{
public static void testSynchronizedMethod(){
Tickets t = new Tickets(10);
new Consumer(t).start();
new Producer(t).start();
}
}
public class Exp3 {
public static void main(String[] args) {
Test.testSynchronizedMethod();
}
}
结果如下:
Producer puts tickets 1
Producer puts tickets 2
Producer puts tickets 3
Producer puts tickets 4
Producer puts tickets 5
Producer puts tickets 6
Producer puts tickets 7
Producer puts tickets 8
Producer puts tickets 9
Producer puts tickets 10
Consumer buys ticket 1
Consumer buys ticket 2
Consumer buys ticket 3
Consumer buys ticket 4
Consumer buys ticket 5
Consumer buys ticket 6
Consumer buys ticket 7
Consumer buys ticket 8
Consumer buys ticket 9
Consumer buys ticket 10
总结
- 锁机制同步的时候只能同步方法,不能同步变量(代码块也可以看成一种方法)
- 每个对象只有一个锁
- 类可以同时拥有同步方法和非同步方法,非同步的代码不应当放在 synchronized 关键字影响的范围内以提高代码并发度
- 线程睡眠的时候其所持有的任何锁不会被释放
- 线程可以获得多个锁,比如在一个对象的同步方法里面调用另外一个对象的同步方法(嵌套的 synchronized ),则相当于获得了两个对象的同步锁。
- synchronized 修饰方法可以比修饰代码段方便地去将同步代码分离出来,结构比较清晰,而且不需要手动指定同步的对象(就是包含该方法的类的对象);修饰代码块比较轻量级,而且不需要在同步对象的类中写同步方法,适用于 Java 原生定义的类。
线程等待和唤醒
定义在 Object 类中的 wait(), notify(), notifyAll() 方法保证了在任何类里面都能用有效的手段实现线程间的主动阻塞和唤醒。
- wait():当前线程暂停进入对象x的线程等待池,并释放已经获得的对象x的锁,直到对象x上的其它线程嗲用 notify() 或者 notifyAll() 方法才能够重新获得对象x的锁并继续执行。
- notify():随机唤醒一个等待的线程,本线程继续执行。线程被唤醒之后,还要等待发出唤醒消息的线程释放监视器,在这期间,关键数据仍然有可能被发出唤醒消息的线程改变。
- notifyAll():唤醒所有等待的线程,本线程继续执行。
修改之前的售票程序,实现每存一张票,就售出一张票,售出后再存入
class Tickets{
int number = 0; // 票的编号
int size; // 总票数
boolean available = false; // 是否可售
public Tickets(int size) {this.size = size;}
public synchronized void put(){ // 实现存票功能
if(available){
try{
wait(); // 如果没售出则等待
}catch (Exception e){}
}
System.out.println("Producer puts tickets "+(++number));
available = true; // 可以出售了
notify(); // 唤醒售票程序
}
public synchronized void sell(){ // 实现取票的功能
if(!available){
try{
wait(); // 如果售出了,就等待
}catch (Exception e){}
}
System.out.println("Consumer buys ticket "+(number));
available = false; // 禁止出售,可以存票了
notify(); // 唤醒存票程序
// if(number==size) number++;
}
}
class Producer extends Thread{
Tickets t = null;
public Producer(Tickets t) {this.t=t;}
@Override
public void run() {
super.run();
while(t.number<t.size) t.put();
}
}
class Consumer extends Thread{
Tickets t = null;
public Consumer(Tickets t) {this.t = t;}
@Override
public void run() {
super.run();
while(t.number < t.size)
t.sell();
}
}
class Test{
public static void testWaitNotify(){
Tickets t = new Tickets(10);
new Consumer(t).start();
new Producer(t).start();
}
}
public class Exp5 {
public static void main(String[] args) {
Test.testWaitNotify();
}
}
运行结果如下:
Producer puts tickets 1
Consumer buys ticket 1
Producer puts tickets 2
Consumer buys ticket 2
Producer puts tickets 3
Consumer buys ticket 3
Producer puts tickets 4
Consumer buys ticket 4
Producer puts tickets 5
Consumer buys ticket 5
Producer puts tickets 6
Consumer buys ticket 6
Producer puts tickets 7
Consumer buys ticket 7
Producer puts tickets 8
Consumer buys ticket 8
Producer puts tickets 9
Consumer buys ticket 9
Producer puts tickets 10
Consumer buys ticket 10
可以发现,wait() 和 notify() 配合特别适合用来实现进程间的同步,随后可以看到使用 synchronized 配合 wait(), notify() 实现信号量机制。
使用 synchronized 实现信号量
【注】 这里引用掘金上一位名叫CISay的作者的文章中的方法,在此鸣谢。
【举例】假设我们有一个银行 Bank,银行中预存了 10000 元钱,有两个人分别需要向银行存 10000 元钱,钱比较多,因此不能立刻存进去,我们可以用两个线程完成这个操作,我们的预期目标应该是最终银行里会有 30000 元钱。
使用线程同步的方法首先实现能实现线程同步的信号量的类,我们命名为 Semaphore ,代码如下
class Semaphore {
private int S = 1;
public Semaphore(int S) {
this.S = S;
}
public synchronized void P(){
S--;
if (S < 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void V(){
S++;
if (S <= 0){
notify();
}
}
}
里面定义了两个操作,分别是 P 操作和 V 操作。程序如下:
import java.util.Vector;
class Bank {
private int money = 0;
public Bank(int money){
this.money = money;
}
public void addMoney(){ // 存钱,一次只能存 1
this.money++;
}
public int readMoney(){
return this.money; // 当前存款
}
}
class AddMoneyThread implements Runnable {
private final Semaphore sp;
private final Bank bk;
private final int addMoneyCount;
public int id;
public AddMoneyThread(Bank bk, int addMoneyCount, int id, Semaphore sp) {
this.bk = bk;
this.addMoneyCount = addMoneyCount;
this.id = id;
this.sp = sp;
}
@Override
public void run() {
if (bk != null) {
int index = 1;
while (index <= this.addMoneyCount) {
sp.P(); // 存钱是原子操作
bk.addMoney();
index++;
sp.V();
}
}
}
}
class Test{
public static void testBank(){
for (int i = 0; i < 5; i++) {
Semaphore s = new Semaphore(10);
Bank bank = new Bank(10000);
Vector<Thread> v = new Vector<>();
v.add(new Thread(new AddMoneyThread(bank, 10000, 1, s)));
v.add(new Thread(new AddMoneyThread(bank, 10000, 2, s)));
for(Thread t : v){
t.start();
try{
t.join();
}catch (InterruptedException e){e.printStackTrace();}
}
System.out.println("第" + (i + 1) + "次" + bank.readMoney());
}
}
}
public class Exp4 {
public static void main(String[] args) {
Test.testBank();
}
}
结果如下:
第1次30000
第2次30000
第3次30000
第4次30000
第5次30000
测试了 5 次后,都能正确实现线程同步。
再说一个不一样的例子。现在我们有一个仓库 Repository,Repository 的大小为 10, 有 A 和 B 两个人,A 负责向 Repsitory 里存入 10000 份货物 Goods,B 负责从 Repository 里拿出所有的 Goods。我们可以用两个线程分别完成 A 和 B 的工作,我们的预期目标是最终 B 应该可以拿出来 10000 份货物。
这里不仅有同步,还有互斥。我们假设仓库的容量为10,原来仓库就有2个货物,所以 idle 设为8,used 设为2。最后可以从仓库中取出10000份货物,而且还有两份存在仓库中
class Goods{
int id;
public Goods(int id) {this.id = id;}
}
class SyncSemaphore {
private int idle = 0; // 空
private int used = 0; // 满
private int mutex = 1;
public SyncSemaphore(int idle, int used){
this.idle = idle;
this.used = used;
}
public synchronized void PEmpty(){
idle--;
if (idle < 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void VEmpty(){
idle++;
if (idle <= 0){
notify();
}
}
public synchronized void PFull(){
used--;
if (used < 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void VFull(){
used++;
if (used <= 0){
notify();
}
}
public synchronized void PMutex(){
mutex--;
if (mutex < 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void VMutex(){
mutex++;
if (mutex <= 0){
notify();
}
}
}
class Repository {
private Goods[] goodsArray;
private int usedSize = 0;
public Repository(int goodSize){
goodsArray = new Goods[goodSize];
}
public void saveGood(int goodId) { // 存货
if (usedSize < goodsArray.length) {
goodsArray[usedSize++] = new Goods(goodId);
}
}
public Goods takeGood(){ // 取货
if (usedSize == 0){
return null;
}
return goodsArray[--usedSize];
}
}
class SaveGoodThread implements Runnable{
private Repository repository;
private int saveNumber = 0;
SyncSemaphore s;
public SaveGoodThread(Repository repository, int saveNumber, SyncSemaphore s){
this.repository = repository;
this.saveNumber = saveNumber;
this.s = s;
}
/* saveGoodThread */
@Override
public void run(){
if (repository != null){
int i = 0;
while (i < this.saveNumber){
s.PEmpty();
s.PMutex();
repository.saveGood(i + 1);
i++;
s.VMutex();
s.VFull();
}
}
}
}
class TakeGoodThread implements Runnable {
private int number = 0;
private List<Goods> goodsList = null;
private Repository repository;
private int takeNumber;
SyncSemaphore s;
public TakeGoodThread(Repository repository, int takeNumber, List<Goods> goodsList, SyncSemaphore s) {
this.repository = repository;
this.goodsList = goodsList;
this.s = s;
this.takeNumber = takeNumber;
}
@Override
public void run() {
while (number<takeNumber) {
s.PFull();
s.PMutex();
Goods temp = repository.takeGood();
if (temp != null){
number++;
goodsList.add(temp);
}
s.VEmpty();
s.VMutex();
}
}
}
public class Exp9 {
public static void main(String[] args) {
for (int i = 0; i < 5; i++){
Repository repository = new Repository(10);
List<Goods> goodsList = new ArrayList<>();
SyncSemaphore s = new SyncSemaphore(8, 2);
Thread saveGood = new Thread(new SaveGoodThread(repository, 10000, s));
Thread takeGood = new Thread(new TakeGoodThread(repository, 10000, goodsList, s));
saveGood.start();
takeGood.start();
try {
saveGood.join();
takeGood.join();
System.out.println("第" + (i + 1) + "次" + goodsList.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
最后结果如下:
第1次10000
第2次10000
第3次10000
第4次10000
第5次10000
后台线程
- 后台线程也叫守护线程,通常是为了辅助其他线程而运行的线程。
- 当进程中只要海油一个前台线程在运行,这个进程就不会结束;如果一个进程中的所有前台线程都已经结束,那么无论是否还有美结束的后台线程,还有多少个后台线程,这个进程都会结束。
- JVM 的垃圾回收机制就是一个后台线程
- 线程启动的时候默认为前台线程,将一个线程设置为后台线程需要在调用 start() 方法之前调用 setDeamon(true) 方法,显式地将这个线程变成后台线程。
看一个小🌰 :
class ThreadTest extends Thread {
@Override
public void run() {
super.run();
while(true){}
}
}
public class Exp6 {
public static void main(String[] args) {
ThreadTest t = new ThreadTest();
t.setDaemon(true);
t.start();
System.out.println("Main ends");
}
}
运行结果是直接打印 “Main ends” 整个进程就结束了。ThreadTest 对象 t 设置为后台线程,而其 run() 是一个死循环。这表明后台线程不会决定进程是否结束。
线程的生命周期和死锁
线程的生命周期是指线程从产生到消亡的全过程,线程在任何时刻都处于某种线程状态。
- 诞生状态:线程刚刚被创建
- 就绪状态(Ready):线程的 start() 方法已经被执行并且线程已经准备好运行
- 运行状态(Running):线程被调度器调用,正在使用 CPU 资源。
- 阻塞状态(Blocked):当线程在等待信号的时候(如输入输出),遇到 synchronized 而未获得锁的时候,被 wait() 阻塞时
- 休眠状态(Sleeping):执行 sleep 方法进入休眠
- 死亡状态(Dead):运行结束,等待资源的回收
死锁不在于发生后的处理,而在于预防
下面模拟一个哲学家就餐问题的死锁情况
class Fork{
int size;
boolean[] flag;
public Fork(int size){
this.size = size;
flag = new boolean[size];
}
}
class Philosopher extends Thread{
private Fork fk;
int id;
public Philosopher(Fork f, int id) {
this.fk = f;
this.id = id;
}
@Override
public void run() {
super.run();
while(true){
while(fk.flag[id]){}
fk.flag[id] = true;
while(fk.flag[(id+1)%fk.size]) {}
if(fk.flag[id] && !fk.flag[(id+1)%fk.size]){
fk.flag[(id+1)%fk.size] = true;
System.out.println("Philosopher "+id+" has two forks");
fk.flag[id] = false; fk.flag[(id+1)%fk.size] = false;
try{
sleep(1);
}catch (InterruptedException e){e.printStackTrace();}
}
}
}
}
public class Exp7 {
public static void main(String[] args) {
int size = 5;
Fork f = new Fork(size);
for(int i =0; i<size; i++){
new Philosopher(f, i).start();
}
}
}
每次哲学家都先拿起左边那个叉子,然后拿起右边的叉子。并且当拿了一个叉子之后会一直等待直到下一个叉子空闲。当程序运行运行一段时间之后,程序陷入死锁。(这里在 Philosopher 类的 run() 方法里面有 sleep 的原因是使得程序更容易死锁。)
线程结束的方法
- 通常可以通过控制 run() 方法中的循环条件来结束一个线程
- 通过 stop() 方法可以结束线程的生命。当线程在对共享数据段写的时候吗,如果贸然使用 stop() 方法结束,则会产生数据的不完整。不提倡使用这种方法
线程的优先级
- JVM 的线程调度算法是一种十分简单的,基于优先级的线程调度算法。一般会有限调度高优先级的线程,再调度低优先级的线程。同一级线程功能之间的调度顺序是随机的。
- 每个 Java 线程都有一个优先级,默认值为5,范围在 1 到 10 之间。
- 在某一线程中创建其它线程的时候,被创建线程有着和创建的线程相同的优先级。
- 如果创建线程是后台线程,则被创建的线程也是后天线程;通用如果创建线程是前台线程,则被创建的相乘默认也是前台线程。
- 在线程创建之后的任何时候,都可以通过 setPriority(int priority) 方法改变原来的优先级。
- 底层操作系统支持的线程优先级可能少于 10 个,这样可能使得基于优先级的调度算法混乱,所以只能将优先级当做一种很粗陋的工具只用,可以使用 yield() 方法暂停自己,将 CPU 让给同优先级的线程或更高优先级的线程执行。如果只存在低优先级的线程,则还是自己继续执行。
- 我们只能从基于代码实现效率的角度使用线程的优先级,而不能依赖线程的优先级作为调度顺序的倚靠,也不能倚靠优先级保证算法的正确性。
线程碰到以下的情况可能会出现暂停运行
- 有一个高优先级的线程就绪
- 前面提到过:等待信号,sleep,wait,yield
- 对于支持时间片的系统,当时间片的时间用完
举例如下:
class TestThread extends Thread{
private int tick = 1;
private int num;
public TestThread(int i){this.num = i;}
@Override
public void run() {
super.run();
while(tick<40000000){
tick++;
if((tick%10000000)==0){
System.out.println("Thread #"+num+", tick = "+tick);
yield(); // 放弃执行权
}
}
}
}
public class Exp8 {
public static void main(String[] args) {
TestThread[] runners = new TestThread[2];
for(int i=0; i<2; i++){
runners[i] = new TestThread(i);
}
runners[0].setPriority(2);
runners[1].setPriority(3);
for(int i=0; i<2; i++) runners[i].start();
}
}
理论上高优先级的 runner[1] 即使后启动,而且使用了 yield 等待,但是应该会立即从 runner[0] 手上抢夺回来,从而 runner[1] 先执行完,但实际上几乎和没有设置优先级没有区别。所以不能把优先级当做线程执行顺序的筹码。
在线程 sleep 期间,低优先级的线程也有机会获得 CPU,Java虚拟机本身不支持某个线程抢夺另一个具有同优先级的线程的执行权,我们可以在线程中插入 yield() 使得当前线程放弃 CPU 的使用权,从而达到目的。