java并发编程实战 对象共享

发布与逸出

发布publish 一个对象意思是指,使对象能够在当前作用域之外的代码中使用。例如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。在许多情况下,我们要确保对象及其内部状态不被发布。而在某些情况下,我们有需要发布某个对象,但如果在发布时要确保线程安全性,则可能需要同步。发布内部状态可能会破坏封装性,并使得程序难以维持不变性条件。例如在对象构造完成之前就发布该对象,就会破坏线程安全性。当某个不应该被发布的对象被发布时,这种情况就称为逸出(Escape)

发布对象最简单的方法是将对象的引用保存到一个公有的静态变量中,以便任何类和线程都能看见对象

当发布某个对象时,可能会间接发布其他对象。如果将一个secret对象添加到集合,那么同样会发布这个对象,因为任何代码都可以遍历这个集合,并获得这个对象的引用。

当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。一般来说,如果一个已经发布的对象能够通过非私有的变量引用和方法调用到其他的对象,那么这些对象也都会被发布。

假定有一个类C,对于C来说 外部(Alien)方法是指行为并不完全由C来规定的方法,包括其他类中定义的方法以及类C中可以被改写的方法。当把一个对象传递给某个外部方法,就相当于发布了这个对象,你无法知道哪些代码会执行,也不知道外部方法中究竟会发布对象,还是保留对象的引用并随后由另一个线程使用。

安全对象的构造过程

如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法

只有构造函数返回时,this引用才从应该从线程中抛出。构造函数可以将this保存到某个地方,只要其他线程不会再构造函数完成之前使用它。

线程封闭

当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在县城内访问数据,就不需要同步数据。这种技术被称为线程封闭(Thread Confinement)

线程封闭技术常见的应用是JDBC的Connection对象。JDBC规范并不要求Connection对象必须是线程安全的。在典型的服务器应用程序中,线程从连接池获得一个Connection对象,并且用该对象来处理请求,使用完后再将对象返还给连接池。大多数请求都是单个线程采用同步的方式来处理,并且在Connection对象返回之前,连接池不会再将它分配给其他线程,因此,这种连接管理模式在处理请求时,将Connection对象封闭在线程中。

在Java语言中并没有强制规定某个变量必须由锁来保护,同样在Java语言中也无法强制将对象封闭在某个线程中。线程封闭是在程序设计中的一个考虑因素,必须在程序中实现。Java语言及其核心库提供了一些机制来帮助维持线程封闭性,例如局部变量和ThreadLocal类,但即便如此,程序员仍然需要负责确保封闭在线程中的对象不会从线程中逸出。

ad-hoc线程封闭

ad-hoc线程封闭式指,维护线程封闭的职责完全由程序来承担。ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,例如修饰符或局部变量,能将对象封闭到目标线程上。

当决定用线程封闭技术时,同行是因为将某个特定的子系统实现成为一个单线程程序子系统。在某些情况下,单线程子系统提供的简便性要胜过ad-hoc线程封闭技术的脆弱性。

在volatile变量上存在一种特殊的线程封闭。只要您能确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在这些共享的volatile变量上执行读取修改写入操作。在这种情况下,相当于修改操作封闭在单个线程中以防止发生竞态条件,并且volatile变量可见性还确保了其他线程能看到最新值。

由于ad-hoc线程封闭技术的脆弱性,因此在程序中尽量少用它,在可能的情况下,应该使用更强的线程封闭技术(比如栈封闭或Threadlocal类)

栈封闭

栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。正如封装能使得代码更容易维持不变性条件那样,同步变量也能使对象更易于封闭在线程中。局部变量的固有属性之一就是封闭在执行线程中。其他线程无法访问这个栈(栈线程私有,堆线程共享)比ad-hoc线程封闭更容易维护,也更加强壮。

不变性

