java多线程使用

2 篇文章 0 订阅
2 篇文章 0 订阅

引言

本文属于作者笔记,知识点来自《java并发编程实战》。共分为4个部分,主要记录java多线程的使用。第一个部分描述多线程是什么、第二个部分描述为什么使用多线程、第三个部分描述在什么场景下使用多线程、怎么简单使用多线程、第四个部分描述使用多线程的注意事项。

多线程是什么?

线程也被称为轻量级进程。在大多数现代操作系统中,都是以线程为基本的调度单位,而不是进程。如果没有明确的协同机制,那么线程将彼此独立执行。线程共享同一个进程中的内存地址空间。

为什么用多线程?

  1. 提高资源利用率:在某些情况下,程序必须等待某个操作完成。而在等待时程序无法执行其他工作。因此,如果程序在等待期间可以运行另一个程序,那么可以提高资源利用率。
  2. 公平性:不同的用户对于计算机上的资源有着相同的使用权,通过粗粒度的时间分片使这些用户的程序能共享计算机资源,而不是一个程序从头到尾执行之后再执行下一个程序。
  3. 便利性:通常来说,计算多个任务,应该编写多个程序,这比编写一个程序来计算多个任务更容易实现。
  4. 发挥多处理器的强大能力:由于基本调度单位是线程,因此如果在程序中只有一个线程,那么最多同时只能在一个处理器上运行。在双处理器,程序只能使用一半CPU资源。使用多线程可以同时在多个处理器上执行。如果设计正确,多线程可以通过提高处理器资源的利用率来提升系统吞吐率。
  5. 建模的简单性:如果在程序中只包含一种类型的任务,那么比包含多种不同类型的任务的程序要更易于编写,错误更少,也更容易测试。
  6. 响应更灵敏的用户界面:如果用户在页面操作执行的任务需要很长的执行时间,那界面的响应灵敏度机会降低。更糟糕的是,不仅页面失去响应,即使包含了取消按钮,也无法取消这个长时间执行的任务。因为只有执行完这个长时间任务才能响应取消操作。如果将这个长时间运行的任务放在一个单独的线程中运行,那么就能及时处理界面事件,从而使用户界面具有更高的灵敏度。
  7. 使代码更容易编写:线程可以降低代码的复杂度、开发和维护成本,使代码更容易编写,阅读和维护。同时提升复杂应用程序的性能。

怎么使用Java多线程?

1.使用场景
大多数并发应用程序都是围绕“任务执行( Task Execution)”来构造的:任务通常是一些抽象的且离散的工作单元。通过把应用程序的工作分解到多个任务中,可以简化程序的组织结构,提供一种自然的事务边界来优化错误恢复过程,以及提供一种自然的并行工作结构来提升并发性。
当围绕“任务执行”来设计应用程序结构时,第一步就是要找出清晰的任务边界。在理想情况下,各个任务之间是相互独立的:任务并不依赖于其他任务的状态、结果或边界效应。独立性有助于实现并发,因为如果存在足够多的处理资源,那么这些独立的任务都可以并行执行。为了在调度与负载均衡等过程中实现更高的灵活性,每项任务还应该表示应用程序的一小部分处理能力。在正常的负载下,服务器应用程序应该同时表现出良好的吞吐量和快速的响应性。应用程序提供商希望程序支持尽可能多的用户,从而降低每个用户的服务成本,而用户则希望获得尽快的响应。而且,当负荷过载时,应用程序的性能应该是逐渐降低,而不是直接失败。要实现上述目标,应该选择清晰的任务边界以及明确的任务执行策略(参见下文)。

2.线程使用与示例

  • 显示地为任务创建线程
    通过为每个请求创建一个新的线程来提供服务,从而实现更高的响应性,如下列程序所示。
/**
 1. 程序清单 不要这么做
 */
class TherdPerTaskWebServer{
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (true){
            final Socket connection = socket.accept();
            Runnable task = new Runnable() {
                public void run() {
                    handleRequest(connection);
                }
            };
          new Thread(task).start();  
        }
    }
}

