Skip to content

happens-before

独享模式和共享模式

在 Java 并发编程中,尝试以共享模式获取对象状态通常是指在多个线程之间共享访问资源时的一种机制。 这种机制是通过使用 AbstractQueuedSynchronizer(AQS)框架实现的,它是构建锁和其他同步组件的基础。AQS 定义了两种资源共享方式:独占模式和共享模式。

独占模式(Exclusive Mode)

独占模式意味着某一时刻只有一个线程可以获取资源。ReentrantLock 是一个典型的使用独占模式的同步器,确保一次只有一个线程可以执行临界区代码。

共享模式(Shared Mode)

共享模式允许多个线程同时获取某种资源,适用于读-写锁等场合,其中读操作可以同时由多个线程执行 ,而写操作则需要独占访问。在共享模式下,tryAcquireShared(int arg) 方法是 AQS 提供给开发者用来实现共享资源访问控制的关键方法。 此方法尝试以共享方式获取对象状态,返回值指示获取是否成功以及后续操作(如线程是否应该被阻塞)。

  • 如果返回值大于等于 0,则表示线程成功获取了共享资源,且后续等待的线程可以尝试继续获取。
  • 如果返回值小于 0,则表示线程获取共享资源失败,且该线程将会被加入到等待队列中,直到其他线程释放资源。

示例:读-写锁(ReadWriteLock)

一个典型的使用共享模式的例子是读-写锁 ReadWriteLock,它有两个锁:一个读锁和一个写锁。通过分离读操作和写操作的锁定,它允许多个读操作并发进行,只要没有线程正在执行写操作。这是通过在共享模式下获取读锁实现的,而写锁则是在独占模式下获取。

结论

尝试在共享模式下获取对象状态是 Java 并发编程中一种重要的同步机制,允许开发者在构建可以由多个线程并发访问的同步组件时,实现更细粒度的控制。这种机制特别适合于读多写少的场景,可以显著提高程序的并发性能和吞吐量。

公平锁 非公平锁

公平锁(Fair Lock)和非公平锁(Nonfair Lock)是计算机科学中同步机制的两种不同策略,它们定义了多线程环境下线程获取锁的行为方式。

公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,类似于现实生活中的排队等待。在公平锁的控制下,如果一个锁已经被其他线程持有,那么新到的线程必须加入等待队列。只有队列中的前面的线程释放了锁,后面的线程才能获取锁。这种方式保证了所有线程获取锁的机会是平等的。

公平锁的优点是所有线程都能获得平等执行的机会,避免了饥饿现象。但是,由于在释放锁与获取锁之间需要频繁地进行上下文切换,所以公平锁的吞吐量通常会低于非公平锁。

非公平锁

非公平锁则是指多个线程获取锁的顺序并不一定是按照申请锁的顺序进行的,这意味着新请求的线程可以“插队”,在某些情况下先于等待队列中的线程获取锁。 非公平锁的主要优势在于吞吐量,由于减少了线程之间的切换,执行速度通常比公平锁快。

但是,非公平锁也有其缺点,主要是在高负载下可能导致某些线程饥饿,即等待很长时间都无法获取到锁,因为总是有新的线程插队获取锁。

在Java中的应用

在Java中,ReentrantLock类在构造时可以指定锁是否是公平的。 默认情况下,ReentrantLock使用的是非公平锁,但可以通过传递true给它的构造函数来创建一个公平锁:

java
ReentrantLock fairLock = new ReentrantLock(true); // 创建一个公平锁
ReentrantLock nonfairLock = new ReentrantLock(); // 默认创建一个非公平锁

选择公平锁还是非公平锁取决于你的应用需求,以及对于响应时间和吞吐量的权衡。公平锁提供了更平等的获取锁的机会,而非公平锁则在某些场景下能提供更高的性能。

synchronized

jvm实现

同步一个代码块

java
public void func() {
   synchronized (this) {
   // ...
   }
   }

它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。 对于以下代码,使用 ExecutorService 执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步, 当一个线程进入同步语句块时,另一个线程就必须等待。

