Halo
发布于 2022-05-13 / 143 阅读 / 0 评论 / 0 点赞

java原子操作和同步

原子操作

  • 除了 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 时间片轮到哪个线程,那个线程就能获取锁。

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 个必要条件:

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

定位死锁

jps
jstack pid

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


评论