如果某个对象在被创建后其状态就不能被修改,那么这个的对象就称为不可变对象。线程安全性不是可变对象的固有属性之一,它们的不变性条件是由构造函数创建的,只要他们的状态不会改变,那么这些不变性条件就能得以维持。

不变的对象一定是线程安全的

安全发布

不可变对象与初始化安全性

由于不可变对象是一种非常重要的对象,因此java内存模型为不可变对象共享提供了一种特殊的初始化安全性保证。我们已经知道,即使某个对象的引用对于其他线程是可见的,也并不意味着对象状态对于使用该对象的线程来说一定是可见的。为了保证对象状态一致,就必须使用同步。

另一方面,即使在发布不可变对象的引用时没有使用同步,也仍然可以安全访问该对象。为了位置这种初始化安全的保证,必须满足不可变性的所有需求:状态不可修改,所有域都是final类型,以及正确的构造过程。

如何线程都可以再不需要额外同步的情况下安全访问不可变对象,即使在发布这些对象时没有使用同步。然而如果final类型的域指向的是可变对象,那么在访问这些域锁指向的对象状态时仍需要同步。

常用的安全发布模式

要安全发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。

  • 在静态初始化函数中初始化一个对象引用。
  • 将对象引用保存到volatile类型的域或者atomicreferance对象中。
  • 将对象的引用保存到某个正确构造对象的final类型域中。将对象的引用保存在一个由锁保护的域中

事实不可变对象

如果对象在发布后不会被修改,那么对于其他在没有额外同步的情况下安全地访问这些对象的线程来说,安全发布是足够的。所有的安全发布机制都能确保,当对象的引用对所有访问该对象的线程可见时,对象发布时的状态对于所有线程也将是可见的,并且如果对象状态不会再改变,那么就足以确保任何访问都是安全的。
如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象(Effectively Immutable Object)”。这些对象不需要满足3.4节中提出的不可变性的严格定义。在这些对象发布后,程序只需将它们视为不可变对象即可。通过使用事实不可变对象,不仅可以简化开发过程,而且还能由于减少了同步而提高性能。
在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

可变对象

果对象在构造后可以修改,那么安全发布只能确保“发布当时”状态的可见性。对于可变对象,不仅在发布对象时需要使用同步,而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。要安全地共享可变对象,这些对象就必须被安全地发布,并且必须是线程安全的或者由某个锁保护起来。
对象的发布需求取决于它的可变性:

  • 不可变对象可以通过任意机制来发布。
  • 事实不可变对象必须通过安全方式来发布。
  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。

安全的共享对象

当获得对象的一个引用时,你需要知道在这个引用上可以执行哪些操作。在使用它之前是否需要获得一个锁?是否可以修改它的状态,或者只能读取它?许多并发错误都是由于没有理解共享对象的这些“既定规则”而导致的。当发布一个对象时,必须明确地说明对象的访问方式。
在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

对象的组合

我们并不希望每一次内存访问都进行分析以确保程序是线程安全的,而是希望将一些现有的线程安全组件组合为更大规模的组件或程序。一些组合模式,这些模式能够使一个类更容易称为线程安全的,并且维护这些类时不会无意破坏类的安全性保证。

设计线程安全的类

在线程安全的程序中,虽然可以将程序所有状态都保存在公有静态域中,但与那些将状态封装起来的程序相比,这些程序的线程安全更难以得到验证,并且在修改时也更难以时钟确保其线程安全性。通过使用封装技术,可以使得在部队整个程序进行分析的情况下就可以判断一个类是否是线程安全的。

  • 找出构成对象状态的所有变量
  • 找出约束状态的不变性条件
  • 建立对象状态的并发访问策略

要分析对象的状态首先从对象域开始。如果对象中所有域都是进本类型变量,那么这些域将构成对象的全部状态。

GuardedBy

Java并发编程中,用到了一些专门为并发编程准备的 Annotation。主要包括三类:

<dependency>  
    <groupId>net.jcip</groupId>  
    <artifactId>jcip-annotations</artifactId>  
    <version>1.0</version>  
</dependency> 

最常见的@GuardedBy(lock)记录的是:线程只有持有一个特定的锁后,才能访问某个域或者方法。lock可能值如下:(以下注解都是标识)

  • @GuardedBy(“this”):对象中的内部锁
  • @GuardedBy(“fieldName”):与fieldName引用的对象相关联的锁,可能是显式锁、隐式锁
  • @GuardedBy(“ClassName.fieldName”):与@GuardedBy(“fieldName”)类似,指的是静态域
  • @GuardedBy(“methodName()”):锁对象指的是方法的返回值
  • @GuardedBy(“ClassName.class”):是指ClassName类的对象

@threadsafe表示这个类是线程安全的,具体是否安全取决于实现

  • @NoThreadSafe 被修饰过得类即为线程不安全的,线程安全的类也可以被此注解修饰
  • @ThreadSafe 被修饰过的类都即为线程安全的
  • @Immutable 被修饰的类都是不可变的,其中也包含了线程安全的意思

收集同步需求

要确保类的线程安全性,就需要确保它的不变性条件不会在并发访问的情况下被破坏,这就需要对其状态进行推断。对象与变量都有一个状态空间,即所有可能的取值。状态空间越小,就越容易判断线程的状态。final类型的域使用得越多,就越能简化对象可能状态的分析过程。(在极端的情况中,不可变对象只有唯一的状态。)
在许多类中都定义了一些不可变条件,用于判断状态是有效的还是无效的。Counter中的value域是long类型的变量,其状态空间为从Long.MIN_VALUE到Long.MAX_VALUE,但Counter中value在取值范围上存在着一个限制,即不能是负值。

同样,在操作中还会包含一些后验条件来判断状态迁移是否是有效的。如果Counter的当前状态为17,那么下一个有效状态只能是18。当下一个状态需要依赖当前状态时,这个操作就必须是一个复合操作。并非所有的操作都会在状态转换上施加限制,例如,当更新一个保存当前温度的变量时,该变量之前的状态并不会影响计算结果。
由于不变性条件以及后验条件在状态及状态转换上施加了各种约束,因此就需要额外的同步与封装。如果某些状态是无效的,那么必须对底层的状态变量进行封装,否则客户代码可能会使对象处于无效状态。如果在某个操作中存在无效的状态转换,那么该操作必须是原子的。另外,如果在类中没有施加这种约束,那么就可以放宽封装性或序列化等需求,以便获得更高的灵活性或性能。

依赖状态的操作

类的不变性条件与后验条件约束了在对象上有哪些状态和状态转换是有效的。在某些对象的方法中还包含一些基于状态的先验条件(Precondition)。例如,不能从空队列中移除一个元素,在删除元素前,队列必须处于“非空的”状态。如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖状态的操作。
在单线程程序中,如果某个操作无法满足先验条件,那么就只能失败。但在并发程序中,先验条件可能会由于其他线程执行的操作而变成真。在并发程序中要一直等到先验条件为真,然后再执行该操作。

状态所有权

如果以某个对象为根节点构造一张对象图,那么该对象的状态时对象图中所有包含的域的一个子集。

在定义哪些变量将构造成对象的状态时,只考虑对象拥有的数据。所有权(ownership)在java中并没有得到充分的体现,而是属于类设计中的一个要素。如果分配并填充了一个hashmap对象,那么就相当于创建了多个对象:HashMap对象,在HashMap中包含了多个对象,以及在Map.Entry中可能包含的内部对象。HashMap对象的逻辑状态包括所有的Map.Entry对象以及内部对象,即使这些对象都是一些独立的对象。

无论如何,垃圾回收机制使我们避免了如何处理所有权的问题。在C++中,把一个对象传递给某个方法时,必须认真考虑这种操作是否返回对象的所有权,是短期的所有权还是长期的所有权。在java中同样存在这些所有权模型,只不过垃圾回收器为我们减少了许多在引用共享方面常见的错误,因此降低了所有全处理上的开销。

许多情况下,所有权域封装性总是相互关联的:对象封装它拥有的状态,反之也成立,即对它封装的状态拥有所有权。状态变量的所有者将决定采用何种加锁协议来维持状态的完整性。所有权意味着控制权。然而如果发布了某个可变对象的引用,那么就不再拥有独占的控制器,最多是共享控制权。对于狗仔函数或者从方法中传递进来的对象,类通常并不拥有这些对象,除非这些方法是被专门设计为转移传递进来的对象的所有权(例如,容器封装的工厂方法)。

容器类通常表现出以以中国所有权分离的形式,其中容器类拥有其自身状态,而客户代码则拥有容器中各个对象的状态。Servlet框架中的一个servletContext就是其中一个实例。ServletContext为Servlet提供了类似于Map形式的对象容器服务,在ServletContext中可以通过名称注册setAttribute和获取getAttribute应用程序对象。由servlet容器实现的ServletContext对象必须是线程安全的,因为它肯定会被多个线程同时访问。当调用注册和获取时。Servlet不需要使用同步,但当使用保存在ServletContext中的对象时,则可能需要使用同步。

实例封闭

如果对象不是线程安全的,那么也可以通过多种技术使其在多线程程序中安全地使用。你可以确保该对象只能由单个线程访问(线程封闭),或者通过一个锁来保护对象的所有访问。

封装简化了线程安全类的实现过程,它提供了一种实例封闭机制(Instance Confinement)通常简称为“封闭”。当一个对象被封装到另一个对象中时,能够访问被封装对象的所有代码路径都是已知的。对象可以由整个程序访问的情况相比,更易于对代码进行分析。通过将封闭机制与合适的加锁策略结合起来,可以确保线程安全的方式来使用非现场安全的对象。

被封闭的对象一定不能超出它们既定的作用域。对象可以封闭在类的一个实例(例如作为类的一个私有成员)中,或者封闭在某个作用域内(局部变量),再或者封闭在线程内。

封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全就无需检查整个程序

监视器模式

从线程封闭及其逻辑推论可以得出监视器模。遵循java计算器模式的对象会把所有可变状态都封装起来,并由对象自己的内置锁来保护。

许多类都使用了java监视器模式,例如Vector和Hashtable。监听器模式的优势在于它的简单些。

线程安全性的委托

大多数对象都是组合对象。当从头开始构建一个类,或者将多个非线程安全的类组合为一个类时,Java监视器模式是非常有用的。但是,如果类中的各个组件都已经是线程安全的,会是什么情况呢?我们是否需要再增加一个额外的线程安全层?答案是“视情况而定”。在某些情况下,通过多个线程安全类组合而成的类是线程安全的

取消与关闭

任务和线程的启动很容易。在大多数时候,我们都会让他们运行直到结束。然而有时候我们希望提前结束任务线程,或许是因为用户取消了操作,或者应用程序需要被快速关闭。

要使任务和线程安全,快速,可靠地停止下来,并不是一件容易的事。java没有提供任何机制来安全的终止线程。但它提供了中断(Interruption),这是一种协作机制,能够使一个线程终止另一个线程的当前工作。

这种协作式的方法是必要的,我们很少希望某个任务,线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务式可以使用一种协作的方式:需要停止时,它们首先会清除当前正在执行的工作,然后再结束。这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行工作。

任务取消

如果外部代码能在某个正常完成之前置入完成状态,那么这个操作就称为可以取消的。

java中没有一种安全的抢占式方法来停止线程任务,因此也就没有安全的抢占式方式来停止任务。只有一些协作式机制,使请求取消和任务和代码都遵循一种协商好的协议。

