java 并发深入学习一

来源:互联网 发布:网络dns存在问题 编辑:程序博客网 时间:2024/06/09 17:25

以下内容均学习自Thinking in Java。

并发学习第二章,任务间的资源共享,终止与协作

并发学习第三章,死锁、新控件、仿真与其他

1、并发的多面性

    并发编程令人困惑的一个主要原因 :使用并发时需要解决的问题有多个,而实现方法也有多种,并且两者之间没有明显的映射关系。因此必须理解所有这些问题和特例,以便有效地使用并发。

    用并发解决的问题大体上可以分为“速度”和“设计可管理性”两种。


1.1 更快的执行

如果想要一个程序运行的更快,那么可以将其断开为多个片段,在单独的处理器上运行每个片段。并发是用于多处理器编程的基本工具,但是并发通常是提高运行在单处理器上的程序的性能。
   使这个问题变得不同的是阻塞,如果程序中的某个人物因为该程序控制范围之外的某些条件(如I/O)而导致不能继续运行,那么我们就说这个任务或者线程阻塞了。如果没有并发,则整个程序都将停止下来,直至外部条件发生变化。从性能的角度看,如果没有任务会阻塞,那么在单处理器机器上使用并发就没有任何意义。
实现并发最直接的方式是在操作系统级别使用进程。进程是运行在它自己的地址空间内的自包容程序。操作系统通常会将进程互相隔开,因此它们不会彼此干涉,这使得用进程编程相对容易点。于此相反的是,像Java所使用的这种并发系统会共享诸如内存和I/O这样的资源,因此编写多线程程序最基本的困难在于协调不同线程驱动的任务之间对这些资源的使用,以使得这些资源不会同时被多个任务访问。

1.2 改进代码设计

并发提供了一个重要的组织结构上的好处:你的程序设计可以极大的简化。某些类型的问题,例如仿真,没有并发的支持是很难解决的。
以仿真为例,完整的仿真可能涉及非常大量的任务,这与仿真中的每个元素都可以独立动作这一事实相对应。多线程系统对可用的线程数量的限制通常都会是一个相对较小的数字,有时就是数十或数百的数量级。解决这个问题的典型方式是使用协作多线程。
Java的线程机制是抢占式的,这表示调度机制会周期性的终端线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合适的时间去驱动它的任务。在协作式系统中,每个任务都会自动地放弃控制,这要求程序员要有意识地在每个任务中插入某种类型的让步语句。协作式系统的优势是双重的:上下文切换的开销通常比抢占式系统要低廉许多,并且对可以同时执行的线程数量理论上没有限制。
在另一个极端,当使用流行的消息系统工作时,由于消息系统涉及分布在整个网络中的多台独立的计算机,因此并发将会成为一种非常有用的模型。在这种情形中,所有的进程都彼此完全独立地运行,甚至没有任何可能去共享资源。但是,人就必须在进程间同步信息,使得整个消息系统不会丢失信息或在错误的时刻混进信息。
并发需要付出代价,包含复杂性代价,但是这些代价与在程序设计、资源负载均衡以及用户方便使用方面的改进相比,就显得微不足道了。通常线程使你能够创建更加松散耦合的设计,否则,代码各个部分都必须显示地关注哪些通常可以由线程来处理的任务。

2、基本的线程机制

    并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立任务中的每一个都将由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,因此单个进程可以拥有多个并发执行的任务。

2.1 定义任务

    线程可以驱动任务,因此需要一种描述任务的方式,这可以用Runnable接口来提供。如下:
public class LiftOff implements Runnable {    protected int countDown = 10;    private static int taskCount = 0;    private final int id = taskCount++; // final型标示符id可以用来区分任务的多个实例    public LiftOff() {    }    public LiftOff(int countDown) {        this.countDown = countDown;    }    public String status() {        return "#" + id + "(" + (countDown > 0 ? countDown : "Liftoff!") + "),";    }    /* run()通常总会用某种形式的循环,使得任务一直运行下去直到不再需要。所以要设定跳出循环的条件。     * 通常,run()被写成无限循环的形式,这就意味着,除非有某个条件使得run()终止,否则它将永远运行下去。     */    @Override    public void run() {        while (countDown-- > 0) {            System.out.print(status());           /* 对线程调度器的一种建议,声明“已完成生命周期中最重要的部分,此刻是切换给其他任务执行一段时间的大好时机”,              这完全是可选择性的。       */            Thread.yield();        }    }}

直接驱动,无线程能力。
public class MainThread {public static void main(String[] args){LiftOff launch = new LiftOff();launch.run();}}/* Output: #0(9),#0(8),#0(7),#0(6),#0(5),#0(4),#0(3),#0(2),#0(1),#0(Liftoff!),

2.2 Thread类

    将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器,下例:
public class BasicThreads {public static void main(String[] args){Thread t = new Thread(new LiftOff());//Thread构造器需要一个Runnable对象t.start();//start()方法为线程执行的必需的初始化操作。        //输出语句在t.start()之前就出现了,意味着程序仍旧执行main()线程System.out.println("Waiting for liftOff");}}/*output:Waiting for liftOff        #0(9),#0(8),#0(7),#0(6),#0(5),#0(4),#0(3),#0(2),#0(1),#0(Liftoff!),*/

