当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。可以通过原子性、一致性、不可变对象、线程安全的对象和加锁保护同时被多个线程访问的可变状态变量来解决线程安全的问题。
可见性在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读写操作的线程都必须持有同一把锁。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。volatile变量是一种比synchronized关键字更轻量级的同步机制。加锁机制即可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
发布逸出当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。即使发布对象的语句位于构造函数的最后一行也是如此。如果this引用在构造函数中逸出,那么这种现象就被认为是不正确构造。常见的逸出有,在构造函数中创建并启动一个线程、内部私有可变状态逸出等。
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过一下方式来安全地发布:
在静态初始化函数中初始化一个对象引用
将对象的引用保存到volatile类型的域或者AtomicReference对象中
将对象的引用保存到某个正确构造对象的final类型域中
将对象的引用保存到一个由锁保护的域中
对象的发布需求取决于它的可变性:
不可变对象可以通过任意机制来发布
事实不可变对象必须通过安全方式来发布
可变对象必须通过安全方式发布,并且必须是线程安全的或者由某个锁保护起来
千万不要在A线程中创建对象,在B线程中使用该对象。在对象初始化的时候,首先会去申请一个内存空间,然后给对象中的属性赋默认值(如:int类型的变量默认值为0等),再通过构造函数或者代码块对属性进行赋值,最后地址空间指向的对象才算是创建完成了(当然还有很多其他的步骤,这里只是简单说明一下)。这样很有可能出现B线程获取到的对象是不完整的,因为Java线程模型的和对象的可见性的原因。
线程中断调用Thread.interrupt()并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。
对中断操作的正确理解是:它并不是真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。(这些时刻也被称为取消点)。有些方法,例如:Object.wait()、Thread.sleep()和Thread.join()等,将严格地处理这种请求,当它们收到中断请求或者在开始执行时发现某个已被设置好的中断状态时,将抛出一个异常。
在使用静态的interrupted时应该小心,因为它会清除当前线程的中断状态。如果在调用interrupted时返回了true,那么除非你想屏蔽这个中断,否则必须对它进行处理—可以抛出InterruptedException,或者通过再次调用interrupt()来恢复中断状态。Future.cancel()方法可以取消线程。
通常,中断是实现取消的最合理方式。
未捕获的异常在运行时间较长的应用程序中,通常会为所有线程的未捕获异常指定同一个异常处理器(实现Thread.UncaughtExceptionHandler接口),并且该处理器至少会将异常信息记录到日志中。
如果你希望在任务由于发生异常和失败时获得通知,并且执行一些特定于任务的居处操作,那么可以将任务封装在能捕获异常的Runnable或Callable中,或者改写ThreadPoolExecutor.afterExecute()方法。
只有通过execute()提交的任务,才能将它抛出的异常交给未捕获异常处理器,而通过submit提交的任务的异常都被封装在Future.get()的ExecutionException中重新抛出。
关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程。JVM并不能保证关闭钩子的调用顺序。在关闭应用程序线程时,如果有(守护或非守护)线程仍然在运行,那么这些线程接下来将与关闭进程并发执行。当所有的关闭钩子都执行结束时,如果runFinalizersOnExit为true,那么JVM将运行终结器,然后再停止。