多线程——线程安全及实现机制

多线程——线程安全及实现机制

一、线程安全概念

讨论线程安全,需要以多个线程之间存在共享数据访问为前提。因为如果根本不存在多线程,又或者一段代码根本不会与其他线程共享数据,那么从线程安全的角度来看,程序是串行执行还是多线程并发执行对于它来说是没有什么区别的

线程不安全是指在一项工作进行期间,会被不断地中断和切换,对象的属性(数据)可能会在中断期间被修改和变脏,会造成程序运行错乱,甚至会造成重大经济损失,产生重大生产事故。所以我们程序员必须保证程序如何在高速地计算机中正确无误的运行

关于线程安全,《Java并发编程实战》作者做过一个比较恰当的定义:“当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,调用方不需要进行任何其他的协调工作,调用这个对象的行为都能够获得正确的结果,那就称这个对象是线程安全的。”

上面这种定义要求线程安全的代码应该具备一个共同特征:代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无需关心多线程下的调用问题,更无需自己实现任何措施来保证多线程环境下的正确调用

二、Java 语言中的线程安全

上面我们已经有了线程安全的定义,那么在 Java 语言中,线程安全具体是如何实现的?有哪些操作是线程安全的?按照线程安全的 “安全程度” 由强至弱来排序,Java 语言中各种操作共享的数据分为以下几类:不可变、绝对线程安全、相对线程安全、线程兼容

1、不可变

在 Java 语言中,不可变对象一定是线程安全的,无论是方法实现还是方法的调用者,都不需要再进行任何线程安全的保障措施,只要一个不可变对象被正确的构建出来,那其外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态。不可变带来的安全性是最直接、纯粹的

Java 语言中,如果多线程共享的数据是一个基本的数据类型,那么只要在定义的时候使用 final 关键字修饰它,就可以保证它是不可变的

2、绝对线程安全

绝对线程安全的定义是很严格的,一个类要达到 “不管运行时环境如何,调用者都不需要任何额外的同步措施”,可能付出的代价是非常高昂的,甚至无法实现

在 Java 语言中标注自己是线程安全的类,也并不是绝对的线程安全。比如 Vector 是一个线程安全的容器,大家应该不会有异议,因为它的add()、get() 和 size() 等方法都是被 synchronized 修饰的,尽管效率不高,但是保证了原子性、可见性和有序性;但是调用它的话仍然需要合理的调用,否则还是会造成数据不一致

3、相对线程安全(重要)

相对线程安全就是我们通常意义上所讲的线程安全,他需要保证这个对象单次的操作是线程安全的,在单次调用中间就算被打断,别的线程的执行也并不能对这个线程操作的数据安全产生影响,我们再调用的时候不需要进行额外的保障措施

大部分声称线程安全的类都属于这种类型,例如 Vector、HashTable等

4、线程兼容

线程兼容是指对象本身并不是线程安全的,但是可以通过再调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用;我们平常说的一个类不是线程安全的,通常就是指这种情况

我们要做的工作就是在调用这些本身并不是线程安全的类的时候,通过锁等手段对其包装并暴露给外部调用,使外部调用的时候是线程安全的

三、线程安全的实现机制

具体的线程安全的实现是由代码的编写来保证的,但是线程安全的代码的实现都是基于 JVM 提供的同步和锁机制,所以下面我们来探索以下 JVM 如何实现同步与锁机制

1、互斥同步(Synchronized、Reentrantlock)

互斥同步是一种最常见也是最主要的并发正确性保障手段,互斥同步也称为阻塞同步

同步是指在多个线程并发访问数据时,保证共享数据在同一时刻只被一条线程使用;而互斥是实现同步的一种手段,临界区、互斥量和信号量都是常见的互斥实现方法

在 Java 语言中,最基本的互斥同步手段就是 synchronized 关键字,这是一种块结构的同步语法。 synchronized 关键字经过 Javac 编译之后,会在同步块的前后分成 monitorenter 和 monitorexit 这两个字节码指令,这两个字节码指令都需要一个 reference 类型的参数来指明要锁定和解锁的对象,简单来说就是 synchronized 作用于什么对象,分为如下三种情况:

  • 作用于同步代码块:synchronized 明确指定的对象参数,则以此对象引用作为 reference ,如果未指定就会锁定执行该代码块的 this 对象
  • 作用于实例方法:synchronized 修饰普通方法,则锁定调用此方法的实例对象
  • 作用于类方法:synchronized 修饰 static 静态方法,锁定当前类的 Class 对象

此处只是概述,后续我们还会详细讲解 Synchronized 关键字

