Java 并发编程

Java并发编程(1):可重入内置锁

每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果摸个线程试图获得一个已经由它自己持有的 锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是调用。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线 程。当计数值为0时,这个锁就被认为是没有被任何线程所持有,当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1,如果同一 个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。

重入进一步提升了加锁行为的封装性,因此简化了面向对象并发代码的开发。分析如下程序:

public class Father
{
    public synchronized void doSomething(){
        ......
    }
}
 
public class Child extends Father
{
    public synchronized void doSomething(){
        ......
        super.doSomething();
    }
}
 

子类覆写了父类的同步方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码件产生死锁。

由于Fither和Child中的doSomething方法都是synchronized方法,因此每个doSomething方法在执行前都会 获取Child对象实例上的锁。如果内置锁不是可重入的,那么在调用super.doSomething时将无法获得该Child对象上的互斥锁,因为这 个锁已经被持有,从而线程会永远阻塞下去,一直在等待一个永远也无法获取的锁。重入则避免了这种死锁情况的发生。

同一个线程在调用本类中其他synchronized方法/块或父类中的synchronized方法/块时,都不会阻碍该线程地执行,因为互斥锁时可重入的。

Java并发编程(2):线程中断(含代码)

使用interrupt()中断线程

当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它 已经被中断,并立即返回。这里需要注意的是,如果只是单纯的调用interrupt()方法,线程并没有实际被中断,会继续往下执行。

下面一段代码演示了休眠线程的中断:

public class SleepInterrupt extends Object implements Runnable{
    public void run(){
        try{
            System.out.println("in run() - about to sleep for 20 seconds");
            Thread.sleep(20000);
            System.out.println("in run() - woke up");
        }catch(InterruptedException e){
            System.out.println("in run() - interrupted while sleeping");
            //处理完中断异常后,返回到run()方法人口,
            //如果没有return,线程不会实际被中断,它会继续打印下面的信息
            return; 
        }
        System.out.println("in run() - leaving normally");
    }
 
