Skip to content
java原子操作和同步
2025年3月13日 root

原子操作 ​

  • 除了 long 和 double 之外的基本类型(int, byte, boolean, short, char, float)的读/写操作, 都天然的具备原子性;

  • 所有引用 reference 的读/写操作;

  • 加了 volatile 后, 所有变量的读/写操作(包含 long 和 double). 这也就意味着 long 和 double 加了 volatile 关键字之后, 对它们的读写操作同样具备原子性;

  • 在 java.concurrent.Atomic 包中的一部分类的一部分方法是具备原子性的, 比如 AtomicInteger 的 incrementAndGet 方法.

  • long 和 double 的值需要占用 64 位的内存空间, 而对于 64 位值的写入, 可以分为两个 32 位的操作来进行.

synchronized ​

synchronized 是 java 一个内置关键字. synchronized 的使用三个地方:

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

synchronized 是基于原子性的内部锁机制,是可重入的. 也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的.

CAS ​

英文全称是 Compare-And-Swap, 中文叫做"比较并交换" CAS 有三个操作数:内存值 V, 预期值 A, 要修改的值 B. CAS 最核心的思路就是, 仅当预期值 A 和当前的内存值 V 相同时, 才将内存值修改为 B.

优点 ​

CAS 的优点是利用了计算机 CAS 指令来实现此功能, 避免加互斥锁, 可以提高程序的运行效率

缺点 ​

CAS 最大的缺点就是 ABA 问题, CAS 并不能检测出在此期间值是不是被修改过, 它只能检查出现在的值和最初的值是不是一样.
由于单次 CAS 不一定能执行成功, 所以 CAS 往往是配合着循环来实现的, 有的时候甚至是死循环, 不停地进行重试, 直到线程竞争不激烈的时候, 才能修改成功, 因此在在高并发的场景下, 通常 CAS 的效率是不高的

常见类 ​

ConcurrentHashMap, ConcurrentLinkedQueue, AtomicInteger 都使用了CAS

AQS ​

  • AQS(AbstractQueuedSynchronizer), 是一个用于构建锁, 同步器等线程协作工具类的框架
  • AQS 最核心的三大部分就是状态, 队列和期望协作工具类去实现的获取/释放等重要方法, 其中状态管理是利用 volatile 特性 + 变量只读取和赋值实现
  • ReentrantLock, ReentrantReadWriteLock, Semaphore, CountDownLatch, ThreadPoolExcutor 的 Worker 中都有运用 AQS

ReentrantLock ​

ReentrantLock 由于使用了 volatile 特性, 比 synchronized 更适合复杂的并发场景, 同时还可以定义为公平锁或者非公平锁. new 一个 ReentrantLock 的时候参数为 true,表明实现公平锁. 每个线程获取锁的概率是一致的. 非公平锁就是随机的获取,谁运气好,cpu 时间片轮到哪个线程,那个线程就能获取锁。

java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    private static final Lock lock = new ReentrantLock();
    public static void main(String[] args) {
       new Thread(() -> test(), "线程A").start();
       new Thread(() -> test(), "线程B").start();
    }
    public static void test() {
       for (int i = 0; i < 2; i++) {
          lock.lock();
          System.out.println(Thread.currentThread().getName() + "获取了锁");
           try {
             TimeUnit.SECONDS.sleep(2);
           } catch (InterruptedException e) {
              e.printStackTrace();
           } finally {
              lock.unlock();
           }
       }
    }
}

synchronized 与 Lock(xxxLock) 的区别 ​

  • Lock 是一个接口,而 synchronized 是关键字,底层使用monitorenter和monitorexit实现。
  • synchronized 会自动释放锁,而 Lock 必须手动释放锁。
  • synchronized 不可中断,除非运行完成或抛出异常。lock 可以设置超时方法trylock 或lockInterruptibly()放代码块中,调用 interrup()方法中断
  • synchronized 非公平,ReentrantLock 默认非公平,可以显示设置公平锁
  • synchronized 能锁住类、方法和代码块,而 Lock 是块范围内的

死锁 ​

发生死锁的 4 个必要条件:

  • 互斥条件, 每个资源每次只能被一个线程(或进程, 下同)使用
  • 请求与保持条件, 当一个线程因请求资源而阻塞时, 则需对已获得的资源保持不放
  • 不剥夺条件, 指线程已获得的资源, 在未使用完之前, 不会被强行剥夺
  • 循环等待条件, 只有若干线程之间形成一种头尾相接的循环等待资源关系时, 才有可能形成死锁

定位死锁 ​

shell
jps
jstack pid

展示的 deadlock 相关内容, 有定位到代码行

Last updated: