深入浅出带你读懂线程安全性(持续更新)

基础知识

如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:

  • 在访问状态变量时使用同步
  • 不在线程之间共享该变量
  • 将状态变量修改为不可变的变量

线程安全性的定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么这个类是线程安全的。

竞态条件:当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。换句话说,就是正确的结果取决于运气。最常见的竞态条件类型就是“先检查后执行”操作,即通过一个可能失效的观测结果来决定下一步的动作。

大多数竞态条件的本质——基于一种可能失效的观察结果来做出判断或者执行某个计算。这种竞态条件称为“先检查后执行”:首先观察到某个条件为真(例如文件X不存在),然后根据这个观察结果采取相应的动作(创建文件X),但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这期间创建了文件X),从而导致各种问题(未预期的异常、数据被覆盖、文件被破坏)。

Java提供了一种内置的锁机制来支持原子性,以避免竞态条件的发生:同步代码块。同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。

重入

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将会记下锁的持有者,并且将获取的计数器置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程突出同步代码块时,计数器会相应地递减。当计数值为0是,这个锁将被释放。

对象的共享

我们已经知道了同步代码块和同步方法可以确保以原子的方式执行操作,但是一种常见的误解是,认为synchronized只能用于实现原子性或者确定“临界区”。同步还有另一个重要的方面:内存可见性。我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。如果没有同步,那么这种情况就无法实现。

Java语言提供了了一种稍弱的同步机制,即voltile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为voltile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。voltile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取voltile类型的变量总是返回最新写入的值。

我们并不建议过度依赖voltile变量提供的可见性。如果在代码中依赖voltile变量来控制状态的可见性,通常比使用锁的代码更脆弱,也更难以理解。

voltile变量的正确使用方法包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生。

voltile的语义不足以确保递增操作(count++)的原子性,除非你能确保只有一个线程对变量执行写操作。(原子变量提供了“读-改-写”的原子操作,并且常常用作一种“更好的voltile变量”。)

线程封闭

当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭,它是实现线程安全性的最简单方式之一。当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。

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

Java语言及其核心库提供了一些机制来帮助维持线程封闭性,例如局部变量和ThreadLocal类。

栈封闭

栈封闭是线程封闭的一种特例,在栈封装中,只有通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。

ThreadLocal类

维护线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。

ThreadLocal对象通常用于防止对可变的单实例变量或全局变量进行共享。例如,在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个Connection对象。通过将JDBC的连接保存到ThreadLocal对象,每个线程都会拥有自己的连接。

不变性

满足同步需求的另一个方法是使用不可变对象。如果某个对象在创建后其状态就不能被修改,那么这个对象就称为不可变对象。

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

不可变性并不等于将对象中所有的域都声明为final类型,即使对象中所有的域都是final类型的,这个对象也仍然是可变的,因为在final类型的域中可以保存对可变对象的引用。

当满足以下条件是,对象才是不可变的:

  • 对象创建以后其状态就不能修改。
  • 对象的所有域都是final类型的。
  • 对象是正确创建的。(在对象的创建期间,this引用没有逸出)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值