Java 线程 — synchronized、volatile、锁

时间:2022-09-28 22:31:27

线程同步基础

synchronized 和volatile是Java线程同步的基础。

synchronized

将临界区的内容上锁,同一时刻只有一个进程能访问该临界区代码

使用的是内置锁,锁一个时刻只能被一个线程持有,可以重入(表示一个处于synchronized代码中的线程可以进入另外一个使用synchronized的代码快,比如:方法A和方法B同时使用synchronized修饰,在方法A中调用了方法B,调用某个线程调用方法A的时候不会造成死锁,因为synchronized是可重入的锁,线程在进入方法A的时候获得了当前对象的锁,但是此时这个线程依然可以获得方法B的synchronized锁)

synchronized修饰不同对象时获得的锁:

  • 修饰普通方法:获得的锁是当前对象
  • 修饰静态方法:获得的锁是当前类class
  • 修饰代码块:取决于具体的锁对象

在Java的同步方法中synchronized是比较重量级的锁,而且不够灵活,jvm提供了更轻量的volatile,jdk提供了更灵活的Lock。

volatile

在多处理器的CPU架构下,因为每个处理器都有自己的缓存,线程访问变量的时候会读取缓存,多个线程读取的缓存不一样会导致每个线程得到的值不一样。使用该关键字的效果是:

  • 处理器将缓存写回到内存
  • 处理器将缓存写回到内存的时候会导致其他处理器的内存失效

作用

  • 保证可见性,Java内存模型(JMM)确保所有线程看到的这个变量的值是一致的
  • 只保证简单操作的原子性(保证变量简单赋值操作的原子性,如:temp = 1,不保证复杂操作的原子性,如:temp++)

内存语义(就是内存会做的操作)

  • 写:当写一个volatile的变量时,JMM把该线程对应的本地缓存中的共享变量刷新到主内存
  • 读:当读一个volatile变量时,JMM把该线程对应的本地缓存置为无效,线程接下来将从主存中读取共享变量

问题:上面提到的刷新操作是对这个本地内存刷新,还是只刷新volatile变量?

解答:是刷新整个本地缓存,包括其他共享变量

CAS

CAS:Compare And Switch,在Java中是通过调用C/C++写的本地方法完成的,C又调用了CPU的cmpxchg指令完成的。

一般来说有三个值:内存值V,期望值A,更新值B,如果内存值和期望值相等,则用更新值B替换内存值A,否则什么也不做

什么叫锁:锁其实就是维护一种状态,比如一个int状态值state,state变量对于所有线程可见,线程A将state改为1的时候(假设,当然根据具体的需要可以设为对应的值)表示上锁的状态,线程在试图修改state的时候,发现是1,说明其他线程已经改过,线程B则进入阻塞状态,当线程A释放锁的时候,也就是将state改为0的时候,唤醒线程B,线程B会重新试图获取锁,也就是修改state的值,如果state为0,那么修改成功,线程B获得锁

  • 偏向锁:为了让之前获得过该锁的线程更容易获得锁(获得锁的代价更低,不需要CAS加锁和解锁),因为Hotspot的作者研究发现:大多数情况下锁不仅不存在竞争,而且总是由同一线程多次获得

内存语义

  • 获取锁:当线程获取锁时,JMM会把线程对应的本地缓存中的共享变量刷新到主存
  • 释放锁:当线程释放锁时,JMM会把线程对应的本地缓存置为无效

对比volatile的和锁的内存语义,volatile的写——锁的获取,volatile的读——锁的释放

释放锁的线程在释放锁之前可见的变量,在获取锁的线程获取锁之后也可以看见这些变量,volatile也一样

锁的实现

  1. 利用volatile的读-写内存语义
  2. 利用CAS(CompareAndSet)附带的volatile读-写内存语义(因为在实现CAS的时候汇编会有一个lock前缀,这个前缀会带来和volatile相同的内存语义)

一些零碎的知识点:

  • JVM在类初始化阶段(Class加载后,且被线程使用前),JVM会获取一个锁,这个锁可以同步多个线程对同一个类的初始化

锁参考

CAS参考