【Java编程教材】Java教程之多线程编程

来源:互联网 发布:淘宝lolcdk是真的吗 编辑:程序博客网 时间:2024/06/10 06:37

今天我们写点关于并发编程的东西。作为Java教程中必讲的内容,我们也来看看Java里面的并发编程。

1.1 关于并发的多面性

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

1.1.1 更快执行

传统的摩尔定律对于传统的蕊片有些过时了,想要提高速度就需要以多核处理器的形式而不是更快的芯片的形式出现,为了使程序运行的更快,就必须要学会如何利用这些额外的处理器。像多处理器的web服务器,可以为每个请求分配一个线程,它就可以将大量的用户请求分布到多个CPU上。

摘自Tomcat:

对tomcat来说,每一个进来的请求(request)都需要一个线程,直到该请求结束。如果同时进来的请求多于当前可用的请求处理线程数,额外的线程就会被创建,直到到达配置的最大线程数(maxThreads属性值)。如果仍就同时接收到更多请求,这些来不及处理的请求就会在Connector创建的ServerSocket中堆积起来,直到到达最大的配置值(acceptCount属性值)。至此,任何再来的请求将会收到connection refused错误,直到有可用的资源来处理它们

像tomcat就是典型的多处理器web服务器。

“并发通常是提高运行在单处理器上的程序性能” 我们怎么来理解这句话呢?

如果程序中的某个任务因为该程序控制范围之外的某些条件(通常是I/O)而导致不能继续执行,那这个线程或任务就阻塞了。如果是串行执行这个时候整个任务就停止了。一直会等到外部条件发生变化。但如果这个时候这个程序是用并发写的,那当一个任务阻塞时,程序中其他任务还是可以继续执行的,因此这个程序还是在继续往前走的。如果没有阻塞,那在单处理器上面使用并发就没有任何意义了。使用并发反而会增加CPU的上下文切换次数。

在单处理器系统中性能提高的常见办法是采用事件驱动编程。实现并发最直接的方式就是在操作系统级别使用进程。

像某些编程语言设计将并发任务彼此隔离,这些语言通常叫函数型语言,其中每个函数调用都不会产生任何副作用,并可以当成独立的任务来驱动。像Erlang这种语言,它包含了针对任务之间彼此通信的安全机制。

像Java采取了更传统的方式,在顺序型语言的基础上提供对线程的支持,线程机制是在由执行程序表示的单一进程中创建任务。这种方式产生的下好处是操作系统的透明性,这个也是为了让java程序可以跑在任何一个平台上面。

Java的线程机制是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另一线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。

1.2 基本的线程机制

并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立的任务中的每一个都将由执行线程来驱动。一个线程就是在进程中的一个单一顺序控制流。因此单个进程可以有多个并发执行的任务,但是你的程序使得每个任务都好像有其自己的CPU一样,其底层机制是切分CPU时间。程序员不需要考虑它。

1.2.1 定义任务

线程可以驱动任务,因此就需要一种描述任务的方式。这可以由Runnable接口来提供,要想定义任务只需要实现这个接口,并编写run()方法,使得这个任务执行你的命令。

1.2.2 Thread类

将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器。

Thread构造器只需要一个Runnable对象,调用Thread对象的start()方法为该线程执行必需的初始化操作,然后调用Runnable的run()方法,以便在这个新线程中启动该任务。

1.2.3 使用Executor

在java se5的java.util.concurrent包(需要系统学习下这个包)中的执行器可以管理Thread对象,从而可以简化并发编程。Executor在客户端和任务执行之间提供了一个间接层,与客户端直接执行任务不同,这个中介对象将执行任务,Executor允许你管理异步任务的执行,无须显式地管理线程的生命周期。Executor在JavaSE5/6是启动任务的优选方式。

常见的如:

ExecutorService exec = Executors.newCachedThreadPool();


ExecutorService exec = Executors.newFixedThreadPool(5); // 一次性预先执行线程分配,可以限制线程的数量,这样可以节省时间因为你不用为每个任务都固定地付出创建线程的开销。

注意:在任何线程池中,现有线程在可能的情况下,都会自动被复用。

CachedThreadPool 在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选。

SingleThreadExecutor就像是线程数量为1的FixedThreadPool。这对于希望在另一个线程中连续运行的任何事物(长期存活的任务)都非常有用。因为它就一个线程不存在锁的问题。如果向这个提交了多个任务,那这些任务就会排队,每个任务都会在下一个任务开始之前运行结束,所有任务将使用相同的线程。

1.2.4 从任务中产生返回值

Runnable是执行工作的独立任务,但是它不返回任何值,如果你希望任务在完成时能够返回一个值,那可以实现Callable接口而不是Runnable接口。如下一个示例:

public class TaskWithResult implements Callable<String> {    private int id;    public TaskWithResult(int id){        this.id=id;    }    @Override    public String call() throws Exception {        return "result of TaskwithResult " + 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++){            results.add(exec.submit(new TaskWithResult(i))); // 必须要使用submit()方法来调用它。        }        for (Future<String> fs:results){            try {                System.out.println(fs.get()); // get()将阻塞,直至结果准备就绪.            }catch (Exception ex){            } finally {                exec.shutdown();            }        }    }}

submit()方法会产生Futurn对象。(Callable是产生结果,Futurn对象是拿结果这两个要结合着来)。

1.2.5 休眠

在线程里面异常是不能跨线程传播的。比如子线程里面的异常不能传播到父线程里面。

1.2.6 优先级

线程的优先级将该线程的重要性传递给了调度器。尽管CPU处理现有线程集的顺序是不确定的,但调度器将倾向于让优先权最高的线程先执行。然而这并不是意味着优先权较低的线程将得不到执行,优先级较低的线程仅仅是执行频率较低。

1.2.7 后台线程

后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。当所有非后台线程结束时,程序也就终止了,同时会杀死进程中所有后台线程。

public class SimpleDaemon implements Runnable {        @Override    public void run() {        while (true){            try {                TimeUnit.MICROSECONDS.sleep(100);                System.out.println("Thread.currentThread"+this);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }    public static void main(String[] args) throws InterruptedException {        for (int i = 0; i < 10; i++) {            Thread daemon = new Thread(new SimpleDaemon());            daemon.setDaemon(true);            daemon.start();        }        System.out.println("all daemons start");        TimeUnit.MICROSECONDS.sleep(200);    }}

后台线程必须要在启动之前先setDaemon方法才能把它设置成后台线程.

1.2.11 加入一个线程

一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join()此线程将挂起,直到目标线程t结束才恢复。join还可以指定参数作为超时,这样如果目标线程在这段时间到期时还没有结束的话,join方法总是能够返回.

对join方法的调用可以被中断,做法是调用线程上调用interrupt()方法,这时需要用到try-catch子句。