java
   public class SynchronizedExample {
   public void func1() {
   synchronized (this) {
   for (int i = 0; i < 10; i++) {
   System.out.print(i + " ");
   }
   }
   }
   }
   public static void main(String[] args) {
   SynchronizedExample e1 = new SynchronizedExample();
   ExecutorService executorService = Executors.newCachedThreadPool();
   executorService.execute(() -> e1.func1());
   executorService.execute(() -> e1.func1());
   }
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

对于以下代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。 从输出结果可以看出,两个线程交叉执行。

java
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
   SynchronizedExample e2 = new SynchronizedExample();
   ExecutorService executorService = Executors.newCachedThreadPool();
   executorService.execute(() -> e1.func1());
   executorService.execute(() -> e2.func1());
   }
   0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

同步一个方法

java
public synchronized void func () {
   // ...
   }

它和同步代码块一样,作用于同一个对象。

同步一个类

java
public void func() {
   synchronized (SynchronizedExample.class) {
   // ...
   }
   }

作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。

java
public class SynchronizedExample {

   public void func2() {
   synchronized (SynchronizedExample.class) {
   for (int i = 0; i < 10; i++) {
   System.out.print(i + " ");
   }
   }
   }
   }
   public static void main(String[] args) {
   SynchronizedExample e1 = new SynchronizedExample();
   SynchronizedExample e2 = new SynchronizedExample();
   ExecutorService executorService = Executors.newCachedThreadPool();
   executorService.execute(() -> e1.func2());
   executorService.execute(() -> e2.func2());
   }
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

同步一个静态方法

java
public synchronized static void fun() {
   // ...
   }

作用于整个类。

reentrantLock

jdk实现 ReentrantLockReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。

java
public class LockExample {

    private Lock lock = new ReentrantLock();

    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            lock.unlock(); // 确保释放锁,从而避免发生死锁。
        }
    }
}
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

俩种锁的区别

  • synchronized是jvm实现的 reentrantLock是jdk实现的一个类
  • synchronized是非公平锁 reentrantLock默认非公平锁,但可以设置为公平锁(在公平锁下,等待时间最长的线程将获得锁的使用权,必须按照申请锁的时间顺序来依次获得锁。)
  • synchronized不可以中断 reentrantLock可以中断(长期获取不到锁的时候,可以选择放弃等待)
  • ReentrantLock可以绑定多个condition条件

悲观锁和乐观锁

悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

CAS Compare and swap (比较和交换)

AtomicInteger

AtomicInteger是一个提供原子操作的int值的类,用于在多线程环境中实现线程安全的操作。这意味着当多个线程尝试同时更新AtomicInteger的值时, AtomicInteger类能保证每个线程的操作都能被正确处理,而不会相互干扰,确保了数据的一致性和完整性。

在Java中,AtomicInteger提供了多种方法来执行诸如自增(increment)、自减(decrement)、添加(add)和设置新值(set)等操作,而无需使用synchronized关键字,这使得代码既安全又易于理解。

以下是一些常用的AtomicInteger方法示例:

  • get(): 返回当前的值。
  • set(int newValue): 设置新的值。
  • getAndIncrement(): 自增并返回旧值。
  • incrementAndGet(): 自增并返回新值。
  • getAndDecrement(): 自减并返回旧值。
  • decrementAndGet(): 自减并返回新值。
  • addAndGet(int delta): 添加指定的值到当前值,并返回新值。
  • compareAndSet(int expect, int update): 如果当前值等于预期值expect,则将值更新为update

AtomicInteger是Java并发包(java.util.concurrent)中的一个重要类,广泛用于开发高性能的并发应用程序。

java
package com.jasper.cas;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(0);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                atomicInteger.incrementAndGet();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                atomicInteger.incrementAndGet();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final value: " + atomicInteger.get());
    }
}

ABA