在java中没有一种安全的抢占方式方法来停止线程,因此也就没有安全的抢占方法来停止任务。只有一些协作的机制,使请求取消的任务和代码都遵循一种协商好的协议。

其中一种协作机制能设置某个已请求取消标志,而任务定期查看该标志,如果设置了这个标志,那么任务提前结束。

中断

但任务有可能永远不会检查取消标志,因此永远不会结束。

每一个线程都有一个boolean类型的中断状态,当线程中断时,这个线程的中断状态将被设置为true。在Thread中包含了中断线程以及查询线程中断状态的方法。Interrupt方法能中断目标线程,而isInterrupted方法能返回目标程序的中断状态。静态的Interrupt方法将清除当前线程的中断状态,并返回它之前的值,这也是清除中断状态的唯一方法。

阻塞方法,例如,Thread.sleep,Object.wait()等都会检查线程何时中断,比企鹅在发现中断时提前返回。他们在相应中中断时执行的操作包括:清除中断状态,抛出InterruptedException,表示阻塞操作由于中断而提前结束。JVM并不能保证阻塞方法检测到中断的速度,但实际情况中响应速度还是非常快的。

当线程在非阻塞状态下中断时,它的中断状态将被设置,然后根据被取消的操作来检查中断状态以判断发生了中断。通过这样的方法,中断操作将 变得有粘性,如果不触发InterruptedException,那么中断状态将一直保持,直到明确清除中断状态。

调用interrupt并不意味着立即停止目标线程正在进行的工作,而是传递了请求中断的信息。

在使用静态的interrupted时应该小心,因为它会清除当前线程的中断状态。如果调用interrupted返回了true,除非你想屏蔽这个中断,否则必须对它进行处理

中断策略

正如任务重应该包含取消策略一样,线程中应该包含中断策略。中断策略规定线程如何解释某个中断请求。当发现中断请求时,应该做哪些工作,哪些工作单元对于中断来说是原子性操作,以及多块的速度来响应中断。

正如代码不应该对其执行的线程中断策略做出假设,执行取消操作的代码也不应该对线程的中断策略做出假设。线程应该只能由其所有者中断,所有者可以将线程的中断策略封装到何时的取消机制中,例如关闭方法。

java没有提供抢占式中断机制,而且还强迫开发人员必须处理InterruptedExecpthion。然而推迟中断请求的处理,开发人员能更灵活的中断策略,从而使应用程序在响应性和健壮性之间实现合理的平衡。

计时运行

许多问题永远也无法解决,而某些问题很快能得到的答案,也可能永远也得不到答案。在这些情况下,如果能够指定最多花10分钟搜索答案,或者枚举出10分钟内能找到的答案,那么将是非常有用的。

