HOME/Articles/

多线程之两阶段终止、以及临界问题

Article Outline

多线程之两阶段终止、以及临界问题

两阶段终止问题

什么是优雅的终止

即如何在线程t1中优雅的终止线程t2,优雅是指给t2线程处理自身关键内容的机会而不是直接强制终止。

错误思路

  • 使用线程对象的stop()方法停止线程

stop() 方法会真正杀死线程,如果此时线程锁住了共享资源,那么当它被杀死后,没有机会释放锁,则其他线程将永远无法获得锁。

  • 使用System.exit()方法停止线程

    目的仅是停止一个线程,但是这种做法会让整个程序都听下来。

所以比较推荐的方法,是需要终止线程时,给线程发送interrupt(),并且线程判断IsInterrupted()来判断是不是发送了打断指令。这里需要注意的是,线程在sleep()的时候,执行interrupt()会抛出异常,但是isInterrupted()仍然是false,所以,要注意捕获这个异常,并且对应终止线程。

以上提到思路,更适用于线程while(true)时终止的情况,如果正常执行,那么更希望线程可以正常运行完成。

多线程上下文切换的问题

临界区

一个程序运行多个线程本身是没有问题的,问题出现在多个线程访问一个共享资源

多个线程读共享资源也是没有问题的,但是多个线程对共享资源读写操作时,因为时间片切换的问题,会发生指令交错,就会出现问题

一段代码块内,如果存在对共享资源的多线程读写操作,称这段代码块为临界区。

竞态条件

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。

应用互斥

为了避免临界区的竞态条件发生,有多种方法可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock

  • 非阻塞式的解决方案: 原子变量

    synchronized俗称对象锁,采用互斥的方法,让同一时刻至多只有一个线程能够持有对象锁,其他线程想再获得这个对象锁,就会被阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

    synchronized实际上是用对象锁保证了临界区代码的原子性,临界区的代码对外是不可分割的,不会被线程切换打断。

public class test {
    static int r =0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    r++;
                }

            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    r--;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
      System.out.println("r="+r);
    }
}



结果为:
r=-423
r=-615。。。。。。

这个代码就是多个线程读写了共享的资源 r。
因为r++和r--操作,对应的底层代码有取数、加1、存数的操作,所以,在执行到中途时,时间片可能用完,而切换为另一个线程,而线程保存当前的执行结果。如线程t1 执行到第二步,r=200,然后等待存数,这时t2执行,r--,r应该为199,但是在切换t1时,继续执行r=200的存数操作,导致出错。

改进:

public class test {
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    synchronized (object) {
                        r++;
                    }
                }

            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    synchronized (object) {
                        r--;
                    }
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("r=" + r);
    }
}

其实synchronized要求锁住一个对象即可,在这段代码中,只要保证线程1和线程2之间由于锁住同一个对象,从而导致互斥即可,所以这里声明了一个object对象,使t1和t2形成互斥即可。