ABA问题是并发编程中的一个著名问题,主要出现在使用原子操作进行比较并交换(CAS)时。它发生在一个线程在CAS操作中检查到一个值(A),然后去执行一些其他操作, 当这个线程回来准备更新这个值时,它再次检查并发现值仍然是A,然后进行更新。问题在于,这个值虽然在两次检查之间没有变化,但它可能在此期间被其他线程改变过一次或多次——即先从A变成B ,然后又从B变回A。这就是所谓的ABA问题。

ABA问题的影响

ABA问题可能导致一些非预期的行为,特别是在设计依赖于值不变性的算法时。例如,在一个基于锁自由算法的栈实现中,一个线程尝试弹出栈顶元素A,然后由于某些原因被挂起。 在这个线程挂起的时候,另一个线程将A弹出并执行了一些操作,之后再把A(或看起来像A的另一个值)放回了栈顶。当原来的线程恢复执行时,它看到栈顶仍然是A, 误以为自己的弹出操作还没有被执行,然后基于这个错误的假设继续执行,这可能导致数据的不一致或其他错误。

解决ABA问题的方法

为了解决ABA问题,一种常见的策略是使用版本号或时间戳。即每次修改变量时,除了改变变量的值外,还更新一个额外的版本号或时间戳。这样,即使一个值从A变成B,再变回A,版本号也会有所不同。 CAS操作不仅比较值,还比较版本号,只有当两者都匹配时,才会进行更新。这种方法可以有效地防止ABA问题。

在Java中,AtomicStampedReference是解决ABA问题的一个工具。它维护了对象引用及其整数标记,即“时间戳”, 用于在比较和交换时检查对象状态是否发生了变化。通过这种方式,即使对象的引用未改变,但版本号改变了,AtomicStampedReference也能识别出来,从而避免ABA问题。

代码示例

java
package com.jasper.cas;

import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceDemo {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> stampRef = new AtomicStampedReference<>(1, 0);

        System.out.println("stampRef.getReference() = " + stampRef.getReference());
        System.out.println("stampRef.getStamp() = " + stampRef.getStamp());

        boolean isSuccess = stampRef.compareAndSet(1, 2, stampRef.getStamp(), stampRef.getStamp() + 1);
        System.out.println("isSuccess = " + isSuccess);

        System.out.println("stampRef.getReference() = " + stampRef.getReference());
        System.out.println("stampRef.getStamp() = " + stampRef.getStamp());
    }
}

防止库存超卖

java
package com.jasper.cas;

import com.jasper.cas.entity.Inventory;
import com.jasper.cas.mapper.InventoryMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class InventoryControl {
    private AtomicInteger inventory = new AtomicInteger(100);

    @Autowired
    private InventoryMapper inventoryMapper;

    public boolean reduceInventory(int quantity) {
        int currentInventory;
        int newInventory;
        do {
            currentInventory = inventory.get();
            if (quantity > currentInventory) {
                System.out.println("库存不足,无法减少");
                return false;
            }
            newInventory = currentInventory - quantity;
        } while (!inventory.compareAndSet(currentInventory, newInventory));

        // 更新数据库
        Inventory dbInventory = new Inventory();
        dbInventory.setId(1); // 假设库存记录的ID为1
        dbInventory.setQuantity(newInventory);
        inventoryMapper.updateById(dbInventory);

        System.out.println("成功减少库存,当前库存: " + newInventory);
        return true;
    }
}

当只对一个共享变量执行操作时 可以使用cas来保持原子性 但是多个变量时不能保证操作的原子性 需要使用锁来保证原子性

Synchronized和AtomicInteger.compareAndSet区别

在并发编程中,synchronized关键字和AtomicIntegercompareAndSet方法都可以用来 解决线程安全问题,但它们在实现机制和性能上存在明显的差异。

