java笔记:熟练掌握线程技术---基础篇之解决资源共享的问题(中)--前篇

  上篇里我讲到了java里如何去创建一个线程的问题,上篇的代码里创建的线程都是独立的,也就是说创建的线程都不会相互的干扰,独立的进行属于自己的运算,更重要的是上篇博文里创建的线程所使用的资源都是独占式的,不会有人跟它争,但是实际对线程的应用中,更多的也是更难的还是几个线程会抢夺同一个资源,比如火车的售票系统,碰到这样的问题就麻烦多了。

  由于这个问题比较复杂我把线程的基础篇中篇分为两篇文章来发布,今天是前篇。回到主题吧,当N多的线程同时访问一个资源,并且N多的线程都有对这个资源修改和访问的能力,解决资源冲突的技术就太重要了,记得我在研究前端优化技术的时候,脑海里浮现最多的名词就是高并发,而对于网站在高并发下又能保证数据的准确性的问题,在我知道java线程调度机制是随机切换时间片的时候,我就感到这个问题比想象中要复杂的多。

  为了便于阐述我要讲的主题,我想要写一个监控程序(Watcher),这个监控程序可以随时检查我们调用的资源的内容比如数字,代码如下:

View Code
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)是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程不是属于程序里不可或缺的部分。所以,当所有的非后台线程结束时,程序也就终止了,反过来说只要有任何非后台线程还在运行,程序就不会终止。大家看下面的代码:

View Code
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线程实际上是观察特定情况下监控对象内部是否违反了约束条件,对于客户而言,客户指向知道我们定义的约束条件是否被违反了,还要知道这个违反约束条件的数值是多少,如是我定义了下面的接口:

package cn.com.sxia;

public interface InvariantState {

}

  这接口就是为了查看数值是否违反我们定义约束的接口,它有两个实现类:

  表示成功的:

package cn.com.sxia;

public class InvariantOK implements InvariantState {

}

  表示失败的:

package cn.com.sxia;

public class InvariantFailure implements InvariantState {
public Object value;

public InvariantFailure(Object value)
{
this.value = value;
}
}

  在InvariantFailure对象将包括一个对象,这个对象表示了有关失败原因的信息,当监控到失败情况我们就可以打印出有关失败的错误信息。

  下面我们再定义一个接口,任何需要对我们定义的约束条件进行测试的类都必须要实现这个接口:

package cn.com.sxia;

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文档里的解释:

    public class Timer extends Object
一种工具,线程用其安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。
与每个 Timer 对象相对应的是单个后台线程,用于顺序地执行所有计时器任务。计时器任务应该迅速完成。如果完成某个计时器任务的时间太长,那么它会“独占”计时器的任务执行线程。因此,这就可能延迟后续任务的执行,而这些任务就可能“堆在一起”,并且在上述不友好的任务最终完成时才能够被快速连续地执行。
对 Timer 对象最后的引用完成后,并且 所有未处理的任务都已执行完成后,计时器的任务执行线程会正常终止(并且成为垃圾回收的对象)。但是这可能要很长时间后才发生。默认情况下,任务执行线程并不作为守护线程 来运行,所以它能够阻止应用程序终止。如果调用者想要快速终止计时器的任务执行线程,那么调用者应该调用计时器的 cancel 方法。
如果意外终止了计时器的任务执行线程,例如调用了它的 stop 方法,那么所有以后对该计时器安排任务的尝试都将导致 IllegalStateException,就好像调用了计时器的 cancel 方法一样。
此类是线程安全的:多个线程可以共享单个 Timer 对象而无需进行外部同步。
此类不 提供实时保证:它使用 Object.wait(long) 方法来安排任务。
实现注意事项:此类可扩展到大量同时安排的任务(存在数千个都没有问题)。在内部,它使用二进制堆来表示其任务队列,所以安排任务的开销是 O(log n),其中 n 是同时安排的任务数。
实现注意事项:所有构造方法都启动计时器线程。

  一切都准备好了,我们创建用于进行监控的完美类了,代码如下:

package cn.com.sxia;

public class 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里跑跑就会有点感觉了,我们接着往下看了。

  我们先从理论开始,共享资源的线程难题到底是啥呢?我们还是套用用笔的例子,有一支笔,两个人同时都要使用它,结果是两个人争执不下,最后谁都没用到这支笔,大家都苦耗在哪里。

  因此我们应该在使用多线程时候避免这样的事情的发生,要防止这样问题的发生,只要在线程使用资源的时候给它加一把锁就行了。那么情形就会变成这样,访问该资源的第一个线程给资源加锁后,其他线程只能等待第一个线程把锁解开才能访问资源,锁解除的同时另外一个线程就可以对该资源加锁并且进行访问了。

  这里我又将引入线程里又一个重要的概念:信号量

  什么是信号量了?这个问题似乎很复杂,我现在获得的理解应该是最简单的理解,下面是我从网上资料总结出来的结论:

多个线程访问某一个资源,例如数据库的连接。假想在服务器上运行着若干个回答客户端请求的线程。这些线程需要连接到同一数据库,但任一时刻只能获得一定数目的数据库连接。你要怎样才能够有效地将这些固定数目的数据库连接分配给大量的线程?一种控制访问一组资源的方法(除了简单地上锁之外),就是使用众所周知的信号量计数 (counting semaphore)。Java多线程信号量计数将一组可获得资源的管理封装起来。信号量是在简单上锁的基础上实现的,相当于能令线程安全执行,并初始化为可用资源个数的计数器。例如我们可以将一个信号量初始化为可获得的数据库连接个数。一旦某个线程获得了Java多线程信号量,可获得的数据库连接数减一。线程消耗完资源并释放该资源时,计数器就会加一。当信号量控制的所有资源都已被占用时,若有线程试图访问此信号量,则会进入阻塞状态,直到有可用资源被释放。Java多线程信号量最常见的用法是解决“消费者-生产者问题”。当一个线程进行工作时,若另外一个线程访问同一共享变量,就可能产生此问题。消费者线程只能在生产者线程完成生产后才能够访问数据。使用信号量来解决这个问题,就需要创建一个初始化为零的信号量,从而让消费者线程访问此信号量时发生阻塞。每当完成单位工作时,生产者线程就会向该信号量发信号(释放资源)。

  对于信号量我们可以简单的这么来理解它,信号量就是两个线程间通信的标志对象。信号量为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函数总会有报错的时候,例如:

Invariant violated: -1

  程序报错退出了,多个线程访问同一个资源会造成数据的错误,这是我们写多线程程序最大的风险所在。

  好了,今天学到这里了,明天我将解决共享资源的博文写完。

 











  

posted @ 2011-12-16 22:52  夏天的森林  阅读(2580)  评论(1编辑  收藏  举报