TherdPerTaskWebServer在结构上类似于单线程版本主线程不断地交替执行“接受外部连接”与“分发请求”等操作。区别在于,对于每个连接,主循环都将创建一个新线程来处理请求,而不是在主循环中进行处理。由此可得出3个主要结论:

  1. 任务处理过程从主线程中分离出来,使得主循环能够更快地重新等待下一个到来的连接 这使得程序在完成前面的请求之前可以接受新的请求,从而提高响应性。
  2. 任务可以并行处理,从而能同时服务多个请求。如果有多个处理器,或者任务由于某种原因被阻塞,例如等待IO完成、获取锁或者资源可用性等,程序的吞吐量将得到提高。
  3. 任务处理代码必须是线程安全的,因为当有多个任务时会并发地调用这段代码。

在正常负载情况下,“为每个任务分配一个线程”的方法能提升串行执行的性能。只要请求的到达速率不超出服务器的请求处理能力,那么这种方法可以同时带来更快的响应性和更高的吞吐率。

无限制创建线程的不足
在生产环境中,“为每个任务分配一个线程”这种方法存在一些缺陷,尤其是当需要创建
大量的线程时:
线程生命网期的开非常高。线程的创建与销毁并不是没有代价的根据平台的不同,实际的开销也有所不同,但线程的创建过程都会需要时间,延迟处理的请求,并且需要JMM和操作系统提供一些辅助操作。如果请求的到达率非常高且请求的处理过程是轻量级的,例如大多数服务器应用程序就是这种情况,那么为毎个请求创建一个新线程将消耗大量的计算资源。
资源消耗。活跃的线程会消耗系统资源,尤其是内存。如果可运行的线程数量多于可用处理器的数量,那么有些线程将闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量线程在竞争CPU资源时还将产生其他的性能开销。如果你已经拥有足够多的线程所有CPU保持忙碌状态,那么再创建更多的线程反而会降低性能。
稳定性。在可创建线程的数量上存在一个限制。这个限制值将随着平台的不同而不同,并且受多个因素制约,包括VM的启动参数、 Thread构造函数中请求的栈大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么很可能抛出OutOfMemory Error异常,要想从这种错误中恢复过来是非常危险的,更简单的办法是通过构造程序来避免超出这些限制。
在一定的范围内,增加线程可以提高系统的吞吐率,但如果超出了这个范围,再创建更多的线程只会降低程序的执行速度,并且如果过多地创建一个线程,那么整个应用程序将崩溃。要想避免这种危险,就应该对应用程序可以创建的线程数量进行限制,并且全面地测试应用程序,从而确保在线程数量达到限制时,程序也不会耗尽资源。
“为每个任务分配一个线程”这种方法的问题在于,它没有限制可创建线程的数量,只限
制了远程用户提交HTTP请求的速率。与其他的并发危险一样,在原型设计和开发阶段,无限制地创建线程或许还能较好地运行,但在应用程序部署后并处于高负载下运行时,才会有问题不断地暴露出来。因此,某个恶意的用户或者过多的用户,都会使Web服务器的负载达到某个阈值,从而使服务器崩溃。如果服务器需要提供高可用性,并且在高负载情况下能平缓地降低性能,那么这将是一个严重的故障。

  • Executor框架
    任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。两种通过线程来执行任务的策略,即把所有任务放在单个线程中串行执行,以及将每个任务放在各自的线程中执行。这两种方式都存在一些严格的限制:串行执行的问题在于其槽糕的响应性和吞吐
    量,而“为每个任务分配一个线程”的问题在于资源管理的复杂性。
    线程池简化了线程的管理工作,并且 javautil。concurrent提供了一种灵活的线程池实现作为 Executor框架的一部分。在Java类库中,任务执行的主要抽象不是 Thread,而是 Executor,如程序清单:
public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

