【IT168技术】前文中我讲到了java里如何去创建一个线程的问题,代码里创建的线程都是独立的,也就是说创建的线程都不会相互的干扰,独立的进行属于自己的运算,更重要的是文章里创建的线程所使用的资源都是独占式的,不会有人跟它争,但是实际对线程的应用中,更多的也是更难的还是几个线程会抢夺同一个资源,比如火车的售票系统,碰到这样的问题就麻烦多了。
相关阅读:JAVA线程:两种不同创建方式全面解析
由于这个问题比较复杂我把线程的基础篇中篇分为两篇文章来发布,今天是前篇。回到主题吧,当N多的线程同时访问一个资源,并且N多的线程都有对这个资源修改和访问的能力,解决资源冲突的技术就太重要了,记得我在研究前端优化技术的时候,脑海里浮现最多的名词就是高并发,而对于网站在高并发下又能保证数据的准确性的问题,在我知道java线程调度机制是随机切换时间片的时候,我就感到这个问题比想象中要复杂的多。
为了便于阐述我要讲的主题,我想要写一个监控程序(Watcher),这个监控程序可以随时检查我们调用的资源的内容比如数字,代码如下:
package cn.com.sxia;
public class AlwaysEven {
private int i;
public void next(){
i++;
i++;
}
public int getValue(){
return i;
}
public static void main(String[] args) {
final AlwaysEven ae = new AlwaysEven();
new Thread("Wacther"){
public void run(){
while(true){
int val = ae.getValue();
if (val % 2 == 0){
System.out.println(val);
System.exit(0);
}
}
}
}.start();
while(true)
{
ae.next();
}
}
}
程序注解如下:
AlwaysEven类里有一个属性i,next方法每执行一次i的值会自动加2,getValue返回i的数值。在main函数里我们构建了一个AlwaysEven对象ae,注意这个变量前一定要用final,否则在监控线程里是不能访问到这个变量,最后我们写了一个死循环:调用next方法。
当我们多次执行这个main函数,发现打印出来的结果都会不一样。这个现象道出了运用线程所会遇到的一个基本问题:我们永远都不知道线程何时会运行。这个感觉就像我们创造了一支笔,想用它写字,写着写着,在没有任何征兆的情况下笔不见了,这个实在是很郁闷,但这种情况就是我们在写并发程序经常会遇到的问题。
上面的例子也表现了不同线程共同使用一个资源的现象,监控线程监视ae对象里i属性的数值变化,在主线程main里面又不断调用next方法增加i的数值。这就是在争抢同一个资源的实例。
为了更好阐述我后面要阐述的内容,这里我要补充一下在上篇里漏掉的一部分线程的知识:后台线程(daemon)。后台线程(daemon)是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程不是属于程序里不可或缺的部分。所以,当所有的非后台线程结束时,程序也就终止了,反过来说只要有任何非后台线程还在运行,程序就不会终止。大家看下面的代码:
package cn.com.sxia;
public class SimpleDaemon extends Thread {
public SimpleDaemon(){
setDaemon(true);
start();
}
public void run(){
while(true){
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this);
}
}
public static void main(String[] args) {
for (int i = 0;i < 10;i++){
new SimpleDaemon();
}
}
}
要想使线程成为后台线程,必须则在线程启动前调用setDaemon()方法,才能把这个线程设置为后台线程。当我们运行这个程序时候,发现没有任何结果打印到控制台,这就是因为没有非后台线程(除了main,main是一个非后台线程)使得程序保持运行。因此,程序没有打印任何信息就停止了。
好了,现在回到我们讲到的第一个实例代码,我想根据这个代码改写下,写一个测试框架,这个框架可以简化对我们遇到这种类型线程例子的测试工作。我们的watcher线程实际上是观察特定情况下监控对象内部是否违反了约束条件,对于客户而言,客户指向知道我们定义的约束条件是否被违反了,还要知道这个违反约束条件的数值是多少,如是我定义了下面的接口:
public interface InvariantState {}
这接口就是为了查看数值是否违反我们定义约束的接口,它有两个实现类:
表示成功的:
public class InvariantOK implements InvariantState { }
表示失败的:
public Object value;
public InvariantFailure(Object value)
{
this.value = value;
}
}
在InvariantFailure对象将包括一个对象,这个对象表示了有关失败原因的信息,当监控到失败情况我们就可以打印出有关失败的错误信息。
下面我们再定义一个接口,任何需要对我们定义的约束条件进行测试的类都必须要实现这个接口:
public interface Invariant {
InvariantState invariant();
}
为了防止程序因为所运行的平台(例如不同版本的windows,linux,多核系统等)对java底层技术支持的问题我们再定义一个超时类,这个类当程序在一定时间内无法正常运行时候,程序会自动终止,代码如下:
package cn.com.sxia;
import java.util.Timer;
import java.util.TimerTask;
public class Timeout extends Timer {
public Timeout(int delay,final String msg){
super(true);//设为true表明该线程是一个后台线程(Daemon)
schedule(new TimerTask() {
@Override
public void run() {
System.out.println(msg);
System.exit(0);
}
}, delay);
}
}
代码里我们继承了Timer类,在构造函数里我们调用了super(true),这个设置表明此线程将作为一个后台程序创建,前面我们讲到后台线程不会影响到非后台程序,也就是说当其他线程让程序退出时候,这个创建的Timeout对象不会干扰其他线程的运行。Timer类非常有用,java里设计它就是为了处理大量并发调度任务,下面是Timer在jdk文档里的解释:
一切都准备好了,我们创建用于进行监控的完美类了,代码如下:
InvariantWatcher extends Thread {
private Invariant invariant;
public InvariantWatcher(Invariant invariant){
this.invariant = invariant; setDaemon(true);
start();
}
public InvariantWatcher(Invariant invariant,final int timeout){
this(invariant);
new Timeout(timeout, "超时了....");
}
public void run(){
while(true){
InvariantState state = invariant.invariant();
if (state instanceof InvariantFailure){
System.out.println("Invariant violated: " + ((InvariantFailure)state).value);
System.exit(0);
}
}
}
}
InvariantWatcher类就是我们定义好的监控类,InvariantWatcher类里我定义了两个构造函数,第一个构造函数接受一个要测试的Invariant对象的引用作为参数,然后启动线程。第二个构造函数调用第一个构造函数,然后创建一个Timeout,用来在一定的时间延迟之后终止所有的线程。
特别注意:我们不能再线程里抛出异常,因为这只会终止线程而不会终止程序,所以我都是写的是System.exit(0);
下面我们将我们的个实例代码修改下,代码如下:
package cn.com.sxia;
public class EvenGenerator implements Invariant {
private int i;
public void next(){
i++;
i++;
}
public int getValue(){
return i;
}
@Override
public InvariantState invariant() {
int val = i;
if (val % 2 == 0)
return new InvariantOK();
else
return new InvariantFailure(new Integer(val));
}
public static void main(String[] args){
EvenGenerator gen = new EvenGenerator();
new InvariantWatcher(gen);
while(true){
gen.next();
}
}
}
我们为学习共享资源的java线程问题所设计的监控测试框架已经完成了,或许有些人可能不太明白为啥要这么设计,没事,先把代码在eclipse里跑跑就会有点感觉了,我们接着往下看了。
我们先从理论开始,共享资源的线程难题到底是啥呢?我们还是套用用笔的例子,有一支笔,两个人同时都要使用它,结果是两个人争执不下,最后谁都没用到这支笔,大家都苦耗在哪里。
因此我们应该在使用多线程时候避免这样的事情的发生,要防止这样问题的发生,只要在线程使用资源的时候给它加一把锁就行了。那么情形就会变成这样,访问该资源的第一个线程给资源加锁后,其他线程只能等待第一个线程把锁解开才能访问资源,锁解除的同时另外一个线程就可以对该资源加锁并且进行访问了。
这里我又将引入线程里又一个重要的概念:信号量。
什么是信号量了?这个问题似乎很复杂,我现在获得的理解应该是最简单的理解,下面是我从网上资料总结出来的结论:
对于信号量我们可以简单的这么来理解它,信号量就是两个线程间通信的标志对象。信号量为0,则表明信号量监控的资源是可用的,不为零则信号量监控的资源是不可用的,线程们都要等待了,当资源可用的时候,线程会增加信号量的值,然后继续执行并使用这个监控资源,而信号量这种增加值和减少值的操作是不能被中断的,很保险,所以信号量能够保证两个线程同时访问同一个资源的时候不产生冲突。下面是信号量概念的简化版:
package cn.com.sxia;
public class Semaphore implements Invariant {
private volatile int semaphore = 0;
public void acquire(){
++semaphore;
}
public boolean available(){
return semaphore == 0;
}
public void release(){
--semaphore;
}
@Override
public InvariantState invariant() {
int val = semaphore;
if (val == 0 || val == 1){
return new InvariantOK();
}else{
return new InvariantFailure(new Integer(val));
}
}
}
这个代码里包括三个方法,既然线程在获取资源的时候要检查可用性,我们让调用该类对象,在逻辑上使得semaphore的值都不会是0或1,下面是我写的测试代码了:
package cn.com.sxia;
public class SemaphoreTester extends Thread {
private volatile Semaphore semaphore;
public SemaphoreTester(Semaphore semaphore){
this.semaphore = semaphore;
setDaemon(true);
start();
}
public void run(){
while(true){
if (semaphore.available()){
yield();
semaphore.acquire();
yield();
semaphore.release();
yield();
}
}
}
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore();
new SemaphoreTester(semaphore);
new SemaphoreTester(semaphore);
new InvariantWatcher(semaphore).join();
}
}
大家可以看到run方法里的内容保证了semaphore值都是在0或1来进行,但是我们运行这个main函数总会有报错的时候,例如:
程序报错退出了,多个线程访问同一个资源会造成数据的错误,这是我们写多线程程序最大的风险所在。