第二十一章:并发(中)

来源:互联网 发布:淘宝上有爱弹幕账号么 编辑:程序博客网 时间:2024/06/07 16:19

终结任务

线程状态

  • 线程状态:一个线程可以处于四种状态之一:
    1. 新建(new):当线程被创建时,它只会短暂地处于这种状态。此时它已经分配了必需的系统资源,并执行了初始化。此刻线程已经有资格获得CPU时间了,之后调度器将把这个线程转变为可运行状态或阻塞状态。
    2. 就绪(Runnable):在这种状态下,只要调度器把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度器能分配时间片给线程,它就可以运行;这不同于死亡阻塞状态。
    3. 阻塞(Blocked):线程能够运行,但又某个条件阻止它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入了就绪状态,它才有可能执行操作。
    4. 死亡(Dead):处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断,你将要看到这一点。
  • 进入阻塞状态:一个任务进入阻塞状态,可能有以下原因:
    1. 通过调用sleep()使任务进入休眠状态。
    2. 通过调用wait()使线程挂起。直到线程得到了notify()notifyAll()消息(或signal()、signalAll()消息),线程才会进入就绪状态。
    3. 任务在等待某个输入/输出完成。
    4. 任务试图在某个对象上调用其同步控制方法,但是对象锁已经被其他任务获得。

中断

  • Thread类中包含interrupt()方法,因此你可以终止被阻塞的任务,这个方法将设置线程的中断状态。如果一个线程已经阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将抛出InterruptedException。当抛出异常,或者该任务调用Thread.interrupted()时,中断状态将被复位。
  • 为了调用interrupt(),你必须持有Thread对象。也可以在Executor上调用shutdownNow(),那么它将发送一个interrupt()调用给它启动的所有线程。如果你只想中断Executor中某一个任务而不是全部,那么你必须通过调用submit()(而不是executor())来启动任务,因为这样可以持有任务的上下文。submit()将返回一个泛型Future< ? >,你不需要对其调用get(),你可以直接对这个对象调用cancel(),以此来中断这个任务。
import java.io.IOException;import java.io.InputStream;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;//这个就是sysout,我为了方便使用了静态引入。import static test.thread.PrintUtil.print;public class Interrupting {    private static ExecutorService exec = Executors.newCachedThreadPool();    static void test(Runnable r) throws InterruptedException {        Future<?> f = exec.submit(r);        Thread.sleep(100);        print("Interrupting " + r.getClass().getSimpleName());        f.cancel(true);//中断        print("Interrupt sent to " + r.getClass().getSimpleName());    }    public static void main(String args[]) throws InterruptedException {        test(new SleepBlocked());        test(new IOBlocked(System.in));        test(new SynchronizedBlocked());        Thread.sleep(3000);        print("exit(0)");        System.exit(0);    }}class SleepBlocked implements Runnable {    @Override    public void run() {        try {            Thread.sleep(100000);        } catch (InterruptedException e) {            print("InterruptedException");        }        print("Exiting SleepBlocked.run()");    }}class IOBlocked implements Runnable {    private InputStream in;    public IOBlocked(InputStream in) {        this.in = in;    }    @Override    public void run() {        try {            print("Waiting for read():");            in.read();        } catch (IOException e) {            print(""+Thread.interrupted());            if (Thread.interrupted()) {                print("Interrupted from blocked I/O");            } else {                throw new RuntimeException(e);            }        }        print("Exiting IOBlocked.run()");    }}class SynchronizedBlocked implements Runnable {    public synchronized void f() {        while (true) {            Thread.yield();        }    }    public SynchronizedBlocked() {        new Thread() {            public void run() {                f();            }        }.start();    }    @Override    public void run() {        print("Trying to call f()");        f();        print("Exiting SynchronizedBlocked.run()");    }}--------------运行结果:Interrupting SleepBlockedInterrupt sent to SleepBlockedInterruptedExceptionExiting SleepBlocked.run()Waiting for read():Interrupting IOBlockedInterrupt sent to IOBlockedTrying to call f()Interrupting SynchronizedBlockedInterrupt sent to SynchronizedBlockedexit(0)
  • 从运行结果可以看出,SleepBlocked是可以被中断的阻塞示例。而IOBlockedSynchronizedBlocked是不可被中断的阻塞示例。这个程序证明I/O请求同步锁上的等待是不可中断的。其实通过代码也可以看出,它根本不会抛出InterruptedException
  • 不能中断正在试图获取synchronized锁或者试图执行I/O操作的线程。这有点令人烦恼,特别是在创建执行I/O的任务时,因为这意味着I/O具有锁住你的多线程程序的潜在可能性。对于这类问题,有一个略显笨拙但是确实可行的方法,即关闭任务在其上发生阻塞的底层资源:
import static test.thread.PrintUtil.print;import java.io.InputStream;import java.net.ServerSocket;import java.net.Socket;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class CloseResource {    public static void main(String args[]) throws Exception {        ExecutorService exec = Executors.newCachedThreadPool();        ServerSocket server = new ServerSocket(8080);        InputStream in = new Socket("localhost", 8080).getInputStream();        exec.execute(new IOBlocked(in));//IOBlocked在同一个包下        exec.execute(new IOBlocked(System.in));        Thread.sleep(100);        print("Shuting down all threads");        exec.shutdownNow();        Thread.sleep(1000);        print("Closing " + in.getClass().getSimpleName());        in.close();        Thread.sleep(1000);        print("Closing " + System.in.getClass().getSimpleName());        System.in.close();    }}--------------运行结果:最后System.in.close()好像没有起到作用,所以结果和书上有点差异Waiting for read():Waiting for read():Shuting down all threadsClosing SocketInputStreamInterrupted from blocked I/OExiting IOBlocked.run()Closing BufferedInputStream-------等待输入...
  • nio提供了更加人性化的I/O中断。被阻塞的通道会自动的响应中断:
import java.io.IOException;import java.net.InetSocketAddress;import java.net.ServerSocket;import java.nio.ByteBuffer;import java.nio.channels.AsynchronousCloseException;import java.nio.channels.ClosedByInterruptException;import java.nio.channels.SocketChannel;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;import static test.thread.PrintUtil.print;public class NIOInterruption {    public static void main(String args[]) throws Exception {        ExecutorService exec = Executors.newCachedThreadPool();        ServerSocket server = new ServerSocket(8080);        InetSocketAddress isa = new InetSocketAddress("localhost", 8080);        SocketChannel sc1 = SocketChannel.open(isa);        SocketChannel sc2 = SocketChannel.open(isa);        Future<?> f = exec.submit(new NIOBlocked(sc1));        exec.submit(new NIOBlocked(sc2));        exec.shutdown();        Thread.sleep(1000);        f.cancel(true);        Thread.sleep(1000);        sc2.close();    }}class NIOBlocked implements Runnable {    private final SocketChannel sc;    public NIOBlocked(SocketChannel sc) {this.sc = sc;}    @Override    public void run() {        try {            print("Waiting for read() for " + this);            sc.read(ByteBuffer.allocate(1));        } catch (ClosedByInterruptException e1) {            print("ClosedByInterruptException");        } catch (AsynchronousCloseException e2) {            print("AsynchronousCloseException");        } catch (IOException e) {            throw new RuntimeException(e);         }        print("Exiting NIOBlocked.run() " + this);    }}-------------运行结果:Waiting for read() for test.thread.NIOBlocked@5be60800Waiting for read() for test.thread.NIOBlocked@3b14534ClosedByInterruptExceptionExiting NIOBlocked.run() test.thread.NIOBlocked@3b14534AsynchronousCloseExceptionExiting NIOBlocked.run() test.thread.NIOBlocked@5be60800
  • 尽管请求synchronized同步块的线程是不可被中断的,但是另外一种加锁的方式是可以被中断的。下面是一个简单的例子(不做过多解释):
import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class Interrupting2 {    public static void main(String args[]) throws InterruptedException {        final BlockedMutex b = new BlockedMutex();        Thread t = new Thread(new Runnable() {            @Override            public void run() {                System.out.println("线程请求锁");                b.f();                System.out.println("线程执行完毕");            }        });        t.start();        Thread.sleep(1000);        System.out.println("发送中断");        t.interrupt();//手动中断    }}class BlockedMutex {    private Lock lock = new ReentrantLock();//这个锁是可用被中断的    public BlockedMutex() {        lock.lock();//永久加锁    }    public void f() {        try {            lock.lockInterruptibly();//lockInterruptibly可以被中断            System.out.println("得到锁了,其实这里不可能被执行");        } catch (InterruptedException e) {            System.out.println("被中断了");        }    }}---------------运行结果:线程请求锁发送中断被中断了线程执行完毕

线程之间的协作

wait()与notifyAll()

  • wait()使你可以等待某个条件发生变化,而改变这个条件超出了当前线程的控制能力。通常,这种条件将由另一个任务来改变。如果不用wait(),我们可能会用一个死循环,然后去判断一个条件是否改变(如果一个条件要很久才能改变,这样就白白浪费了cpu)。这种方式叫忙等待,通常是一种不良的cpu周期使用方式。因此wait()会在等待外部世界产生变化时将任务挂起(和sleep()很像,区别是wait()会释放对象锁),并且只有在notify()或notifyAll()发生时,即表示发生了某些感兴趣的事物,这个任务才会被唤醒。
  • 我们知道调用sleep()yield()的时候并没有释放对象锁。但wait()会释放对象锁,这就意味着另一个任务可以获得这个锁。因此,当你调用wait()时,就是在声明:“我已经做完能做的所有事情,因此我要在这里等待,但是我希望其他的synchronized操作在条件合适的情况下能够执行。”
  • 只能在同步控制方法或同步控制块里调用wait()、notify()、notifyAll(),原因的话想想就知道了(没有同步就没有资源竞争,没有资源竞争就不存在自己无法控制的条件,也就用不着wait())。下面是一个模仿书上的示例。我启动两个线程分别做开灯和关灯的操作,以此实现开灯关灯有序交替:
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class LightTest {    public static void main(String args[]) throws InterruptedException {        Light light = new Light();        ExecutorService exec = Executors.newCachedThreadPool();        exec.execute(new TurnOn(light));        exec.execute(new TurnOff(light));        Thread.sleep(5000);//执行5秒        exec.shutdownNow();//中断所有线程,如果在sleep之前或者sleep会打印catch中的信息,如果在sleep之后会正常退出循环        //由于在sleep之后中断的可能性比较小,时间很短暂,所以基本都是会打印catch中的信息    }}class Light {    private boolean isLight = false;//初始状态为关    public synchronized void turnOn() {        System.out.println("要开灯啦");        isLight = true;//开灯        notifyAll();//提醒其他线程条件条件已经改变了    }    public synchronized void turnOff() {        System.out.println("关灯啦");        isLight = false;//关灯        notifyAll();//提醒其他线程条件条件已经改变了    }    public synchronized void waitForOn() throws InterruptedException {        //注意这里用while而不是if,原因有3        //1. 可能有多种同样功能的线程在等待锁的释放,当你抢到锁后,也许灯已经被前面的线程关了,此时你应该重新挂起。        //2. 其他任务对这个线程的条件作出了改变。比如有第三个类型的线程是随机开关灯,如果你抢到锁后,条件又变回去了,你也应该重新挂起。        //3. 条件根本没有改变,其他线程调用了notifyAll()不过是为了唤醒其他任务,此时你也应该重新挂起。        while (isLight == false) {            wait();        }    }    public synchronized void waitForOff() throws InterruptedException {        while (isLight == true) {//注意这里用while而不是if            wait();        }    }}class TurnOn implements Runnable {    private Light light;    public TurnOn(Light l) {light = l;}    @Override    public void run() {        try {            while (!Thread.interrupted()) {                Thread.sleep(200);                light.turnOn();//这里是先开灯,因为初始状态是没开灯                light.waitForOff();            }            System.out.println("开灯被中断了 退出循环");        } catch (InterruptedException e) {            System.out.println("开灯被中断了 catch");        }    }}class TurnOff implements Runnable {    private Light light;    public TurnOff(Light l) {light = l;}    @Override    public void run() {        try {            while (!Thread.interrupted()) {                Thread.sleep(200);                light.waitForOn();//这里是先等待开灯,因为初始状态就是关灯                light.turnOff();            }            System.out.println("关灯被中断了 退出循环");        } catch (InterruptedException e) {            System.out.println("关灯被中断了 catch");        }    }}

错失的信号

  • 不恰当的写法可能会导致错误,如果我们把waitForOff改写成第一种写法,当然是正确的,但是如果我们改写成第二种写法,那么就会导致错误
public void waitForOff() throws InterruptedException {    synchronized (this) {        while (isLight == false) {            wait();        }    }    //第二种写法    while (isLight == false) {        //point_1        synchronized (this) {            wait();        }    }}
  • 也许你会觉得没有差别啊,但事实上是有差别的。第二种写法如果在point_1处线程调度到了其他任务,然后isLight被改成了true,而这个任务还是会wait(),那逻辑就不对了。这还不是最糟的:其他任务在修改isLighttrue后还进行了notifyAll(),而这个任务还没有wait(),那么接下来它将可能进入无限的wait()当中(永远等不到那个已经错失了的信号),这个称为死锁。修改也很简单,就是第一种写法,防止在感兴趣的条件产生竞争,也就是在判断感兴趣的条件时,也得获得线程锁,不允许其他任务对这个条件进行修改。第一种写法,如果isLight 在进入同步块时已经true了,那么就不会wait(),如果先进行了wait(),等isLight 再次修改为true,其他的任务也会发出notify()的信号,也就不会导致死锁

生产者与消费者

  • 看着标题也知道这里要讲什么,其实上面的开灯关灯操作也算是一个生产者与消费者。灯是他们的互斥量,开灯和关灯分别是消费者和生产者(或者反着说也行)。不过他们针对的都是灯这个对象,书上有一个更加完整的消费者和生产者模型,厨师和服务员。厨师生产菜肴(厨师有菜等待,做菜并提醒服务员送菜),服务员负责上菜(服务员无菜等待,送菜并提醒厨师做菜)。括号内黑体标注的即需要获得的对象锁。具体例子我就不给了。

Lock和Condition对象

  • 如果用Lock给对象加锁,可以使用Condition对象进行await()signal()signalAll(),具体例子我就不写了,下面是部分代码:
Lock lock = new ReentrantLock();Condition condition = lock.newCondition();大致写法:lock.lock();try {    isLight = true;    condition.signalAll();    //或者condition.signal();一般是  if(条件)    //或者condition.await(); 一般要  while(感兴趣条件)} finally {    lock.unlock();}

生产者-消费者与队列

  • wait()notifyAll()方法以一种非常低级的方式解决了任务互操作问题。在许多情况下,可以瞄向更高的抽象级别,使用同步队列来解决任务协作问题(同步队列其实使用了Lock和Condition对象,对put和get分别创建了两个Lock和Condition对象,有兴趣可以看源代码)。同步队列在任何时刻都只允许一个任务插入或移除元素。如果消费者任务试图从队列中获得对象,而该队列此时为空,那么这些队列还可以挂起消费者任务,并且当有更多的元素可用时恢复消费者任务。
  • 考虑一个示例,有一台机器具有三个任务:一个制作吐司、一个给吐司抹黄油、一个在抹过黄油的吐司上涂果酱。我们可以通过各个处理过程之间的BlockingQueue来运行这个吐司制作程序:
import java.util.Random;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.LinkedBlockingQueue;class ToastQueue extends LinkedBlockingQueue<Toast> {}public class ToastOMatic {    public static void main(String args[]) throws Exception {        ToastQueue dryQueue = new ToastQueue(),                butteredQueue = new ToastQueue(),                finishedQueue = new ToastQueue();        ExecutorService exec = Executors.newCachedThreadPool();        exec.execute(new Toaster(dryQueue));        exec.execute(new Jammer(butteredQueue, finishedQueue));        exec.execute(new Butterer(dryQueue, butteredQueue));        Thread.sleep(5000);        exec.shutdownNow();//中断所有线程    }}class Toast {    public enum Status {        DRY, BUTTERED, JAMMED    }    private Status status = Status.DRY;    private final int id;    public Toast(int id) {        this.id = id;    }    public void butter() {        status = Status.BUTTERED;    }    public void jam() {        status = Status.JAMMED;    }    public Status getStatus() {        return status;    }    public int getId() {        return id;    }    public String toString() {        return "Toast " + id + ": " + status;    }}//制造吐司的任务class Toaster implements Runnable {    private ToastQueue dryQueue;    private int count = 0;    private Random random = new Random(47);    public Toaster(ToastQueue tq) {        dryQueue = tq;    }    public void run() {        try {            while (!Thread.interrupted()) {                Thread.sleep(100 + random.nextInt(500));                Toast t = new Toast(count++);                System.out.println(t);                dryQueue.put(t);//可能会await() 可能会signal()            }        } catch (InterruptedException e) {            System.out.println("Toaster Interrupted");        }        System.out.println("Toaster off");    }}//刷黄油的任务class Butterer implements Runnable {    private ToastQueue dryQueue, butteredQueue;//从干燥的队列中,拿出来放入已经刷好黄油的队列中    private Random random = new Random(47);    public Butterer(ToastQueue dry, ToastQueue buttered) {        dryQueue = dry;        butteredQueue = buttered;    }    public void run() {        try {            while (!Thread.interrupted()) {                Toast t = dryQueue.take();//可能会await() 可能会signal()                t.butter();                System.out.println(t);                butteredQueue.put(t);//可能会await() 可能会signal()            }        } catch (InterruptedException e) {            System.out.println("Butterer Interrupted");        }        System.out.println("Butterer off");    }}//刷果酱的任务class Jammer implements Runnable {    private ToastQueue butteredQueue, finishedQueue;//从刷好黄油的队列中,拿出来放入已经成品的队列中    private Random random = new Random(47);    public Jammer(ToastQueue buttered, ToastQueue finished) {        finishedQueue = finished;        butteredQueue = buttered;    }    public void run() {        try {            while (!Thread.interrupted()) {                Toast t = butteredQueue.take();//可能会await() 可能会signal()                t.jam();                System.out.println(t);                finishedQueue.put(t);//可能会await() 可能会signal()            }        } catch (InterruptedException e) {            System.out.println("Butterer Interrupted");        }        System.out.println("Butterer off");    }}---------------运行结果:0123456789...

任务间使用管道进行输入/输出

  • 通过输入/输出在线程间进行通信通常很有用,它们在Java I/O类库中的对应物就是PipedWrite类和PipedReader类(是用wait和notify实现的)。这里的管道是一个封装好的解决方案,其基本上就是一个阻塞队列。下面是一个简单的例子:
import java.io.IOException;import java.io.PipedReader;import java.io.PipedWriter;import java.util.Random;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class Test {    public static void main(String args[]) throws Exception {        Sender sender = new Sender();        Receiver receiver = new Receiver(sender);        ExecutorService exec = Executors.newCachedThreadPool();        exec.execute(sender);        exec.execute(receiver);        Thread.sleep(5000);        exec.shutdownNow();//Piped是可中断的    }}class Sender implements Runnable {    private Random random = new Random(47);    private PipedWriter out = new PipedWriter();    public PipedWriter getPipedWriter() {return out;}    @Override    public void run() {        try {            while (true) {                for (char c = 'A'; c < 'z'; c++) {                    out.write(c);                    Thread.sleep(random.nextInt(500));                }            }        } catch (IOException e) {            System.out.println(e + "Sender write exception");        } catch (InterruptedException e) {            System.out.println(e + "Sender sleep interrupted");        }    }}class Receiver implements Runnable {    private PipedReader in = new PipedReader();    public Receiver(Sender sender) throws IOException {        in = new PipedReader(sender.getPipedWriter());    }    @Override    public void run() {        try {            while (true) {                System.out.printf("Read: %c,", (char) in.read());            }        } catch (IOException e) {            System.out.println(e + "Receiver read exception");        }    }}

死锁

  • 前面说过synchronized方法必须在拥有互斥量对象的锁的情况下才能够执行,否则就进入阻塞状态。如果现在有两个任务,和两个互斥量。假设任务1刚好拥有A互斥量,任务2刚好拥有B互斥量。而这个任务必须在拥有两个互斥量时才能正确执行。那么此时就会进入死锁状态。死锁一般难以重现,因为条件一般比较苛刻(也因此难以排查)。但是一旦发生了,就会带来严重性的打击。哲学家就餐问题是一个经典的死锁例证,大家可以去看看。
  • 要想解决死锁问题,先来看看死锁产生必须满足的四个条件:
    1. 互斥条件,任务使用的资源中至少有一个是不能共享的。
    2. 至少有一个任务它持有一个资源且正在等待获取一个当前被别的任务持有的资源。
    3. 资源不能被任务抢占,任务必须把资源释放当作普通事件。
    4. 必须有循环等待,即一个任务等待另外一个任务的资源,知道最后一个任务等待第一个任务的资源。
  • 要想防止死锁,只需破坏其中一个条件即可。在程序中,防止死锁最容易的方法是破坏第四个条件。例如哲学家就餐问题,只要最后一个哲学家尝试先获取右边的叉子,再获取左边的叉子,那么就不会造成死锁了(我们写代码时,可以加个判断,对某个任务进行特殊处理)。
原创粉丝点击