由于 synchronized 的局限性,Java 类库中提供了 java.util.concurrent(J.U.C) 包,其中的 Lock 接口是另一种全新的互斥同步手段,基于 Lock 接口,用户能够以非块结构来实现互斥同步,摆脱语言的束缚,为以后的各种锁提供了更广阔的空间

ReentrantLock 是 Lock 接口最常见的一种实现,它和 Synchronized 一样是可重入的。不过 ReentrantLock 与 Synchronized 相比增加了一些高级功能:

  • 等待可中断:当持有锁的线程长期不释放锁,等待线程可以放弃等待,改为处理其他事情
  • 支持公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁,而不是随机抢夺; synchronized 不支持公平锁,ReentrantLock 默认不公平,但是可以设置成公平锁
  • 锁绑定多个条件:ReentrantLock 可以绑定多个 Condition 对象

此处只是概述,后续我们还会详细讲解 Synchronized 和 ReentrantLock 的异同

2、非阻塞同步(CAS、原子类)

互斥同步面临的主要问题是进行线程阻塞和线程唤醒所带来的性能开销,因此这种同步也被称为阻塞同步

从解决问题的方式上看,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(加锁),那就肯定会出现问题,无论共享的数据是否真的会出现竞争,它都会进行加锁(其实现代 JVM 已经优化掉很多不必要的加锁),这将会导致用户态到核心态转换、维护锁计数器和检查是否有阻塞的线程需要唤醒等开销

随着硬件指令集的发展,我们有了另外一种选择:基于冲突检测的乐观并发策略,通俗的来说就是不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数据的确发生了争用,那再进行其他的补救措施,最常用的补救措施就是不断地重试,直到出现没有竞争的共享数据为止。这种乐观并发策略不需要把线程阻塞挂起,因此这种同步操作被称为非阻塞同步

这种乐观并发策略的核心在于必须要求操作和冲突检测这两个步骤具备原子性,这时只能使用硬件级别的指令集来保证原子性,其中最核心的指令就是 Compare-and-Swap(简称 CAS)

CAS 指令需要有三个操作数,分别是内存地址(变量的内存地址,用 V 表示)、旧的预期值(用 A 表示)和准备设置的新值(用 B 表示)。CAS 指令执行时,当且仅当 V 符合 A 时,处理器才会用 B 更新 V 的值,否则就不会执行更新操作(比如另一个线程修改了 V 的值,此条 CAS 操作就不会成功,会继续执行下一次 CAS 操作),上述的处理过程是一个原子操作,由硬件保证其执行期间不会被其他线程中断

在 JDK 5 之后,Java 才开始使用 CAS 操作,该操作由sun.misc.Unsafe 类的compareAndSwapInt()、compareAndSwapLong()等几个方法包装提供,并没有直接开放给我们使用,但是 J.U.C 包中的整数原子类(AtomicInteger)的方法都是使用了 Unsafe 类的 CAS 操作来实现,此类可以用来实现一个整数的安全自增

实现一个整数的安全自增(限制订单数)可以通过加锁,使用同步代码块来完成,但是效率很低;此时使用 CAS 的AtomicInteger 来完成效率就会比较高了

不过 CAS 操作有一个逻辑漏洞——ABA 问题,解决办法是使用添加版本号来保证 CAS 的正确性

3、无同步方案(ThreadLocal)

听起来是不是很不可思议,我们要保证并发情况下的线程安全,怎么能不使用同步呢?其实要保证同步,并非一定要进行阻塞或非阻塞同步,同步和线程安全之间并没有必然的联系

同步只是保障存在共享数据争用时正确性的手段,如果能够让一个方法本来就不必涉及共享数据,那它自然就不需要任何同步措施去保障其安全性,因此有一些代码天生就是线程安全的,最常用的就是——ThreadLocal

线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据是否能保证在同一个线程中执行;如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样无需同步也能保证线程之间不会出现数据争用问题

符合这种特点的应用并不少见,大部分使用消费队列(如“生产者—消费者” 模式)都会将产品的消费过程限制在一个线程这消费完,其中最重要的一种应用实例就是在 Web 交互模型中的 “ 一个请求对应一个服务器线程 ”的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都是以使用本地线程存储来解决线程安全问题

这里重点不是 ThreadLocal,与 Synchronized 和 ReentrantLock 一样,后面会进行专门详细的讲解

四、总结

本篇文章中我们详细了解了什么是线程安全、在 Java 语言中是怎样定义线程安全、以及有哪些具体的实现机制

后面我们将会对实现机制的不同的具体实现进行更细节的探索

参考:《深入理解Java虚拟机》

关联文章:
多线程—Java内存模型与线程

多线程——Volatile 关键字详解

猜你喜欢

转载自blog.csdn.net/qq_42583242/article/details/108189498
今日推荐