Java多线程设计

在我开发的报表引擎中 ,线程的运用颇多,而且是项目的核心。从最开始的线程设计,到目前为止的设计,有了很大的改进。

一、关于Thread和Runnable的选择
    我们知道用Runnable相对于用Thread有两个优势:
1. 避免继承的局限,采用Runnable接口的方式,不占用extends位置。
2. 适合于资源的共享,多个Thread可以共用一个Runnable对象。
    对于第1点,如果线程本身不需要继承其他类,则使用extends Thread更好,这样就可以直接调用很多操作线程的方法,也无需再多一个Runanble对象(见后面的例子)。对于第2点,资源共享是优点也是缺点。下面举个实际的例子:
【案例】
    我需要new一个线程去处理一个任务(task),同时我想把线程的引用(句柄)保存起来,以便后续对线程进行观察和控制。有两种实现方式:
方式一:

public static Map<String , Thread> threadMap;
......
MyTask task = new MyTask();
Thread thread01 = new MyThread(task);
threadMap.put("thread01", thread01);

直接用thread01即可调用线程的start、getState、isAlive等方法。
方式二:

public static Map<String , Thread> threadMap;
public static Map<String , WorkRunnable> workRunnableMap;
......
MyTask task = new MyTask();
WorkRunnable workRunnable01 = new WorkRunnable(task);
Thread thread01 = new Thread(workRunnable01); //用Thread包装Runnable
threadMap.put("thread01", thread01);
workRunnableMap.put("workRunnable01", workRunnable01);


用workRunnable01不能调用线程的方法,所以还需要把thread01也保存起来。

    显然,对于我这个简单的需求而言,方式一更好。
    所谓的资源共享是优点也是缺点,我也举一个例子来说明:
【案例】
    我想要给一个task(任务)分配一个worker(线程)去执行。可以这么写:
    Thread thread01 = new MyThread(task);
    thread01.start();
或者:
    WorkRunnable workRunnable = new WorkRunnable(task);
    Thread thread01 = new Thread(workRunnable);
    thread01.start();
    记住一点,采用Runnable的优势是什么?是可以在多线程中共用一个Runnable实例,以实现资源共享。例如:
    WorkRunnable workRunnable = new WorkRunnable(task);
    Thread thread01 = new Thread(workRunnable);
    Thread thread02 = new Thread(workRunnable); //线程1线程2共享一个workRunnable
    thread01.start();
    thread02.start();
    这就体现出了Runnable的优势,但是很可惜,我这个案例中,一个task只能由一个worker线程去执行,如果new了两个线程去执行同一个task,则会发生意想不到的后果。
    
    所以,在这个案例下,采用继承Thread的方式比采用实现Runnable的方式更好。
    结论:编写线程时,是采用继承Thread的方式,还是采用实现Runnable的方式,不能一概而论,要视情况而定。

    【个人经验】使用Runnable方式时,可以按照下面这个模板来编写:

public class WorkerRunnable implements Runnable {
    private Thread thisThread; //将Runnable对应的thread保存起来 -- 非常推荐
    private WorkerRunnable thisRunable; //将Runnable自身也保存起来  -- 可选,如果有需要
    @Override
    public void run() {
        thisThread = Thread.currentThread(); //指向当前的线程
        // do something
        ......
    }
    public Thread getThisThread() {
        return thisThread;
    }
    public void setThisRunable(WorkerRunnable thisRunable) {
        this.thisRunable = thisRunable;
    }
}

使用方式:
    WorkerRunnable wkRunnable = new WorkerRunnable();
    wkRunnable. setThisRunable(wkRunnable); //将自身保存起来
好处一:
    想要得到WorkerRunnable的Thread对象时,直接可以调用:
    wkRunnable. getThisThread();
    比如,获取线程的状态:wkRunnable. getThisThread().getState();
好处二:
    在WorkerRunnable的内部,可以使用自身的实例对象wkRunnable。
例如,在外部XxxClass中有一个:
    public static Map<String, WorkerRunnable> workRunnableMap;
在WorkerRunnable中,可以在线程运行完之后,从workRunnableMap中移除runnable:
    public void run() {
        // do something
        ......
        XxxClass.workRunnableMap.remove(thisRunable);
    }

    如果有需要,还可以把上面的thisThread和thisRunable变量定义成public static的,那么直接从WorkerRunnable即可调用它们。


二、增强线程的可控性(单线程和多线程通用)
1. 设置启停控制标志位
    以多线程,实现Runable接口为例。(单线程情况还要简单些,不再多说)
假设有:

public class WorkRunnable implements Runnable {
    public void run() {
        // do something
    }
}
WorkRunnable workRunnable = new WorkRunnable();
Thread thread01 = new Thread(workRunnable);
Thread thread02 = new Thread(workRunnable); //线程1线程2共享一个workRunnable
thread01.start();
thread02.start();

    那么想要控制所有加在workRunnable上的线程,可以通过workRunnable入手。但是如果想要控制单个线程thread01和thread02,则只能通过Thread自带的API去操作了。而坑爹的是,Thread自带的API有些是deprecated的,比如stop、destroy、suspend、resume。
    比如,假设workRunnable的run方法是循环运转的,即:
    public void run() {
        while(true){
            // do something
        }
    }
如果我想要停止所有加在workRunnable上的线程,则可以通过一个标识位来实现:
    /** 控制线程内的while循环,进而控制整个线程的启停 */
    private volatile boolean enabled = false;
    public synchronized boolean getEnabled(){ return enabled;}
    public synchronized void setEnabled(boolean enabled){ this.enabled = enabled;}
    public void run() {
        while( enabled ){
            // do something
        }
    }
    注意我上面的写法的每一个细节。我做了一个关于synchronized和volatile效率问题的简单测试,结果如下:
    ===调用setEnabled方法1亿次,执行耗时:===
    没有这两个关键字时:约0.080秒
    有这两个关键字时:约3.744秒
    单有volatile关键字时:约1.513秒
    单有synchronized键字时:约2.371秒
    我认为在一般情况下是用不着加这两个关键字的,因为当enabled改变时,线程本来就不会立即停止,换句话说线程的关闭对enabled的响应是“迟钝的”,enabled是不是线程同步的,对线程本身几乎不构成影响。但是,如果你的程序有可能在多处同时修改和读取enabled,特别是判断enabled状态后需要进行某些重要的后续操作时,就需要按照我上面的写法加上synchronized和volatile,一个都不能少。有人可能要问,加上了synchronized,为什么还有加上volatile呢?因为set了enabled之后并不一定能保证get时能及时获取到set后的enabled值。volatile关键字的使用,其本质是告诉编译器:不要为该变量保存一份线程内的副本,当变量更新时,直接更新到变量的实际共享存储区域(编译器为了提高效率,会为每个线程使用的变量创建了副本,这个副本只允许该线程访问,其他线程访问时,第一次读取的是变量在共享内存中的值,这个值有可能和其他线程内部的这个值不相等)。
    如果你的程序getEnabled()和setEnabled()不会同时执行,或者即使会同时执行,但getEnabled()不是为了做一些重要的后续处理,则用不着设置synchronized和volatile关键字。下面是我的写法,兼顾了安全、高效和实用:

private volatile boolean enabled = false; //为了安全,volatile关键字必须要
public boolean getEnabled(){ return enabled;} //一般读取时用这个get方法
public synchronized boolean getSynEnabled(){ return enabled;} //重要操作时用这个getSyn方法
public synchronized void setEnabled (boolean enabled){ this.enabled = enabled;}
public void run() {
    while( enabled ){
        // do something
    }
}

    另外注意到,网上有人将run方法的while写成while( getSynEnabled() ),这实际上是没有必要的,因为上面我已经分析了,run方法对enabled标识的响应本来就是“迟钝的”。
    另外还有一个通用的技巧。上面使用synchronized也好,使用volatile也罢,都是为了解决线程同步问题。假如上面的那些get和set方法都是别人写好的,而没有加synchronized,但是你又需要进行严格的同步操作,那怎么办呢?方法很简单,再在get和set方法上再自己包装一层。例如:

public class SynTool4WorkRunnable{
    private WorkRunnable workRunnable;
    public SynTool4WorkRunnable(WorkRunnable workRunnable){
        this.workRunnable = workRunnable;
    }
    public synchronized boolean getSynEnabled(){
        return workRunnable.getEnabled();
    }
    public synchronized void setSynEnabled (boolean enabled){
        workRunnable.setEnabled( enabled );
    }
}

    关于synchronized关键字,能不用就尽量不要用,因为不管它加在属性上,还是加在方法上,都是一个对象锁,只要一个地方锁住,整个对象都被锁住了(此处所说的对象分为两种,一种是静态对象,另一种是对象实例,各是各的锁,互不干扰)。建议对synchronized和volatile理解不深的人在使用这两个关键字之前,先进行深入的研究和试验。
    
2. 设置运行控制标志位(只针对单线程,实现Runnbale接口的多线程不适用)
    运行控制标志位如下:
    /** 线程当前的运行控制状态 */
    private volatile boolean runFlag = false;
    常规意义上讲,线程启动后,runFlag =false代表线程没有运行,runFlag=true代表线程正在运行。但是请注意,它只是一个控制标志位,不是用来观察、显示线程状态的。而且它也并不能准确的代表线程的实际状态(包括未启动、睡眠中、正常运行、已停止等,可以用thread.getState()获取)。
    用法如下:

public synchronized boolean getRunFlag(){ return runFlag;}
public synchronized void setRunFlag(boolean runFlag){ this.runFlag = runFlag;}
public void run() {
    // do something...
    runFlag = false; //标识现在运行已结束
}
//启动线程实例,(以Thread为例,Runable的方法类似)
public synchronized MyThread checkStart (){
    if( runFlag ){ throw new RuntimeException("已经启动");}
    MyThread thread = new MyThread();
    thread. setRunFlag(true);
    thread.start();
    return thread; //返回线程的引用
}

按照如下方式启动上面这个单线程:
    // 适用于在多处(即多线程)环境下启动该单线程
    myThread = myThread. checkStart ();
分析:
    如果当某个线程试图去启动myThread时,它会去争夺myThread.checkStart ()方法的使用权,如果已被其他线程抢占了先机,则它进入等待,直到其他线程释放锁。当它抢到锁,执行时却发现线程已经被启动了,则抛出异常,有效的避免了重复启动。同时,还有一个好处(也是这么设计程序的首要原因),注意到myThread.checkStart ()方法和myThread.getRunFlag()方法不能同时调用,所以使得下面这句话为真:
    “当在外面调用getRunFlag()得到runFlag = false时,那代表线程没有运行,因为runFlag = false说明了run()方法没有运行,或者run()方法已经执行完了最后那一条语句”。
    这是一个很重要的结论。另一方面,当runFlag = true时,说明什么呢?聪明的读者,你不要回答说“runFlag = true代表线程正在运行”,因为线程可能会在run方法的中途挂掉,例如从run方法的中部抛出了未捕获的异常,那么此刻runFlag = true但是线程已经死了。所以,线程有没有挂掉,不能根据runFlag = true来判断,可以用thread.isAlive()来判断。
    如果runFlag = true且thread.isAlive()=false,那说明什么呢?说明线程在我们的预期之外挂掉了,即使外层有个大大的try-catch,线程也可能异常挂掉。我们的runFlag现在又派上用场了。

总结runFlag的主要作用:
    一是“当runFlag = false时证明线程已经正常停止”,二是“当runFlag = true且thread.isAlive()=false时证明线程异常终止”。

三、停止线程的方法
1. 使用标志位
    上面已经讲了,如果线程内通过while来循环执行,则可以设置一个while( enabled )来控制线程的停止。这也是不得已的临时解决方法。这种方法很有局限性,它的前提是while循环体能够在一段时间内循环执行。如果while循环卡在了中部,则while( enabled )根本就起不到控制效果。
2. 使用线程自带的API方法
    按理说,可以用Thread自带的stop方法。但是,这是Java废弃的一个方法,存在安全隐患不说,通常情况下还没有效果。没效果,那还说什么……
3. 我研究出来的一个特殊方法
    网上找不到一个可用的方法,那就只能自己研究了。
    Thread的相关API我已经看完了,各种方法也亲自测试过,通常情况下,stop方法确实是无效的。我初步的想法,是通过制造异常来使线程停止。如下的写法是可行的:

/**
 * 强行杀死线程的方法
 * @param thread
 * @author zollty
 */
public static void stopThread(Thread thread){
    try {
        thread.setPriority(Thread.MIN_PRIORITY);
        thread.stop();
        thread.destroy();
    } catch (Throwable e) {
        DebugTool.println(thread.getName()+"线程被强行杀死...");
    }
}

    不看广告看疗效,经过测试,上面的方法能够安全有效的杀死线程。必须stop和destroy配合着使用。调用stop后,再调用destroy,会导致线程内部的异常,从而杀死线程(这只是一个推测)。第一句thread.setPriority并不是必须的,但是加上也无妨。至于它严格意义上的安全性,还有待考究,不过通常情况下应该不存在问题。
    还有一个笨拙但绝对安全有效的方法 ,适用于结束那种程序正常执行,且执行时间比较线性均匀的方法。在线程执行的方法体内部,在多处加上sleep方法,例如IO操作,假设我们的业务要求“这个线程只能执行1个小时,如果超过1个小时,则强行杀死这个线程”。那么我们怎么强行停止它呢?可以在IO操作的循环处,加上一个“隔一段时间就sleep”的机制,sleep如果失败则抛出异常,结束线程。那么到了一个小时后,如果线程还未停止,我们就在外面调用interrupted方法,到了sleep处,线程就会因为异常而结束了。


© 2009-2020 Zollty.com 版权所有。渝ICP备20008982号