    添加更多的线程,驱动更多的任务:
public class MoreBasicThreads {  public static void main(String[] args) {    for(int i = 0; i < 5; i++)      new Thread(new LiftOff()).start();    System.out.println("Waiting for LiftOff");  }} /* Output: (Sample)Waiting for LiftOff#0(9), #1(9), #2(9), #3(9), #4(9), #0(8), #1(8), #2(8), #3(8), #4(8), #0(7), #1(7), #2(7), #3(7), #4(7), #0(6), #1(6), #2(6), #3(6), #4(6), #0(5), #1(5), #2(5), #3(5), #4(5), #0(4), #1(4), #2(4), #3(4), #4(4), #0(3), #1(3), #2(3), #3(3), #4(3), #0(2), #1(2), #2(2), #3(2), #4(2), #0(1), #1(1), #2(1), #3(1), #4(1), #0(Liftoff!), #1(Liftoff!), #2(Liftoff!), #3(Liftoff!), #4(Liftoff!),*///:~
    当程序再运行一次后,结果可能与前一次不同,因为线程调度机制是非确定性的。

2.3 使用Executor
    Java SE5的执行器(Executor)将为你管理Thread对象,从而简化了并发过程。Executor在客户端和任务执行之间提供了一个简介层;与客户端直接执行任务不同,这个中介对象将执行任务。Executor允许你管理异步任务的执行,而无需显示地管理线程的生命周期。Executor在Java SE5/6中是启动任务的优选方案。

    使用Executor代替上例
public class CachedThreadPool {public static void main(String[] args){ExecutorService exec = Executors.newCachedThreadPool();for(int i = 0; i < 5; i++)exec.execute(new LiftOff());exec.shutdown();}}
CachedThreadPool将为每个任务都创建一个线程 - 在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Exectutor的首选。 
shutdown()方法阻止更多的任务进入处理器中,此时任务处理器等待任务结束(而不是强行结束处理器中所有任务)。

    替换不同的Executor:
 // Constructor argument is number of threads:    ExecutorService exec = Executors.newFixedThreadPool(5);
    FixedThreadPool可一次性预先执行代价高昂的线程分配,因此也就可以限制线程的数量。
    注意,在任何线程池中,现有线程在可能的情况下,都会被自动复用。
    SingleThreadExecutor就像是线程数量为1的FixedThreadPool。这对于希望在另一个线程中连续运行的任何事物来说(长期存活),都是很有用的,例如监听进入的套接字连接任务。它对于希望在线程中运行的短任务也同样方便,例如更新本地或者远程日志的小任务,或者事件分发线程。
public class SingleThreadExecutor {  public static void main(String[] args) {    ExecutorService exec =      Executors.newSingleThreadExecutor();      /*如果向SingleThreadExector提交了多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,      所有的任务将使用相同的线程。SingleThreadExector会序列化所有提交的任务,并会维护它自己的悬挂任务队列*/    for(int i = 0; i < 5; i++)      exec.execute(new LiftOff());    exec.shutdown();  }} /* Output:#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(Liftoff!), #1(9), #1(8), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(Liftoff!), #2(9), #2(8), #2(7), #2(6), #2(5), #2(4), #2(3), #2(2), #2(1), #2(Liftoff!), #3(9), #3(8), #3(7), #3(6), #3(5), #3(4), #3(3), #3(2), #3(1), #3(Liftoff!), #4(9), #4(8), #4(7), #4(6), #4(5), #4(4), #4(3), #4(2), #4(1), #4(Liftoff!),*///:~
    

2.4 从任务中产生返回值

