Thinking in java 之并发其三:线程的状态

Thinking in java 之并发其三:线程的状态

一、线程的四种状态

在 java 中,一个线程可以处于下列四种状态之一:

  • 新建(new):当线程被创建时,它会短暂的处于这种状态。在这种状态下时,线程已经分配了必需的系统资源,并执行了初始化。此刻线程已经有资格获得 cpu 时间了,之后调度器将把这个线程转变为就绪或阻塞状态。

  • 就绪(Runnable):在这种状态下,只要调度器把时间片分给线程,线程就可以运行。也就是说,在这种状态下,线程是可以运行也可以不运行的。只要调度器把时间片分给线程,线程立刻可以运行。这是就绪状态与阻塞或死亡状态的区别。

  • 阻塞(Blocked):线程能够运行,但有某个条件阻止了它的运行。当线程进入阻塞状态时,调度器将忽略线程,不会将 cpu 时间分配给它。一个任务进入到阻塞状态,通常有以下几个原因:

    • 通过调用 sleep() 使任务进入休眠状态;
    • 通过调用 wait() 使线程挂起;
    • 任务在等待某个输入/输出完成;
    • 任务试图在某个对象上调用其同步控制方法。
  • 死亡(dead):该状态下,线程不可能再被调度,并且再也不会得到 cpu 时间,它的任务已结束。任务死亡的方式是从 run() 方法返回。

二、终结任务

在一些情况下,我们会希望我们的线程能够在运行一段时间后终止。一种做法是,在 Runnable 里添加一个状态标识码,通过这个状态码来控制任务是否继续进行或者结束。下面就是这种方法的一个例子:

package ThreadTest.SycnSourceTest.concurrency;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class Count{
    private int count=0;
    private Random rand=new Random(47);
    public synchronized int increment() {
        int temp=count;
        if(rand.nextBoolean()) Thread.yield();
        return (count = ++temp);
    }
    public synchronized int value() {
        return count;
    }
}

class Entrance implements Runnable{

    private static Count count = new Count();
    private static List<Entrance> entrances = new ArrayList<Entrance>();
    private int number = 0;
    private final int id;
    private static volatile boolean canceled = false;
    public static void cancel() {canceled = true;}
    public Entrance(int id) {
        this.id = id;
        entrances.add(this);
    }

    @Override
    public void run() {
        while(!canceled) {
            synchronized(this) {
                ++number;
            }
            System.out.println(this+" total: " + count.increment());
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        System.out.println("Stopping "+this);
    }

    public synchronized int getValue(){return number;}
    public String toString() {
        return "Entrances " + id +": " + getValue();
    }
    public static int getTotalCount() {
        return count.value();
    }
    public static int sumEntrances() {
        int sum=0;
        for(Entrance entrance:entrances) {
            sum+=entrance.getValue();
        }
        return sum;
    }
}
public class OrnametalGarden {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i=0;i<5;i++) {
            exec.execute(new Entrance(i));
        }
        TimeUnit.SECONDS.sleep(3);
        Entrance.cancel();
        exec.shutdown();
        if(!exec.awaitTermination(250, TimeUnit.MILLISECONDS))
            System.out.println("Some task were not terminated");
            System.out.println("Total: "+Entrance.getTotalCount());
            System.out.println("Sum of Entrances: "+Entrance.sumEntrances());
    }
}

我们通过布尔变量 cannel 来控制任务是否应该终止,当 main 的线程进行到某一时刻时,我们将 cannel 置为 true (此处的 cannel 是volatile 的,所以它的改变会立刻被其他任务捕捉到),从而终止所有正在进行的任务。

有趣的时,我们从结果中不难发现,计数器并不是递增的,它会出现跳跃的情况。1 2 4 3 6 5... 这说明,虽然某个任务得以先进行,但未必会第一个完成。

java 的 concurrency 包也为我们提供了中断线程的方法。在第一篇线程文章里,我们使用了 Future 实现了让 run() 返回特定类型的信息。Future 也可以帮我们实现中断线程的操作。

如果我们在使用 Excutor 来启动线程时,不使用 executor() 而是使用 submit(),我们就可以获得一个 Future<?> 。这个 Future 是持有任务的上下文的,我们可以通过它的 cancel 方法来实现中断线程的操作。

package ThreadTest.ThreadStatus;

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

class SleepBlocked implements Runnable{
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(100);
        }catch(InterruptedException e) {
            System.out.println("Catch InterruptedExcetpion");
        }
        System.out.println("Exiting SleepBlocked run()");
    }
}

class IOBlocked implements Runnable{
    private InputStream in;
    public IOBlocked(InputStream is) {
        in = is;
    }
    public void run() {
        try {
            System.out.println("Waiting for read()");
            in.read();
        }catch(IOException e) {
            if(Thread.currentThread().isInterrupted()) {
                System.out.println("Interrupted from block I/O");
            }else {
                throw new RuntimeException(e);
            }
        }
        System.out.println("Exiting IOBlocked.run()");
    }
}

class SynchronizedBlocked implements Runnable{
    public synchronized void f() {
        while(true) {
            Thread.yield();
        }
    }
    public SynchronizedBlocked() {
        new Thread() {
            public void run() {
                f();
            }
        }.start();
    }
    public void run() {
        System.out.println("Trying to call f()");
        f();
        System.out.println("Exiting SynchronizedBlocked r()");
    }
}
public class Inturrupting {