ExecutorService executor = Executors.newSingleThreadExecutor();
Future future = executor.submit(new Runnable() {
@Override
public void run() {
// 线程逻辑代码
}
});
try {
future.get(timeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
// 超时异常处理
} finally {
future.cancel(true);
executor.shutdown();
}

在上述代码中,使用ExecutorService创建一个线程池,通过submit方法提交一个Runnable任务并返回一个Future对象。然后使用future.get方法设置超时时间,如果任务在指定时间内未完成,则抛出TimeoutException异常,可以在catch块中进行超时异常处理。最后,使用future.cancel方法取消任务并关闭线程池。

处理不可中断的异常

在java库中,许多可阻塞的方法都是通过提前返回或者抛出InterruptedException来响应中断请求的,从而使开发人员更容易构建出能响应取消请求的任务。然而,并非所有的可阻塞方法或者阻塞机制都能够响应中断;如果一个线程由于执行同步的Socket I/O或者等待内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外没有任何作用。对于那些由于执行不可中断操作而被阻塞的线程可以使用类似于中断的手段来停止这些线程,但这要求我们必须知道线程阻塞的原因。

  • Java.io包中同步 SocketI/O。在服务器应用程序中,最常见的I/O阻塞形式就是对套接字进行读取和写入。虽然InputSream和OutputStream中的read和write等方法都不会响应中断,但通过关闭底层的套接字,可以使得由于执行read或write等方法而被阻塞的线程抛出一个SocketException
  • Java.io包中的同步I/O。当中断一个InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptException并关闭链路。当关闭一个InterruptibleChannel时,将导致所有在链路上操作的阻塞线程都抛出AsynchronousCloseException。大多数标准的Channel都实现了InterruptibleChannel
  • Selector的异步IO,如果一个线程在调用Selector.select方法时阻塞了,那么调用close或wakeup方法会使线程抛出ClosedSelectorException并提前返回
  • 获取某个锁,如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁,所以将不会理会中断请求。但是lock类中提供了lockInterruptibly方法,该方法允许在等待一个锁的同时仍能响应中断。

基于线程的服务

应用程序通常会创建有多个线程的服务,例如线程池,这些服务的生命周期通常比创建它们的生命周期更长。如果应用程序准备退出,那么这些服务所拥有的线程也需要结束。由于无法通过抢占的 方式来停止线程,因此它们需要自行结束。

应用程序通常会创建有多个线程的服务,例如线程池,并且这些服务的生命周期通常要比创建它们的方法的生命周期更长。如果应用程序准备退出,那么这些服务所拥有的线程也需要结束。由于无法通过抢占式的方式来停止线程,因此他们需要自行结束。

正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控。例如中断线程或修改线程的优先等级。在线程API中,并没有对线程所有权给出正式的定义:线程由thread对象表示,并且像其他对象一样可以被自由共享。然而,线程由一个相应的所有者,即创建该线程的类。因此线程池是其工作者线程的所有者,如果要中断这些线程,那么应该使用线程池。

与其他封装对象一样,线程的所有权是不可传递的:应用程序可以拥有服务,服务也可以拥有工作线程,但应用程序并不能拥有工作者线程,因此应用程序不能直接停止工作者线程。相反,服务应该提供生命周期方法来关闭它们自己所拥有的线程。这样当应用程序关闭该服务时,服务就可以关闭所有线程了。在ExecutorService中提供了Shutdown和ShutdownNow等方法,同样,在其他拥有线程的服务中也应该提供累死的关闭机制。

springboot优雅退出Spring Boot 优雅停机——Graceful Shutdown - 简书 (jianshu.com)

关闭ExecutorService

ExecutorService提供了俩种关闭方法:使用shutdown正常关闭,以及使用shutdownNow强制关闭,进行强制关闭时首先关闭当前正在执行的任务。然后返回所有尚未启动的任务清单。

俩种方式的差别在于各自的安全性和响应性:强行关闭的速度更快,但风险也更大,因为任务很可能在执行到一半的时候被结束;正常关闭虽然速度慢,但却更安全,因为ExecutorService会一直等到队列中的所任务都执行完成后才关闭。其他拥有线程的服务中也应该考虑提供累死的关闭方式以供选择。

简单的程序可以直接在main函数中启动和关闭全局的ExecutorService。而在复杂程序中,通常会将ExecutorService封装在某个更高级别的服务中,并且该服务能提供自己的生命周期方法

毒丸对象

另一种关闭生产者消费服务的方式就是使用毒丸(Poison Pill)对象,毒丸对象是指一个放在队列上的对象,其含义是:当得到这个对象时,立即停止。在先进先出的队列中,毒丸对象将确保消费者在关闭之前首先完成队列中所有工作,在提交毒丸对象之前提交的所有工作都会被处理。而生产者提交了毒丸对象后,将不会再提交毒丸对象之前提交的所有工作都会被处理,而生产者在提交了毒丸对象后,将不会在提交任何工作。

shutdown和shutdownNow

shutdown此方法允许线程继续执行已提交的任务,但不会接收新的任务。也就是说线程继续执行等待中的任务所有任务都执行完成位置。调用该方法后,线程池不会立即关闭,而是等待所有已提交的任务执行完成后才会关闭线程。

shutdownNow() 此方法会停止当前正在执行的任务,并尝试停止执行的任务。它会通过调用每个interrupt()方法中断线程,如果任务没有正确处理中断,则可能导致一些任务不被执行或者处于不一致状态。

jvm关闭

JVM既可以正常关闭也可以强行关闭。正常关闭的触发方式有很多种

包括调用了最后一个正常非守护进程时,或者当调用了Systeam.exit时,或者通过其他特定于平台的方法关闭时(比如发送了SIGINT信号键入Ctrl-c)。虽然可以通过这些标准方法来正常关闭,但也可以通过调用Runtime。或者在操作系统重杀死jvm进程来强行关闭JVM

关闭钩子

在正常关闭中jvm调用所有已注册的关闭钩子(Shutdown Hook)。关闭钩子是指通过runtimeaddShutdownHook注册的但尚未开始的线程。JVM并不能保证关闭钩子的调用顺序。在关闭应用程序线程时,如果有(守护或非守护)线程仍在运行,那么这些线程接下来于关闭进程并发执行。当所有的关闭钩子都执行结束时,如果runFinalizersOnExit为true,那么JVM将运行终结器,然后再停止。JVM并不会停止或中断任何在关闭时仍然运行的应用程序线程。当JVM最终结束时,这些线程将被强行结束。如果关闭钩子或终结器没有执行完成,那么正常关闭进程挂起,并且JVM必须被强行关闭。

关闭钩子应该是线程安全的:他们在访问共享数据时必须使用同步机制,并且小心地避免发生思索,与其他的代码要求相同。关闭钩子不应该对应用程序的状态或者JVM的关闭原因做出任何假设,因此在编写关闭钩子的代码时必须考虑周全。最后关闭钩子必须尽快推出,因为它们会延迟JVM的结束时间,而用户线程希望JVM能尽快终止。

关闭钩子可以用于实现服务或应用程序清理工作,例如删除临时文件

守护线程

有时候你希望创建一个线程来执行一些辅助工作,但又不希望这个线程阻碍JVM的关闭。在这种情况下就需要使用守护线程(Daemon thread)。

线程可以分为俩种,普通线程和守护线程。在JVM启动创建的线程中,除了主线程以外,其他的线程都是守护线程(例如垃圾收集器以及其他辅助执行工作的线程)。当创建一个新线程时,新线程将继承创建它的线程的守护状态,因此在默认情况下,主线程创建的所有线程都是普通线程

普通线程与守护线程之间的差异仅在于当前线程退出时发生的操作。当一个线程退出时JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作,当JVM停止时,所有仍然存在的守护线程都将抛弃,既不会执行finally代码块,也不会执行回卷栈,而JVM直接退出。

我们应尽可能少使用守护线程,很少有操作能够在不进行清理的情况下安全地抛弃,特别是,如果守护线程中执行可能包含I/O操作的任务,那么将是一种危险行为。守护线程最好用于执行内部任务,例如周期性的从内存缓存中移除逾期的数据

使用守护线程

使用很简单,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程
如何检查一个线程是守护线程还是用户线程:使用isDaemon()方法

终结器

当不需要内存资源,可以通过垃圾回收器来回收它们,但是对于一些资源,例如文件句柄或套接字句柄,当不再需要它们时,必须显式地交还给操作系统。为了实现这个功能垃圾回收器对那些定义了finalize反复噶的对象会进行特殊处理:在回收器释放它们后,调用它们的finalize方法,从而保证一些持久化的资源被释放。

在大多数情况下,通过使用finally代码块和显式的close方法,能够比使用终结器更好地管理资源。除非:当需要管理对象,并且该对象持有的资源时通过本地方法获得的。

Last modification:November 8, 2023
如果觉得我的文章对你有用,请随意赞赏