   Runnable是执行工作的独立任务,但是它不返回任何值。如果希望任务在完成时能返回一个值,那可以实现Callable接口。
    Java SE5中引入的Callable是一种具有类型参数的泛型,它的类型参数表示的是从方法call()中返回的值,并且必须使用ExecutorService.submit()方法来调用。简单例子:
public class TaskWithResult implements Callable<String> {private int id;public TaskWithResult(int id){this.id = id;}@Overridepublic String call() throws Exception {System.out.println("here's task");return "result : " + id;}}
    

public class CallableDemo {public static void main(String args[]){ExecutorService exec = Executors.newCachedThreadPool();ArrayList<Future<String>> results = new ArrayList<Future<String>>();for(int i = 0 ; i < 10; i++){//            产生Future对象results.add(exec.submit(new TaskWithResult(i)));}for(Future<String> fs : results)try {System.out.println(fs.get());//调用get()来获取结果} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}finally{exec.shutdown();}}}
可以用Future对象的isDone()方法来查询是否已经完成。

2.5 休眠

影响人物行为的一种简单方法是调用sleep(),这将使得任务终止执行给定的时间。修改LiftOff例子将yield()的调用换成sleep():
public class SleepingTask extends LiftOff {    public void run() {        try {            while (countDown-- > 0) {                System.out.print(status());                // Old-style:                // Thread.sleep(100);                // Java SE5/6-style:                TimeUnit.MILLISECONDS.sleep(100);            }        } catch (InterruptedException e) {            //异常不能跨线程传播回main(),所以需要在任务内部产生的异常。            System.err.println("Interrupted");        }    }    public static void main(String[] args) {        ExecutorService exec = Executors.newCachedThreadPool();        for (int i = 0; i < 5; i++)            exec.execute(new SleepingTask());        exec.shutdown();    }} /* Output:#0(9), #1(9), #2(9), #3(9), #4(9), #0(8), #1(8), #2(8), #3(8), #4(8), #0(7), #1(7), #2(7), #3(7), #4(7), #0(6), #1(6), #2(6), #3(6), #4(6), #0(5), #1(5), #2(5), #3(5), #4(5), #0(4), #1(4), #2(4), #3(4), #4(4), #0(3), #1(3), #2(3), #3(3), #4(3), #0(2), #1(2), #2(2), #3(2), #4(2), #0(1), #1(1), #2(1), #3(1), #4(1), #0(Liftoff!), #1(Liftoff!), #2(Liftoff!), #3(Liftoff!), #4(Liftoff!),*///:~

2.6 优先级

