Java的线程安全实现 #
线程安全的定义 #
多个线程同时访问一个对象/方法时,如果不用考虑这些线程运行时环境下的调度和交替进行,也不需要进行额外的同步,或者调用方进行任何其他协调
调用该对象/方法的行为都能获得正确的结果,则该对象线程安全
线程安全的实现方法 #
互斥同步 #
- 同步指:保证并发访问时,共享资源同一时刻只能被一个线程访问
- 互斥是实现同步的一种手段。常见的互斥实现方式有:临界区、互斥量和信号量
- Java中的基本互斥手段为Synchronized关键字
- 除此之外还有java.util.concurrent.locks包下的Lock接口
Synchronized #
- 这是一种块结构的同步语法
- javac编译后,会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令。两个指令都需要指定一个refrence类型的参数指定需要锁定和解锁的对象
- 如果指定了一个对象,则以该对象的引用作为reference;如果没有明确指定,则根据修饰的方法类型(实例方法或类方法),来决定取代码所在对象实例还是取类型对应的Class对象作为锁
- JVM规范规定,执行monitorenter指令时,首先要尝试获取对象锁,如果没有被锁定或者当前线程已经持有,则锁计数器+1,执行monitorexit时,锁计数器-1;一旦计数器值为0,则锁被释放。
- 如果当前线程获取锁失败,则进入阻塞状态,直到锁被释放
注意点:
synchronized锁可重入
synchronized同步块执行完毕并释放锁前,无条件阻塞后面的线程进入,意味着无法像处理某些数据库中的锁一样,强制已获取锁的线程释放锁,也无法让等锁线程中断等待和超时退出
持有锁是重量级操作,由于Java的线程是内核线程实现,切换县城需要操作系统帮忙,涉及到内核态和用户态的转换,耗费很多处理器时间。虚拟机对synchronized进行了一些优化。优化后性能基本能和ReentrantLock持平
非阻塞同步 #
互斥同步属于悲观的并发策略,总是认为只要不做正确的同步措施就一定会出问题,无论数据实际上是否发生竞争,都会加锁。有着线程切换、维护锁计数器的开销
随着硬件指令集的发展,可以做到基于冲突检测的乐观并发策略。
基本思想是:先执行操作,如果没有其他线程竞争,则操作成功,如果有冲突,则做其他补偿措施,通常措施是不断重试,直到没有竞争操作成功。
这种同步操作成为非阻塞同步,代码可称为无锁代码
之所以必须依赖硬件指令集的发展,是要求操作和冲突检测的两个步骤具备原子性
如果以来互斥同步保证原子性则失去意义,只能依靠硬件实现
通过硬件来保证多次操作的行为只通过一条处理器指令就能完成。
Java中使用的指令为CAS,比较并交换
CAS指令 #
CAS:compare and swap
能在不使用锁的情况下,非阻塞地实现多线程安全
- 需要三个操作数:内存地址V,旧的预期值A,准备设置的新值B
- 执行时,当且仅当V符合A,才用B更新V,否则不执行更新。这是一个原子操作
- 该操作由sun.misc.Unsafe类的compareAndSwapInt()和compareAndSwapLong等几个方法包装提供,编译结果是一条平台相关的处理器CAS指令
- 设计上Unsafe类是不提供给用户程序调用的类,限制了只有启动类加载器加载的Class才能访问它,因此JDK9之前只有Java类库可以使用CAS
- J.U.C包中的整数原子类使用了CAS操作实现,如果用户程序需要使用,要么通过反射突破Unsafe的访问限制,要么通过Java类库API间接使用
注意点:
CAS无法覆盖互斥同步的所有使用场景,有一个逻辑漏洞(ABA问题):
- 如果V读取时是A,准备赋值时也是A,不能说明它的值没有被其他线程改过。比如被赋值为B又改回来,会误认为没有被改过。
- 为了解决这个问题,JUC包提供了带引用标记的AtomicStampedReference源自引用类,通过控制变量的版本来保证CAS的正确性,但是比较鸡肋,如果想解决ABA问题,可以使用传统互斥同步可能更高效