使用互斥解决多线程问题是一种简单有效的解决办法,但是由于该方法比较简单,所以只能解决一些基本的问题,对于复杂的问题就无法解决了。
解 决多线程问题的另外一种思路是同步。同步是另外一种解决问题的思路,结合前面卫生间的示例,互斥方式解决多线程的原理是,当一个人进入到卫生间内部时,别 的人只能在外部时刻等待,这样就相当于别的人虽然没有事情做,但是还是要占用别的人的时间,浪费系统的执行资源。而同步解决问题的原理是,如果一个人进入 到卫生间内部时,则别的人可以去睡觉,不占用系统资源,而当这个人从卫生间出来以后,把这个睡觉的人叫醒,则它就可以使用临界资源了。所以使用同步的思路 解决多线程问题更加有效,更加节约系统的资源。
在常见的多线程问题解决中,同步问题的典型示例是“生产者-消费者”模型,也就是生产者线程只负责生产,消费者线程只负责消费,在消费者发现无内容可消费时则睡觉。下面举一个比较实际的例子——生活费问题。
生 活费问题是这样的:学生每月都需要生活费,家长一次预存一段时间的生活费,家长和学生使用统一的一个帐号,在学生每次取帐号中一部分钱,直到帐号中没钱时 通知家长存钱,而家长看到帐户还有钱则不存钱,直到帐户没钱时才存钱。在这个例子中,这个帐号被学生和家长两个线程同时访问,则帐号就是临界资源,两个线 程是同时执行的,当每个线程发现不符合要求时则等待,并释放分配给自己的CPU执行时间,也就是不占用系统资源。实现该示例的代码为:
package syn4; /** * 测试类 */ public class TestAccount { public static void main(String[] args) { Accout a = new Accout(); StudentThread s = new StudentThread(a); GenearchThread g = new GenearchThread(a); } }
package syn4; /** * 模拟学生线程 */ public class StudentThread extends Thread { Accout a; public StudentThread(Accout a){ this.a = a; start(); } public void run(){ try{ while(true){ Thread.sleep(2000); a.getMoney(); //取钱 } }catch(Exception e){} } }
package syn4; /** * 家长线程 */ public class GenearchThread extends Thread { Accout a; public GenearchThread(Accout a){ this.a = a; start(); } public void run(){ try{ while(true){ Thread.sleep(12000); a.saveMoney(); //存钱 } }catch(Exception e){} } }
package syn4; /** * 银行账户 */ public class Accout { int money = 0; /** * 取钱 * 如果账户没钱则等待,否则取出所有钱提醒存钱 */ public synchronized void getMoney(){ System.out.println("准备取钱!"); try{ if(money == 0){ wait(); //等待 } //取所有钱 System.out.println("剩余:" + money); money -= 50; //提醒存钱 notify(); }catch(Exception e){} } /** * 存钱 * 如果有钱则等待,否则存入200提醒取钱 */ public synchronized void saveMoney(){ System.out.println("准备存钱!"); try{ if(money != 0){ wait(); //等待 } //取所有钱 money = 200; System.out.println("存入:" + money); //提醒存钱 notify(); }catch(Exception e){} } }
该程序的一部分执行结果为:
准备取钱! 准备存钱! 存入:200 剩余:200 准备取钱! 剩余:150 准备取钱! 剩余:100 准备取钱! 剩余:50 准备取钱! 准备存钱! 存入:200 剩余:200 准备取钱! 剩余:150 准备取钱! 剩余:100 准备取钱! 剩余:50 准备取钱!
在该示例代码中,TestAccount类是测试类,主要实现创建帐户Account类的对象,以及启动学生线程StudentThread和启动家长线程GenearchThread。在StudentThread线程中,执行的功能是每隔2秒中取一次钱,每次取50元。在GenearchThread线程中,执行的功能是每隔12秒存一次钱,每次存200。这样存款和取款之间不仅时间间隔存在差异,而且数量上也会出现交叉。而该示例中,最核心的代码是Account类的实现。
在Account类中,实现了同步控制功能,在该类中包含一个关键的属性money,该属性的作用是存储帐户金额。在介绍该类的实现前,首先介绍一下两个同步方法——wait和notify方法的使用,这两个方法都是Object类中的方法,也就是说每个类都包含这两个方法,换句话说,就是Java天生就支持同步处理。这两个方法都只能在synchronized修饰的方法或语句块内部采用被调用。其中wait方法的作用是使调用该方法的线程休眠,也就是使该线程退出CPU的等待队列,处于冬眠状态,不执行动作,也不占用CPU排队的时间,notify方法的作用是唤醒一个任意该对象的线程,该线程当前处于休眠状态,至于唤醒的具体是那个则不保证。在Account类中,被StudentThread调用的getMoney方法的功能是判断当前金额是否是0,如果是则使StudentThread线程处于休眠状态,如果金额不是0,则取出50元,同时唤醒使用该帐户对象的其它一个线程,而被GenearchThread线程调用的saveMoney方法的功能是判断当前是否不为0,如果是则使GenearchThread线程处于休眠状态,如果金额是0,则存入200元,同时唤醒使用该帐户对象的其它一个线程。
如果还是不清楚,那就结合前面的程序执行结果来解释一下程序执行的过程:在程序开始执行时,学生线程和家长线程都启动起来,所以输出“准备取钱”和“准备存钱”,然后学生线程按照该线程run方法的逻辑执行,先延迟2秒,然后调用帐户对象a中的getMoney方法,但是由于初始情况下帐户对象a中的money数值为0,所以学生线程就休眠了。在学生线程执行的同时,家长线程也按照该线程的run方法的逻辑执行,先延迟12秒,然后调用帐户对象a中的saveMoney方法,由于帐户a对象中的money为零,条件不成立,所以执行存入200元,同时唤醒线程,由于使用对象a的线程现在只有学生线程,所以学生线程被唤醒,开始执行逻辑,取出50元,然后唤醒线程,由于当前没有线程处于休眠状态,所以没有线程被唤醒。同时家长线程继续执行,先延迟12秒,这个时候学生线程执行了4次,耗时4X2秒=8秒,就取光了帐户中的钱,接着由于帐户为0则学生线程又休眠了,一直到家长线程延迟12秒结束以后,判断帐户为0,又存入了200元,程序继续执行下去。
在解决多线程问题是,互斥和同步都是解决问题的思路,如果需要形象的比较这两种方式的区别的话,就看一下下面的示例。一个比较忙的老总,桌子上有2部电话,在一部处于通话状态时,另一部响了,老总拿其这部电话说我在接电话,你等一下,而没有挂电话,这种处理的方式就是互斥。而如果老总拿其另一部电话说,我在接电话,等会我打给你,然后挂了电话,这种处理的方式就是同步。两者相比,互斥明显占用系统资源(浪费电话费,浪费别人的时间),而同步则是一种更加好的解决问题的思路。
在日常生活中,例如火车售票窗口等经常可以看到“XXX优先”,那么多线程编程中每个线程是否也可以设置优先级呢?
在多线程编程中,支持为每个线程设置优先级。优先级高的线程在排队执行时会获得更多的CPU执行时间,得到更快的响应。在实际程序中,可以根据逻辑的需要,将需要得到及时处理的线程设置成较高的优先级,而把对时间要求不高的线程设置成比较低的优先级。
在Thread类中,总计规定了三个优先级,分别为:
l MAX_PRIORITY——最高优先级
l NORM_PRIORITY——普通优先级,也是默认优先级
l MIN_PRIORITY——最低优先级
在前面创建的线程对象中,由于没有设置线程的优先级,则线程默认的优先级是NORM_PRIORITY,在实际使用时,也可以根据需要使用Thread类中的setPriority方法设置线程的优先级,该方法的声明为:
public final void setPriority(int newPriority)
假设t是一个初始化过的线程对象,需要设置t的优先级为最高,则实现的代码为:
t. setPriority(Thread. MAX_PRIORITY);
这样,在该线程执行时将获得更多的执行机会,也就是优先执行。如果由于安全等原因,不允许设置线程的优先级,则会抛出SecurityException异常。
下面使用一个简单的输出数字的线程演示线程优先级的使用,实现的示例代码如下:
package priority; /** * 测试线程优先级 */ public class TestPriority { public static void main(String[] args) { PrintNumberThread p1 = new PrintNumberThread("高优先级"); PrintNumberThread p2 = new PrintNumberThread("普通优先级"); PrintNumberThread p3 = new PrintNumberThread("低优先级"); p1.setPriority(Thread.MAX_PRIORITY); p2.setPriority(Thread.NORM_PRIORITY); p3.setPriority(Thread.MIN_PRIORITY); p1.start(); p2.start(); p3.start(); } }
package priority; /** * 输出数字的线程 */ public class PrintNumberThread extends Thread { String name; public PrintNumberThread(String name){ this.name = name; } public void run(){ try{ for(int i = 0;i < 10;i++){ System.out.println(name + ":" + i); } }catch(Exception e){} } }
程序的一种执行结果为:
高优先级:0 高优先级:1 高优先级:2 普通优先级:0 高优先级:3 普通优先级:1 高优先级:4 普通优先级:2 高优先级:5 高优先级:6 高优先级:7 高优先级:8 高优先级:9 普通优先级:3 普通优先级:4 普通优先级:5 普通优先级:6 普通优先级:7 普通优先级:8 普通优先级:9 低优先级:0 低优先级:1 低优先级:2 低优先级:3 低优先级:4 低优先级:5 低优先级:6 低优先级:7 低优先级:8 低优先级:9
在该示例程序,PrintNumberThread线程实现的功能是输出数字,每次数字输出之间没有设置时间延迟,在测试类TestPriority中创建三个PrintNumberThread类型的线程对象,然后分别设置线程优先级是最高、普通和最低,接着启动线程执行程序。从执行结果可以看出高优先级的线程获得了更多的执行时间,首先执行完成,而低优先级的线程由于优先级较低,所以最后一个执行结束。
其实,对于线程优先级的管理主要由系统的线程调度实现,较高优先级的线程优先执行,所以可以通过设置线程的优先级影响线程的执行。