虽然 Executor是个简单的接口,但它却为灵活且强大的异步任务执行框架提供了基础,该框架能支持多种不同类型的任务执行策略。它提供了一种标准的方法将任务的提交过程与执行中为清过程解耦开来,并用 Runnable来表示任务。 Executor的实现还提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。
Executor基于生产者一消费者模式,提交任务的操作相当于生产者(生成待完成的工作单
元),执行任务的线程则相当于消费者(执行完这些工作单元)。如果要在程序中实现一个生产者一消费者的设计,那么最简单的方式通常就是使用 Executor。
基于 Executor来构建Web服务器是非常容易的。在程序清单中用 Executor代替了硬编码的线程创建过程。在这种情况下使用了一种标准的 Executor实现,即一个固定长度的线程池,可以容纳100个线程。
程序清单:

    class TaskExecutionWebserver {
        private static final int NTHREADS = 100;
        private static final Executor exec = Executors.newFixedThreadPool(NTHREADS);

        public static void main(String[] args) throws IOException {
        final      ServerSocket socket = new ServerSocket(80);
            while (true){
                final Socket connection = socket.accept();
                Runnable task = new Runnable() {
                    public void run() {
                        handleRequest(connection);
                    }
                };
                exec.execute(task);
            }
        }

    }

在 TaskExecutionWebServer中,通过使用 Executor,将请求处理任务的提交与任务的实际执行解耦开来,并且只需采用另一种不同的 Executor实现,就可以改变服务器的行为。改变Executor实现或配置所带来的影响要远远小于改变任务提交方式带来的影响。通常, Executor的配置是一次性的,因此在部署阶段可以完成,而提交任务的代码却会不断地扩散到整个程序中,增加了修改的难度。
执行策略
通过将任务的提交与执行解耦开来,从而无须太大的困难就可以为某种类型的任务指定和某修改执行策略。在执行策略中定义了任务执行的“What、 Where、When、How”等方面,包括:

  • 在什么(What)线程中执行任务?
  • 任务按照什么(What)顺序执行(FIFO、LIFO、优先级)?
  • 有多少个( How Many)任务能并发执行?
  • 在队列中有多少个( How Many)任务在等待执行?
  • 如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个( Which)任务?另外,如 何(How)通知应用程序有任务被拒绝?
  • 在执行一个任务之前或之后,应该进行哪些(What)动作?
    各种执行策略都是一种资源管理工具,最佳策略取决于可用的计算资源以及对服务质量的需求。通过限制并发任务的数量,可以确保应用程序不会由于资源耗尽而失败,或者由于在稀缺资源上发生竞争而严重影响性能。通过将任务的提交与任务的执行策略分离开来,有助于在部署阶段选择与可用硬件资源最匹配的执行策略。
    线程池
    线程池,从字面含义来看,是指管理一组同构工作线程的资源池,线程池是与工作队列( Work Queue)密切相关的,其中在工作队列中保存了所有等待执行的任务,工作者线程( Worker Thread)的任务很简单:从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。
    “在线程池中执行任务”比“为每个任务分配一个线程”优势更多。通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。另个额外的好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性,通过适当调整线程池的大小,可以创建足够多的线程以便使处理器保持忙碌状态,同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败。
    类库提供了一个灵活的线程池以及一些有用的默认配置,可以通过调用 Executors中的静态工厂方法之一来创建一个线程池:
    newFixedThreadPool. newFixedThreadPool将创建一个固定长度的线程池,每当提交个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的 Exception而结束,那么线程池会补充一个新的线程)。
    newCachedThreadPool, newCachedThreadPool将创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线,线程池的规模不存在任何限制。
    newSingleThreadExecutor, newSingleThreadExecutor是一个单线程的 Execute,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代,newSingleThreadExecutor能确保依照任务在队列中的顺序来串行执行(例如FIFO、LIFO、优先级)。
    newScheduledThreadPool. newScheduledThreadPool创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于 Timer。
    newFixedThreadPool和newCachedThreadPool这两个工厂方法返回通用的 Thread PoolExecutor实例,这些实例可以直接用来构造专门用途的 executor。
    TaskExecution WebServer中的Web服务器使用了一个带有有界线程池的 Executor,通过execute方法将任务提交到工作队列中,工作线程反复地从工作队列中取出任务并执行它们。
    从“为每任务分配一个线程”策略变成基于线程池的策略,将对应用程序的稳定性产生重大的影响:Web服务器不会再在高负载情况下失败,由于服务器不会创建数千个线程来争夺有限的CPU和内存资源,因此服务器的性能将平缓地降低,通过使用 Executor,可以实现各种调优、管理、监视、记录日志、错误报告和其他功能,如果不使用任务执行框架,那么要增加这些功能是非常困难的。
    Executor的生命周期
    我们已经知道如何创建一个 Executor,但并没有讨论如何关闭它。 Executor的实现通常会创建线程来执行任务。但JVM只有在所有(非守护)线程全部终止后才会退出,因此,如果无法正确地关闭 Executor,那么JVM将无法结束。
    由于 Executor以异步方式来执行任务,因此在任何时刻,之前提交任务的状态不是立即可见的,有些任务可能已经完成,有些可能正在运行,而其他的任务可能在队列中等待执行,当关闭应用程序时,可能采用最平缓的关闭形式(完成所有已经启动的任务,并且不再接受任新的任务),也可能采用最粗暴的关闭形式(直接关掉机房的电源),以及其他各种可能的形式。既然 Executor是为应用程序提供服务的,因而它们也是可关闭的(无论采用平缓的方式还是粗暴的方式),并将在关闭操作中受影响的任务的状态反馈给应用程序。
    为了解决执行服务的生命周期问题, Executor扩展了 ExecutorService接口,添加了一些用于生命周期管理的方法(同时还有一些用于任务提交的便利方法)。在程序清单中给出了ExccutorService中的生命周期管理方法。程序清单ExecutorService中的生命周期管理方法:
public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
     boolean awaitTermination(long timeout, TimeUnit unit)throws InterruptedException;
     //其他用于任务提交的便利方法
}

ExccutorService的生命周期有3种状态:运行、关闭和已终止, ExecutorService在初始创建时处于运行状态, shutdown方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务执行完成一一包括那些还未开始执行的任务, shutdownNow方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。
在 ExecutorService关闭后提交的任务将由“拒绝执行处理(RejectedExecutionHandler)来处理,它会抛弃任务,或者使得 execute方法抛出一个未检查的 RejectedExecution Exception。等所有任务都完成后, ExecutorService将转入终止状态,可以调用await Termination来等待 ExecutorService到达终止状态,或者通过调用 is Terminated来轮询ExecutorService是否已经终止。通常在调用 await Termination之后会立即调shutdown,从而产生同步地关闭 ExecutorService的效果。
程序清单的LifecycleWebServer通过增加生命周期支持来扩展Web服务器的功能。可以通过两种方法来关闭Web服务器:在程序中调用stop,或者以客户端请求形式向Web服务器发送一个特定格式的HTTP请求。
程序清单:

        class LifecyclewebServer {
            private final ExecutorService exec=...;
            public void start() throws IOException {
                ServerSocket socket = new ServerSocket(80);
                while (!exec.isShutdown()){
                    try {
                        final Socket connection = socket.accept();
                        exec.execute(new Runnable() {
                            public void run() {
                                handleRequest(connection); 
                            }
                        });
                    }catch (RejectedExecutionException e){
                        if(!exec.isShutdown())
                            log("task submission rejected",e);
                    }
                }
                
            }
            public void stop(){
                exec.shutdown();
            }
            void handleRequest(Socket connection){
            Request req= readRequest(connection);
            if(isShutdownRequest(req))
                stop();
            else
                dispatchRequest(req);
            }
        }

使用多线程注意事项

  1. 安全性问题:线程的安全性可能是非常复杂的,在没有充足同步的情况下,多个线程的执行顺序是不可预测的,甚至可能产生奇怪的结果。
    .
  2. 活跃性问题:活跃性问题的形式之一就是无意中造成的无限循环,从而使循环之外的代码无法得到执行。线程将带来其他的一些活跃性问题。例如,如果线程A在等待线程B释放其持有的资源,而线程B永远都不会释放该资源,那么线程A将会永久的等待下去。
    .
  3. 性能问题:与活跃性问题密切相关的性能问题。性能问题包括多个方面,比如服务时间过长,响应不灵敏,吞吐率低,资源消耗过高,扩展性较低等。在多线程程序中不仅存在与单线程程序相同的性能问题,而且还存在由于使用线程而引入的其他性能问题。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值