【Java并发】JAVA并发编程实战-读书笔记13

JVM既可以通过正常手段关闭,也可以强行关闭。当最后一个“正常(非守护)”线程终结时或者有人调用了System.exit时以及铜鼓哦使用其他与平台相关手段时(比如发送了SIGINT或键入Ctrl-C),都可以开始一个正常的关闭。尽管JVM可以通过这些标准的首选方法关闭,他仍然能够通过调用Runtime.halt或者杀死JVM的操作系统进程被强行关闭(比如发送SIGKILL)。

在正常的关闭中,JVM首先启动所有已注册的shutdown hook。关闭钩子是使用Runtime.addShutdownHook注册的尚未开始的线程。JVM并不能保证关闭钩子的开始顺序。如果关闭应用程序线程时,他仍然在运行,他们接下来将于关闭进程并发执行。当所有关闭钩子结束的时候,如果runFinalizersOnExittrueJVM可以选择运行finalizer之后停止。JVM不会尝试停止或中断任何关闭时仍然在运行中的应用程序线程,他们在JVM最终终止时被强制退出。如果关闭钩子或finalizer没有完成,那么正常的关闭进程挂起并且JVM必须强行关闭。在强行关闭中,JVM不需要完成除了关闭JVM以外的任何事情,不会运行关闭钩子。

关闭钩子应该是线程安全的:他们在访问共享数据时必须使用同步,并应该小心地避免死锁。他们不仅需要假设应用程序的状态(比如其他服务是否已经关闭或者正常的线程是否已经完成任务)或者关于JVM为何关闭,因此在代码过程中必须格外小心。最后,在用户可能希望JVM快速终止的情况下,他们必须尽快退出,他们的存在会延迟JVM的终止。

public void start(){

  Runtime.getRuntime().addShutdownHook(new Thread(){

    public void run(){

      try{

        LogService.this.stop();

      }catch(InterruptedException ignored){

      }

    }

  });

}


因为关闭钩子全部都是并发执行的,关闭日志文件可能引起其他需要使用日志服务的关闭钩子的问题。因此关闭钩子不应该依赖于可能被应用程序或其他关闭钩子关闭的服务。实现它的一种方式是对所有服务使用唯一关闭钩子,让他调用一系列关闭行为,而不是每个服务使用一个。这确保了关闭的动作在单线程中顺序发生,因此避免了竞态条件的出现或关闭动作之间的死锁。

有时需要创建一个线程执行一些辅助工作,但是你不希望这个线程的存在阻碍JVM的关闭,此时可以将其设置为守护线程(daemon thread)。

线程分为两种:普通线程和守护线程。JVM启动的时创建所有的线程,除了主线程以外其他的都是守护线程(比如垃圾回收器和其他类似线程)。当一个新的线程创建时,新线程继承了创建他的线程的后台状态,所以默认情况下,任何主线程创建的线程都是普通线程。

普通线程和守护线程之间的差别仅仅在于退出时会发生什么。当JVM停止时,所有仍然存在的守护线程会被抛弃——不会执行finally块也不会释放栈——JVM直接退出。

在任何时候,几乎没有哪些活动的处理可以在不进行清理的情况下被安全地抛弃。特别是IO操作的任务运行在守护线程中是很危险的。

为了释放对象,垃圾回收器对那些具有特殊finalize方法的对象会进行特殊对待:在回收器获得他们后,finalize被调用,这样就能保证持久化的资源可以被释放。因为finalizer可以运行在一个JVM管理的线程中,任何finalizer访问的状态都会被多个线程访问,因此必须被同步。finalizer运行时不提供任何保证,并且拥有复杂的finalizer会带来对象巨大的性能开销。正确的书写finalizer也十分困难。在大多数情况下,使用finally块和显示close方法的结合来管理资源会比使用finalizer起到更好的作用。

避免使用finalizer

public class Threadlock{

  ExecutorService exec=Executors.newSingleThreadExecutor();

  public class RenderPageTask implements Callable<String>{

    public String call() throws Exception{

      Future<String> header,footer;

      header=exec.submit(new LoadFileTask(“header.html”));

      footer=exec.submit(new LoadFileTask(“footer.html”));

      String page=renderBody();

      return header.get()+page+footer.get();

    }

  }

}

上面的例子出现死锁——任务等待子任务的结果。

关于线程池的大小,对于计算密集型的任务,一个有N个处理器的系统通常使用一个N+1个线程的线程池来获得最优的利用率。(计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因暂停,刚好有一个额外的线程,可以确保在这种情况下CPU周期不会中断工作)。对于包含了IO和其他阻塞操作的任务,不是所有的线程都会在所有的时间呗调度,所以需要一个更大的池。为了正确的设置线程池的长度,你必须估算出任务花在等待时间与用来计算的时间的比率,这个估算不必十分精确,而且可以通过一些监控工具获得。

为保持处理器达到期望的使用率,最优的池的大小等于

线程池的大小=CPU的数量*目标CPU的使用率(大于等于0小于等于1*(1+等待时间/计算时间)

int N_CPUS=Runtime.getRuntime().availableProcessors();

CPU周期并不是唯一你需要考虑的因素,其他可以约束资源池大小的资源包括:内存、文件句柄、套接字句柄和数据库连接等。

最常用的ThreadPoolExecutor的构造函数。

pulbic ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,

  long keepAliveTime,TimeUnit unit,

  BolckingQueue<Runnable> workQueue,

  ThreadFactory threadFactory

  RejectedExecutionHandler handler)
{

}