synchronized

  • 机制: synchronized关键字提供了一种互斥机制,确保同一时刻只有一个线程可以执行某个方
  • 法或代码块的内容。当一个线程访问任何一个synchronized方法或代码块时,
  • 它会自动获取锁,并且在离开时释放锁,无论是通过正常路径返回还是通过抛出异常退出。
  • 性能: 使用synchronized可能导致线程阻塞和唤醒,这些操作都需要操作系统的介入,
  • 因此在高竞争环境下可能会降低性能。特别是当锁被频繁地争用时,管理锁的开销可能会变得相当显著。
  • 用途: synchronized更适用于执行时间较长的操作或者需要保护多个变量一致性的场景。

AtomicIntegercompareAndSet

  • 机制: compareAndSet基于CAS(Compare-And-Swap)操作,它是一种CPU并发原语。
  • 它包含三个操作:读取值、比较值、更新值。这一系列操作是作为单个原子操作完成的,不会被中断。
  • 性能: compareAndSet通常比synchronized更轻量级,因为它不涉及线程的阻塞和唤醒,
  • 所有操作都在用户态完成,没有内核态的切换。在低至中等竞争的环境下,CAS通常能提供更好的性能。
  • 然而,在高竞争的环境下,频繁的失败和重试(自旋)可能会导致性能下降。
  • 用途: compareAndSet适用于操作简单、执行快速且只涉及单个变量的原子操作。

结论

  • 适用场景: 如果你正在执行的操作非常快速,并且只涉及单个变量,使用AtomicIntegercompareAndSet可能更高效。它避免了锁的开销,尤其是在低竞争的环境中。对于更复杂的操作,尤其是那些需要保护多个变量或执行较长时间的操作,synchronized可能是更好的选择。
  • 性能考量: 在选择使用synchronized还是compareAndSet时,重要的是要考虑应用的具体需求,包括并发级别、操作的复杂性以及性能要求。在一些情况下,即使compareAndSet在理论上更高效,高竞争的环境也可能使得它不如使用锁那样有效。

在Java并发编程中,选择使用synchronized关键字还是原子类(如AtomicIntegerAtomicReference等)依赖于多个因素,包括应用的具体需求、性能考虑以及代码的复杂度。下面是一些决定使用哪种同步机制的指导原则:

使用synchronized的情况

  1. 复杂操作:当你需要执行一系列操作,且这些操作整体上必须是原子性的时,应该使用synchronized。因为synchronized可以保证一个代码块在执行时,不会被其他线程中断。
  2. 对象级别的锁定:如果你需要对某个对象的状态进行整体的同步控制,或者需要调用某个对象的多个方法并保持对象状态的一致性,那么使用synchronized是合适的。
  3. 等待/通知机制:当你的并发模型需要等待/通知机制(wait()notify()notifyAll()方法)来协调线程间的工作时,synchronized是必须的,因为这些方法只能在同步代码块或同步方法中调用。

使用原子类的情况

  1. 单一变量的原子操作:如果你需要对单个变量进行原子操作(如增加、减少、比较并交换等),并且没有其他复杂的逻辑控制,使用原子类会更简单且效率更高。
  2. 减少锁的竞争:在高并发环境下,如果多个线程频繁访问同一资源,使用synchronized可能会导致显著的性能问题。原子类通常提供了一种更轻量级的同步机制,可以减少锁的竞争。
  3. 无锁编程需求:原子类利用CAS(Compare-And-Swap)操作,提供了一种无锁的线程安全实现,这在某些场景下可以提高性能,尤其是当更新操作不频繁阻塞时。

性能和简洁性

  • 性能:原子类通常比synchronized更轻量级,尤其是在只涉及单一变量或简单操作时。但在高竞争环境下,频繁的CAS操作可能会导致性能下降。
  • 简洁性:原子类的使用通常比synchronized简洁,因为它们直接提供了一系列原子操作的API,而不需要写额外的同步代码块。

结论

选择synchronized还是原子类取决于具体情况。如果操作简单且局限于单个变量,那么原子类是一个好选择;如果涉及到复杂的操作或多个变量,或者需要对象级的锁定和等待/通知机制,那么synchronized可能是更好的选择。在实践中,两者经常被结合使用,以达到最优的并发性能和代码的清晰度。

锁的优化和升级

锁的优化和升级