    public static ExecutorService exec = Executors.newCachedThreadPool();
    static void test(Runnable r) throws InterruptedException{
        Future<?> f = exec.submit(r);
        TimeUnit.MILLISECONDS.sleep(100);
        System.out.println("Interrupting "+r.getClass().getName());
        f.cancel(true);
        System.out.println("Interrupt sent to "+r.getClass().getName());
    }
    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        test(new SleepBlocked());
        test(new IOBlocked(System.in));
        test(new SynchronizedBlocked());
        TimeUnit.SECONDS.sleep(3);
        System.out.println("Aborting with system.exit(0)");
        System.exit(0);

    }

}

在这个示例中,我们一共对3中阻塞情况进行了中断任务操作。

对于 sleep() 引起的阻塞,在我们通过 Future 对其进行了中断操作之后,任务跑出了 InturruptedException 异常,证明了任务的确被中断。

另外两种情况(IO 阻塞和等待锁阻塞)我们并没有得到它们被中断的输出。这会导致一些问题,尤其是在创建 IO 的任务是,我们可能会被 IO 锁住多线程程序。

一个比较笨拙的解决方式是关闭任务在其上发生阻塞的底层资源。

(此处本该有示例,但是运行结果并没有符合预期,目前原因未知)

Java 的 IO 的 nio 类还为我们提供更加人性化 IO 中断操作。被阻塞的 nio 通道会自动的响应中断。

至于由于等待锁而造成的阻塞,Java 的 ReentrantLock 具备阻塞时中断的功能。

package ThreadTest.ThreadStatus;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class BlockedMutex{
    private Lock lock = new ReentrantLock();
    public BlockedMutex() {
        lock.lock();
    }

    public void f() {
        try {
            lock.lockInterruptibly();
            System.out.println("lock acquire in f()");
        }catch(InterruptedException e) {
            System.out.println("Interrupted from lock acquisitiong in f()");
        }
    }
}

class Blocked2 implements Runnable{
    BlockedMutex block = new BlockedMutex();
    public void run() {
        System.out.println("wait for f() in BlockedMuex");
        block.f();
        System.out.println("Broken out of blocked call");
    }
}
public class Interrupting2 {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Blocked2());
        t.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("Issuing t.interrupt()");
        t.interrupt();
    }

}

BlokedMutex 类的构造器会获取所创建对象上自身的 lock,并且我们没有在任何地方去释放这个锁。所以当其他任务想要调用 f() 时,将会因为Mutex不可获得而被阻塞。在Blcked2中,run() 方法总是在调用 f() 的地方停止。与 I/O 调用不同,interript() 可以打断被互斥锁阻塞的调用。

如果我们编写的程序有线程中断的可能,那么为了避免 run() 里面的循环能够检测到线程被中断并且正确退出(而不是通过抛出异常的方式退出)。检测的方式可以利用 Thread.interrupted() 实现:

package ThreadTest.ThreadStatus;

import java.util.concurrent.TimeUnit;

class NeedsCleanup{
    private final int id;
    public NeedsCleanup(int ident) {
        this.id = ident;
        System.out.println("NeedsCleanUp: " + id);
    }
    public void cleanup(){
        System.out.println("cleaning up " + id);
    }

}

class Blocked3 implements Runnable{
    private volatile double d = 0.0;
    @Override
    public void run() {
        try {
            while(!Thread.interrupted()) {
                NeedsCleanup n1 = new NeedsCleanup(1);
                try {
                    System.out.println("Sleeping");
                    TimeUnit.SECONDS.sleep(1);
                    NeedsCleanup n2 = new NeedsCleanup(2);
                    try {
                        System.out.println("Calculation");
                        for(int i=1;i<2500000;i++) {
                            d=d+(Math.PI+Math.E)/d;
                        }
                        System.out.println("Finished time-consuming operation");
                    }finally {
                        n2.cleanup();
                    }
                }finally{
                    n1.cleanup();
                }
            }
            System.out.println("Exiting via while() test");
        }catch(InterruptedException e) {
            System.out.println("Exiting via InterruptedException");
        }

    }
}
public class InterruptingIdiom {

    private static int tm = 1002;
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(new Blocked3());
        t.start();
        TimeUnit.MILLISECONDS.sleep(tm);
        t.interrupt();
    }

}

在这个示例中 NeedsCleanup 表示一个必须要做清理操作的类。我们使用 try-finally 来保证它的清理方法 cleanup 总是被调用。

通过调节 tm 的值,我们可以控制程序在 sleep 阶段或者在 calculation 阶段停止。当在 sleep 阶段停止时,任务会以抛出异常的方式退出,而在 calculation 阶段停止时,任务会在 while() 的判断处被中断。

推荐阅读更多精彩内容

  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 9,226评论 3 50
  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    胜浩_ae28阅读 3,933评论 0 23
  • 任务和线程的启动很容易。 在大多数时候, 我们都会让它们运行直到结束,或者让它们自行停止。然而,有时候我们希望提前...
    好好学习Sun阅读 612评论 0 0
  • 单任务 单任务的特点是排队执行,也就是同步,就像再cmd输入一条命令后,必须等待这条命令执行完才可以执行下一条命令...
    Steven1997阅读 677评论 0 6
  • 1.GCD GCD(Grand Central Dispatch) 伟大的中枢调度器GCD是苹果为多核的并行运算提...
    Nbm阅读 93评论 0 3