Java中的线程安全

Java中线程安全的实现方式

Posted by Booogu on March 4, 2021
1533 字 5 分钟

线程安全的较为严谨的定义

线程安全是指,当多个线程同时访问一个对象时,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外同步,或者在调用方继续宁任何其他的协调操作,调用这个对象的行为都可以得到正确的结果,那么就称这个对象是线程安全的。

Java中的线程安全级别/定义

共分为五类:

  • 不可变:final关键字,比如String对象的值不可变
  • 绝对线程安全:成本太高,一般不需要这个高级别
  • 相对线程安全:通俗意义上的Java线程安全
  • 线程兼容:通俗意义上的Java线程不安全(需要同步操作)
  • 线程对立:无论如何无法实现线程安全

线程安全的实现方式:

  • 1、互斥同步:互斥是手段,同步时目的,因果关系,互斥的实现(临界区、互斥量、信号量等),基于悲观锁,对应synchronized关键字

目前synchronized做了优化,比如自旋等待。

ReentrantLock与synchronized对比

  • Reentrantlock是个纯超集,比如多了特性(等待可中断,本来是正在等待的线程必须等待,它允许放弃等待而处理其他事,对于处理执行时间非常长的同步块有优化作用;公平锁:按先后取得锁,但会导致性能急剧下降;锁绑定多个条件,默认synchronized只能有一个条件)
  • JDK1.5之前,ReentrantLock远远优于syn,因为syn优化不够,1.6之后两者基本持平
  • 两者都可时,仍然推荐用synchronized,因为:
    • synchronized在Java语法层面实现的同步,足够清晰简单
    • lock需要手动释放,finally释放,容易出错,而synchronized由JVM保证,即便有异常也能释放锁,更安全
    • 长远来看,JVM对Synchronized进行优化更容易,因为可以在对象头中记录锁相关信息,而使用lock则么有这个能力,jvm很难得知哪些锁对象由具体线程持有
  • 2、非阻塞同步:需要硬件指令集支持(CAS),因为不需要再在语言层面进行互斥,而是使用操作系统原语,是一种乐观锁,无争抢则直接操作,若由争抢,则补偿(一般是不断重试),而不需要无脑挂起,一般被称为无锁编程(Lock-Free)

为什么要求”硬件指令集支持“? 因为必须要求操作和冲突检测这两个步骤具有原子性,而必须使用硬件来实现,比如:

  • 测试并设置(Test-and-Set)
  • 获取并增加
  • 交换
  • 比较并交换 CAS
  • 加载链接、条件存储

CAS:需要有3个操作数:内存位置(变量的值)、旧的预期值、准备设置的新值,只有当变量的值符合旧的预期值时(认为没有被争抢更新),才会更新新值,存在ABA问题,但大部分情况下不会影响程序并发的正确性

  • 3、无同步方案:同步与线程安全没有必然联系,同步只是保障存在共享数据争用时正确性的手段,如果一个方法本来就不涉及共享数据,那么自然就不需要任何同步措施保证其正确性,这样的代码天生线程安全,一般具有以下特点:
    • 可重入代码:是线程安全代码的一个真子集,其特征:不依赖全局变量、存储在堆上的数据和公共的系统资源,用到的状态量都由参数中传入,不调用非可重入代码等。判断条件:如果一个方法的返回值是可预测的,只要输出了相同的数据,一定有相同的结果,就可以判定为符合可重入代码要求
    • 线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果可以保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样无须同步也能保证线程之间不出现数据争用问题。

一个经典的线程本地存储案例: Web交互模型中”一个请求对应一个服务器线程“,的处理方式,使得很多Web服务器端应用都可以使用线程本地存储来解决线程安全问题(使用一个本地线程变量ThreadLocal)