    public static void main(String[] args) {
        SleepInterrupt si = new SleepInterrupt();
        Thread t = new Thread(si);
        t.start();
        //主线程休眠2秒,从而确保刚才启动的线程有机会执行一段时间
        try {
            Thread.sleep(2000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("in main() - interrupting other thread");
        //中断线程t
        t.interrupt();
        System.out.println("in main() - leaving");
    }
}

 

 

运行结果如下:

     主线程启动新线程后,自身休眠2秒钟,允许新线程获得运行时间。新线程打印信息“about to sleep for 20 seconds”后,继而休眠20秒钟,大约2秒钟后,main线程通知新线程中断,那么新线程的20秒的休眠将被打断,从而抛出 InterruptException异常,执行跳转到catch块,打印出“interrupted while sleeping”信息,并立即从run()方法返回,然后消亡,而不会打印出catch块后面的“leaving normally”信息。
    请注意:由于不确定的线程规划,上图运行结果的后两行可能顺序相反,这取决于主线程和新线程哪个先消亡。但前两行信息的顺序必定如上图所示。
    另外,如果将catch块中的return语句注释掉,则线程在抛出异常后,会继续往下执行,而不会被中断,从而会打印出”leaving normally“信息。

待决中断

在上面的例子中,sleep()方法的实现检查到休眠线程被中断,它会相当友好地终止线程,并抛出InterruptedException异常。 另外一种情况,如果线程在调用sleep()方法前被中断,那么该中断称为待决中断,它会在刚调用sleep()方法时,立即抛出 InterruptedException异常。

下面的代码演示了待决中断:

public class PendingInterrupt extends Object {
    public static void main(String[] args){
        //如果输入了参数,则在mian线程中中断当前线程(亦即main线程)
        if( args.length > 0 ){
            Thread.currentThread().interrupt();
        }
        //获取当前时间
        long startTime = System.currentTimeMillis();
        try{
            Thread.sleep(2000);
            System.out.println("was NOT interrupted");
        }catch(InterruptedException x){
            System.out.println("was interrupted");
        }
        //计算中间代码执行的时间
        System.out.println("elapsedTime=" + ( System.currentTimeMillis() - startTime));
    }
}

 

 

如果PendingInterrupt不带任何命令行参数,那么线程不会被中断,最终输出的时间差距应该在2000附近(具体时间由系统决定,不精 确),如果PendingInterrupt带有命令行参数,则调用中断当前线程的代码,但main线程仍然运行,最终输出的时间差距应该远小于 2000,因为线程尚未休眠,便被中断,因此,一旦调用sleep()方法,会立即打印出catch块中的信息。执行结果如下:

    这种模式下,main线程中断它自身。除了将中断标志(它是Thread的内部标志)设置为true外,没有其他任何影响。线程被中断了,但main线程 仍然运行,main线程继续监视实时时钟,并进入try块,一旦调用sleep()方法,它就会注意到待决中断的存在,并抛出 InterruptException。于是执行跳转到catch块,并打印出线程被中断的信息。最后,计算并打印出时间差。

使用isInterrupted()方法判断中断状态

   可以在Thread对象上调用isInterrupted()方法来检查任何线程的中断状态。这里需要注意:线程一旦被中 断,isInterrupted()方法便会返回true,而一旦sleep()方法抛出异常,它将清空中断标志,此时isInterrupted()方 法将返回false。
   下面的代码演示了isInterrupted()方法的使用:
public class InterruptCheck extends Object{
    public static void main(String[] args){
        Thread t = Thread.currentThread();
        System.out.println("Point A: t.isInterrupted()=" + t.isInterrupted());
        //待决中断,中断自身
        t.interrupt();
        System.out.println("Point B: t.isInterrupted()=" + t.isInterrupted());
        System.out.println("Point C: t.isInterrupted()=" + t.isInterrupted());
 
        try{
            Thread.sleep(2000);
            System.out.println("was NOT interrupted");
        }catch( InterruptedException x){
            System.out.println("was interrupted");
        }
        //抛出异常后,会清除中断标志,这里会返回false
        System.out.println("Point D: t.isInterrupted()=" + t.isInterrupted());
    }
}

 

 

运行结果如下:

使用Thread.interrupted()方法判断中断状态

可以使用Thread.interrupted()方法来检查当前线程的中断状态(并隐式重置为false)。又由于它是静态方法,因此不能在特定 的线程上使用,而只能报告调用它的线程的中断状态,如果线程被中断,而且中断状态尚不清楚,那么,这个方法返回true。与 isInterrupted()不同,它将自动重置中断状态为false,第二次调用Thread.interrupted()方法,总是返回 false,除非中断了线程。

如下代码演示了Thread.interrupted()方法的使用:

public class InterruptReset extends Object {
    public static void main(String[] args) {
        System.out.println(
            "Point X: Thread.interrupted()=" + Thread.interrupted());
        Thread.currentThread().interrupt();
        System.out.println(
            "Point Y: Thread.interrupted()=" + Thread.interrupted());
        System.out.println(
            "Point Z: Thread.interrupted()=" + Thread.interrupted());
    }
}

 

 

运行结果如下:

从结果中可以看出,当前线程中断自身后,在Y点,中断状态为true,并由Thread.interrupted()自动重置为false,那么下次调用该方法得到的结果便是false。

补充

    这里补充下yield和join方法的使用。
    join方法用线程对象调用,如果在一个线程A中调用另一个线程B的join方法,线程A将会等待线程B执行完毕后再执行。
    yield可以直接用Thread类调用,yield让出CPU执行权给同等级的线程,如果没有相同级别的线程在等待CPU的执行权,则该线程继续执行。
 

Java并发编程(3):线程挂起、恢复与终止的正确方法(含代码)

挂起和恢复线程

    Thread 的API中包含两个被淘汰的方法,它们用于临时挂起和重启某个线程,这些方法已经被淘汰,因为它们是不安全的,不稳定的。如果在不合适的时候挂起线程(比 如,锁定共享资源时),此时便可能会发生死锁条件——其他线程在等待该线程释放锁,但该线程却被挂起了,便会发生死锁。另外,在长时间计算期间挂起线程也 可能导致问题。

下面的代码演示了通过休眠来延缓运行,模拟长时间运行的情况,使线程更可能在不适当的时候被挂起:

public class DeprecatedSuspendResume extends Object implements Runnable{
 
    //volatile关键字,表示该变量可能在被一个线程使用的同时,被另一个线程修改
    private volatile int firstVal;
    private volatile int secondVal;
 
    //判断二者是否相等
    public boolean areValuesEqual(){
        return ( firstVal == secondVal);
    }
 
    public void run() {
        try{
            firstVal = 0;
            secondVal = 0;
            workMethod();
        }catch(InterruptedException x){
            System.out.println("interrupted while in workMethod()");
        }
    }
 
    private void workMethod() throws InterruptedException {
        int val = 1;
        while (true){
            stepOne(val);
            stepTwo(val);
            val++;
            Thread.sleep(200);  //再次循环钱休眠200毫秒
        }
    }
 
    //赋值后,休眠300毫秒,从而使线程有机会在stepOne操作和stepTwo操作之间被挂起
    private void stepOne(int newVal) throws InterruptedException{
        firstVal = newVal;
        Thread.sleep(300);  //模拟长时间运行的情况
    }
 
    private void stepTwo(int newVal){
        secondVal = newVal;
    }
 
    public static void main(String[] args){
        DeprecatedSuspendResume dsr = new DeprecatedSuspendResume();
        Thread t = new Thread(dsr);
        t.start();
 
        //休眠1秒,让其他线程有机会获得执行
        try {
            Thread.sleep(1000);}
        catch(InterruptedException x){}
        for (int i = 0; i < 10; i++){
            //挂起线程
            t.suspend();
            System.out.println("dsr.areValuesEqual()=" + dsr.areValuesEqual());
            //恢复线程
            t.resume();
            try{
                //线程随机休眠0~2秒
                Thread.sleep((long)(Math.random()*2000.0));
            }catch(InterruptedException x){
                //略
            }
        }
        System.exit(0); //中断应用程序
    }
}

 

某次运行结果如下:

从areValuesEqual()返回的值有时为true,有时为false。以上代码中,在设置firstVal之后,但在设置 secondVal之前,挂起新线程会产生麻烦,此时输出的结果会为false(情况1),这段时间不适宜挂起线程,但因为线程不能控制何时调用它的 suspend方法,所以这种情况是不可避免的。

当然,即使线程不被挂起(注释掉挂起和恢复线程的两行代码),如果在main线程中执行asr.areValuesEqual()进行比较时,恰逢stepOne操作执行完,而stepTwo操作还没执行,那么得到的结果同样可能是false(情况2)。
下面我们给出不用上述两个方法来实现线程挂起和恢复的策略——设置标志位。通过该方法实现线程的挂起和恢复有一个很好的地方,就是可以在线程的指定位置实现线程的挂起和恢复,而不用担心其不确定性。

对于上述代码的改进代码如下:

public class AlternateSuspendResume extends Object implements Runnable {
 
    private volatile int firstVal;
    private volatile int secondVal;
    //增加标志位,用来实现线程的挂起和恢复
    private volatile boolean suspended;
 
    public boolean areValuesEqual() {
        return ( firstVal == secondVal );
    }
 
    public void run() {
        try {
            suspended = false;
            firstVal = 0;
            secondVal = 0;
            workMethod();
        } catch ( InterruptedException x ) {
            System.out.println("interrupted while in workMethod()");
        }
    }
 
    private void workMethod() throws InterruptedException {
        int val = 1;
 
        while ( true ) {
            //仅当贤臣挂起时,才运行这行代码
            waitWhileSuspended();
 
            stepOne(val);
            stepTwo(val);
            val++;
 
            //仅当线程挂起时,才运行这行代码
            waitWhileSuspended();
 
            Thread.sleep(200); 
        }
    }
 
    private void stepOne(int newVal)
                    throws InterruptedException {
 
        firstVal = newVal;
        Thread.sleep(300); 
    }
 
    private void stepTwo(int newVal) {
        secondVal = newVal;
    }
 
    public void suspendRequest() {
        suspended = true;
    }
 
    public void resumeRequest() {
        suspended = false;
    }
 
    private void waitWhileSuspended()
                throws InterruptedException {
 
        //这是一个“繁忙等待”技术的示例。
        //它是非等待条件改变的最佳途径,因为它会不断请求处理器周期地执行检查,
        //更佳的技术是:使用Java的内置“通知-等待”机制
        while ( suspended ) {
            Thread.sleep(200);
        }
    }
 
    public static void main(String[] args) {
        AlternateSuspendResume asr =
                new AlternateSuspendResume();
 
        Thread t = new Thread(asr);
        t.start();
 
        //休眠1秒,让其他线程有机会获得执行
        try { Thread.sleep(1000); }
        catch ( InterruptedException x ) { }
 
        for ( int i = 0; i < 10; i++ ) {
            asr.suspendRequest();
 
            //让线程有机会注意到挂起请求
            //注意:这里休眠时间一定要大于
            //stepOne操作对firstVal赋值后的休眠时间,即300ms,
            //目的是为了防止在执行asr.areValuesEqual()进行比较时,
            //恰逢stepOne操作执行完,而stepTwo操作还没执行
            try { Thread.sleep(350); }
            catch ( InterruptedException x ) { }
 
            System.out.println("dsr.areValuesEqual()=" +
                    asr.areValuesEqual());
 
            asr.resumeRequest();
 
            try {
                //线程随机休眠0~2秒
                Thread.sleep(
                        ( long ) (Math.random() * 2000.0) );
            } catch ( InterruptedException x ) {
                //略
            }
        }
 
        System.exit(0); //退出应用程序
    }
}

 

运行结果如下:

由结果可以看出,输出的所有结果均为true。首先,针对情况1(线程挂起的位置不确定),这里确定了线程挂起的位置,不会出现线程在 stepOne操作和stepTwo操作之间挂起的情况;针对情况2(main线程中执行asr.areValuesEqual()进行比较时,恰逢 stepOne操作执行完,而stepTwo操作还没执行),在发出挂起请求后,还没有执行asr.areValuesEqual()操作前,让main 线程休眠450ms(>300ms),如果挂起请求发出时,新线程正执行到或即将执行到stepOne操作(如果在其前面的话,就会响应挂起请求, 从而挂起线程),那么在stepTwo操作执行前,main线程的休眠还没结束,从而main线程休眠结束后执行 asr.areValuesEqual()操作进行比较时,stepTwo操作已经执行完,因此也不会出现输出结果为false的情况。

可以将ars.suspendRequest()代码后的sleep代码去掉,或将休眠时间改为200(明显小于300即可)后,查看执行结果,会发现结果中依然会有出现false的情况。如下图所示:

总结:线程的挂起和恢复实现的正确方法是:通过设置标志位,让线程在安全的位置挂起

终止线程

当调用Thread的start()方法,执行完run()方法后,或在run()方法中return,线程便会自然消亡。另外Thread API中包含了一个stop()方法,可以突然终止线程。但它在JDK1.2后便被淘汰了,因为它可能导致数据对象的崩溃。一个问题是,当线程终止时,很 少有机会执行清理工作;另一个问题是,当在某个线程上调用stop()方法时,线程释放它当前持有的所有锁,持有这些锁必定有某种合适的理由——也许是阻 止其他线程访问尚未处于一致性状态的数据,突然释放锁可能使某些对象中的数据处于不一致状态,而且不会出现数据可能崩溃的任何警告。

终止线程的替代方法:同样是使用标志位,通过控制标志位来终止线程。

 
 
posted @ 2016-07-10 22:00  He_quotes  阅读(107)  评论(0)    收藏  举报