核心池大小、最大池大小和存活时间共同管理线程的创建与销毁。核心池大小是目标的大小。线程池的实现视图维护池的大小。即使没有任务执行,池的大小也等于核心池打大小,并且直到工作队列充满前,池都不会创建更多的线程。最大池的大小是可同时活动的线程数的上限。如果一个线程已经闲置的时间超过了存活时间,他将成为一个被回收的候选者,如果当前的池的大小超过了核心池的大小,线程池会终止他。通过调节核心大小和存活时间,可以促进线程池归还空闲线程占有的资源,让这些资源可以用于更有用的工作。

有时会把核心池的大小设置为零,认为这样工作者线程最终会销毁,因此不会妨碍JVM的退出。但是,这会在没有使用SynchronousQueue作为工作队列的线程池中引起一些看似奇怪的行为。如果池已经具有与核心数量相同的线程,ThreadPoolExecutor只会在工作队列已满的情况下创建新的线程。所以任务通过工作队列提交给核心长度为零的线程池,那么不等到队列填满,这些任务就不会开始工作。这通常不是期望发生的事情。在Java6中,通过allowCoreThreadTimeOut可以允许所有的线程响应超时。如果你有一个有限的线程池和一个有限的工作队列,同时又要所有的线程在没有任务的情况下销毁,可以设置非零的核心大小来激活这个特性。

即使通常平均请求率都很稳定,也难免会突然激增。尽管队列有助于缓和瞬时的任务激增,但是如果任务持续快速地到来,你最终还是必须要遏制住请求达到率,以避免耗尽内存。即使没有耗尽内存,响应时间也会随着任务队列的增长而逐渐地变糟。

ThreadPoolExecutor允许你提供一个BlockingQueue来持有等待执行的任务。任务排队有3种基本方法:无限队列、有限队列和同步移交(synchronous handoff)。

一个稳妥的策略是使用有限队列,比如ArrayBlockingQueue或者有限的LinkBlockingQueue以及PriorityBlockingQueue。有界队列有助于避免资源耗尽的情况发生,但是他也引入了当队列满时如何处理的问题。

对于庞大或者无线的池,也可以使用SynchronousQueue,完全绕开队列,将任务直接从生产者移交给消费者。为了把一个元素放入到SynchronousQueue中,必须有另一个线程正在等待接收移交的任务。如果没有这样一个线程,只要当前池的大小还小于最大值,ThreadPoolExecutor就会创建一个新的线程,否则根据饱和策略,任务会被拒绝。只有当池是无线的或者可以接受任务被拒绝,SynchronousQueue才是一个有实际价值的选择。newCachedThreadPool工厂就使用了SynchronousQueue

使用LinkedBlockingQueueArrayBlockingQueue会造成任务以他们到达的顺序开始执行,如果进一步控制顺序可以使用PriorityBlockingQueue,自然顺序或者Comparator都可以定义优先级。

只有当任务彼此独立时才能使有限线程池或者有限工作队列的使用是合理的。倘若任务之间相互依赖,有限的线程池或队列就可能引起线程饥饿死锁。

当一个有限的队列充满后,饱和策略开始起作用。ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改(如果任务提交到一个已经关闭的Executor时,也会用到饱和策略)。JDK提供了几种不同的RejectedExecutionHandler实现:AbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy

默认是abort策略会引起execute抛出未检查的RejectedExecutionException,调用者可以捕获该异常。

discard策略会放弃这个任务。

遗弃最旧的策略选择丢弃的任务时本应该接下来就执行的任务,该策略还会尝试去重新提交新任务。如果工作队列是优先级队列,那么该策略会丢弃的刚好是优先级最高的元素,所以一般该策略不会和优先级队列混用。

caller-runs策略既不会丢弃哪个任务也不会抛出任何异常。他会把一些任务推回到调用者那里,以此减缓新任务流。他不会在池线程中执行新提交的任务,但是会在一个调用了execute的线程中执行。

假设WebServer使用有限队列和调用者运行策略。当所有的池线程都被占用,而且工作队列已充满后,下一个任务会在主线程中执行。主线程调用execute执行这个任务。因为这将会花费一些时间,所以主线程在一段时间内不能提交任何任务。同时这也给了工作者线程时间来追赶进度。这期间主线程也不会调用accept,所以外来的请求不会出现在应用程序中,而会在TCP层的队列中等候。如果持续高负载的话,最终会由TCP层判断他的连接请求队列是否已经排满,如果已满就开始丢弃请求任务。当服务器过载时,他的负荷会逐渐地外移——从池线程到工作队列到应用程序再到TCP层,最终到用户——这使得服务器在高负载下可以平缓地劣化(graceful degradation)。

public class BoundedExecutor{

  private final Executor exec;

  private final Semaphore semaphore;

  public BoundedExecutor(Executor exec,int bound){

    this.exec=exec;

    this.semaphore=new Semaphore(bound);

  }

  public void submitTask(final Runnable command)throws InterruptedException{

    semaphore.acquitre();

    try{

      exec.execute(new Runnable(){

        public void run(){

          try{

            command.run();

          }finally{

            semaphore.release();

          }

        }

      });

    }catch(RejectedExcutionException e){

      semaphore.release();

    }

  }

}


使用信号量来遏制任务的提交

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值