        线程的优先级将该线程的重要性传递给了调度器。我们可以用getPriority()来读取现有线程的优先级,并且可以通过setPriority()来修改,调整优先级的时,为了可移植性,使用MAX_PRIORITY,NORM_PRIORITY,MIN_PRIORITY三个级别。
public class SimplePriorities implements Runnable {    private int countDown = 5;    private volatile double d; // No optimization    private int priority;    public SimplePriorities(int priority) {        this.priority = priority;    }    public String toString() {        return Thread.currentThread() + ": " + countDown;    }    public void run() {        Thread.currentThread().setPriority(priority);//设置优先级,在构造器中设置无意义,只有在run中设置才有意义        while (true) {            // An expensive, interruptable operation:            for (int i = 1; i < 100000; i++) {                d += (Math.PI + Math.E) / (double) i;                if (i % 1000 == 0)                    Thread.yield();            }            System.out.println(this);            if (--countDown == 0) return;        }    }    public static void main(String[] args) {        ExecutorService exec = Executors.newCachedThreadPool();        for (int i = 0; i < 5; i++)            exec.execute(                    new SimplePriorities(Thread.MIN_PRIORITY));        exec.execute(                new SimplePriorities(Thread.MAX_PRIORITY));        exec.shutdown();    }}

2.7 让步

可以通过Thread.yield()方法来实现让步。注意,任何重要的控制或在调整应用时,都不能依赖于yield()。

2.8 后台线程

所谓后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。
只要有任何非后台线程还在运行,程序就不会终止。反之,当所有的非后台程序终止,后台程序也会终止。
            Thread daemon = new Thread(new SimpleDaemons());            daemon.setDaemon(true); // Must call before start()            daemon.start();
注意,必须在线程启动前就调用setDaemon()方法。

通过编写定制的ThreadFactory可以定制由Executor创建的线程的属性(后台、优先级、名称):
//与普通的ThreadFactory的差别就是它把后台状态全部设置为true。public class DaemonThreadFactory implements ThreadFactory {    public Thread newThread(Runnable r) {        Thread t = new Thread(r);        t.setDaemon(true);        return t;    }} ///:~

用一个新的DaemonThreadFactory作为参数传递给Executor.newCachedThreadPool():
public static void main(String[] args) throws Exception {        ExecutorService exec = Executors.newCachedThreadPool(                new DaemonThreadFactory());        for (int i = 0; i < 10; i++)            exec.execute(new DaemonFromFactory());        print("All daemons started");        TimeUnit.MILLISECONDS.sleep(500); // Run for a while    }

每个静态ExecutorService创建方法都被重载为接受一个ThreadFactory对象,而这个对象将用来创建新的线程

public class DaemonThreadPoolExecutor        extends ThreadPoolExecutor {    public DaemonThreadPoolExecutor() {        super(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,                new SynchronousQueue<Runnable>(),                new DaemonThreadFactory());    }} ///:~

可以通过调用isDaemon()方法来确定线程是否是一个后台线程。
后台线程它创建的任何线程都被自动设置为后台线程。
后台进程的finally子句不会运行。

2.9 编码的变体

在非常简单的情况下,可能会希望使用直接从Thread继承这种可替换的方式:
public class SimpleThread extends Thread {    private int countDown = 5;    private static int threadCount = 0;    public SimpleThread() {        // Store the thread name:        super(Integer.toString(++threadCount));        start();    }    public String toString() {        return "#" + getName() + "(" + countDown + "), ";    }    public void run() {        while (true) {            System.out.print(this);            if (--countDown == 0)                return;        }    }    public static void main(String[] args) {        for (int i = 0; i < 5; i++)            new SimpleThread();    }} /* Output:#1(5), #1(4), #1(3), #1(2), #1(1), #2(5), #2(4), #2(3), #2(2), #2(1), #3(5), #3(4), #3(3), #3(2), #3(1), #4(5), #4(4), #4(3), #4(2), #4(1), #5(5), #5(4), #5(3), #5(2), #5(1),*///:~

自管理的Runnbale:
public class SelfManaged implements Runnable {    private int countDown = 5;    private Thread t = new Thread(this);    public SelfManaged() {        t.start();    }    public String toString() {        return Thread.currentThread().getName() +                "(" + countDown + "), ";    }    public void run() {        while (true) {            System.out.print(this);            if (--countDown == 0)                return;        }    }    public static void main(String[] args) {        for (int i = 0; i < 5; i++)            new SelfManaged();    }}

与前面的自管理Thread不同的是使用接口可以继承不同的类。
要意识到,在构造器中启动线程可能会变得很有问题,因为当另一个任务可能会在构造器结束之前开始运行,这意味着该任务的访问出于不稳定状态。这是优选Executor而不是显示地创建Thread对象的另一个原因。

通过内部类来将线程代码隐藏在类中:
class InnerThread1 {    private int countDown = 5;    private Inner inner;    private class Inner extends Thread {        Inner(String name) {            super(name);            start();        }        public void run() {            try {                while (true) {                    print(this);                    if (--countDown == 0) return;                    sleep(10);                }            } catch (InterruptedException e) {                print("interrupted");            }        }        public String toString() {            return getName() + ": " + countDown;        }    }    public InnerThread1(String name) {        inner = new Inner(name);    }}
完整例子:https://github.com/xu509/Java-practise/blob/master/src/main/java/concurrency/ThreadVariations.java

2.10 术语

       要执行的任务与驱动它的线程之间有一个差异,这个差异在Java类库中尤为明显,因为我们队Thread类实际没有任何控制权(并且这种隔离在使用执行器时更加明显,因为执行器将替你处理线程的创建和管理)。
         Thread类自身不执行任何操作,它只是驱动赋予他的任务,而不是“线程就是任务”。
 从概念上,我们希望创建独立于其他任务运行的任务,因此我们应该能够定义任务,然后说“开始”,并且不用操心其细节。从物理上,创建线程可能会代价高昂,因此必须保存并管理它们。
Java的线程机制基本来自于C的低级的p线程的方式。这种低级特性部分地渗入了Java的实现中,因此为了处于更高的抽象级别,在编写代码时,你必须遵循规则。

2.11 加入一个线程

一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复。
也可以在调用join()时带上一个超时参数,如果超时则返回。
对join()方法的调用可以被中断,做法是在调用线程上调用interrupt()方法,这时需要用到try-catch子句。例子:
package concurrency;//: concurrency/Joining.java// Understanding join().import static net.mindview.util.Print.print;class Sleeper extends Thread {    private int duration;    public Sleeper(String name, int sleepTime) {        super(name);        duration = sleepTime;        start();    }    public void run() {        try {            sleep(duration);        } catch (InterruptedException e) {            print(getName() + " was interrupted. " +                    "isInterrupted(): " + isInterrupted());            return;        }        print(getName() + " has awakened");    }}class Joiner extends Thread {    private Sleeper sleeper;    public Joiner(String name, Sleeper sleeper) {        super(name);        this.sleeper = sleeper;        start();    }    public void run() {        try {            sleeper.join();//join        } catch (InterruptedException e) {            print("Interrupted");        }        print(getName() + " join completed");    }}public class Joining {    public static void main(String[] args) {        Sleeper                sleepy = new Sleeper("Sleepy", 1500),                grumpy = new Sleeper("Grumpy", 1500);        Joiner                dopey = new Joiner("Dopey", sleepy),                doc = new Joiner("Doc", grumpy);        grumpy.interrupt();    }} /* Output:Grumpy was interrupted. isInterrupted(): falseDoc join completedSleepy has awakenedDopey join completed*///:~

2.12 创建有响应的用户界面

class UnresponsiveUI {    private volatile double d = 1;    public UnresponsiveUI() throws Exception {        while (d > 0)            d = d + (Math.PI + Math.E) / d;        System.in.read(); // Never gets here    }}public class ResponsiveUI extends Thread {    private static volatile double d = 1;    public ResponsiveUI() {        setDaemon(true);        start();    }    public void run() {        while (true) {            d = d + (Math.PI + Math.E) / d;        }    }    public static void main(String[] args) throws Exception {        //! new UnresponsiveUI(); // Must kill this process        new ResponsiveUI();        System.in.read();        System.out.println(d); // Shows progress    }} ///:~

2.13 线程组

线程组持有一个线程集合,可以把他看成一次不成功的尝试,忽略就好。

2.14 捕获异常

异常一旦逃出任务的run()方法,它就会向外传播到控制台,你在主程序块使用try,catch也无法捕捉到线程的异常。除非采取特殊的步骤捕获这种错误异常。在JAVA SE5后可以用Executor来解决这个问题(之前只能靠线程组)。
为了解决这个问题,我们需要修改Exector产生线程的方式。Thread.UncaughtExceptionHandler是Java SE5中的新街口,允许在每个Thread对象上都附着一个异常处理器。Thread.uncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用。
为了使用他,我们创建了一个新类型的ThreadFactory,它将在每个新创建的Thread对象上附着一个Thread.UncaughtExceptionHandler。我们将这个工厂传递给Executors创建新的ExecutorService的方法:

class ExceptionThread2 implements Runnable {    public void run() {        Thread t = Thread.currentThread();        System.out.println("run() by " + t);        System.out.println(                "eh = " + t.getUncaughtExceptionHandler());        throw new RuntimeException();    }}class MyUncaughtExceptionHandler implements        Thread.UncaughtExceptionHandler {    public void uncaughtException(Thread t, Throwable e) {        System.out.println("caught " + e);    }}class HandlerThreadFactory implements ThreadFactory {    public Thread newThread(Runnable r) {        System.out.println(this + " creating new Thread");        Thread t = new Thread(r);        System.out.println("created " + t);        t.setUncaughtExceptionHandler(                new MyUncaughtExceptionHandler());        System.out.println(                "eh = " + t.getUncaughtExceptionHandler());        return t;    }}public class CaptureUncaughtException {    public static void main(String[] args) {        ExecutorService exec = Executors.newCachedThreadPool(                new HandlerThreadFactory());        exec.execute(new ExceptionThread2());    }} /* Output: (90% match)HandlerThreadFactory@de6ced creating new Threadcreated Thread[Thread-0,5,main]eh = MyUncaughtExceptionHandler@1fb8ee3run() by Thread[Thread-0,5,main]eh = MyUncaughtExceptionHandler@1fb8ee3caught java.lang.RuntimeException*///:~

如果要使用相同的异常处理器,简单的方式是在Thread类中设置一个静态域,并将这个处理器设置为默认的未捕获的异常处理器:
public class SettingDefaultHandler {    public static void main(String[] args) {        Thread.setDefaultUncaughtExceptionHandler(                new MyUncaughtExceptionHandler());        ExecutorService exec = Executors.newCachedThreadPool();        exec.execute(new ExceptionThread());    }} /* Output:caught java.lang.RuntimeException*///:~

这个处理器只有在不存在线程专有的未捕获异常处理器的情况下才会被调用。
系统会检查线程的专有版本,如果没有发现,则检查线程组是否有其专有的uncaughtException()方法,如果也没有,再调用defaultUncaughtExceptionHandler。




0 0
原创粉丝点击